From c2e3d3e63cbe5f2f040b52844b58f7988969e291 Mon Sep 17 00:00:00 2001 From: Vijay-Jagannathan Date: Tue, 17 Oct 2023 23:58:20 +0530 Subject: [PATCH] feat(sdk-coin-ada): add support for multi asset in tx building Ticket: WIN-736 --- modules/sdk-coin-ada/src/lib/transaction.ts | 8 ++ .../src/lib/transactionBuilder.ts | 34 ++++- modules/sdk-coin-ada/test/resources/index.ts | 6 + .../test/unit/transactionBuilder.ts | 117 ++++++++++++++++++ 4 files changed, 164 insertions(+), 1 deletion(-) diff --git a/modules/sdk-coin-ada/src/lib/transaction.ts b/modules/sdk-coin-ada/src/lib/transaction.ts index 196537d922..924c0c2992 100644 --- a/modules/sdk-coin-ada/src/lib/transaction.ts +++ b/modules/sdk-coin-ada/src/lib/transaction.ts @@ -15,9 +15,16 @@ export interface TransactionInput { transaction_index: number; } +export interface Asset { + policy_id: string; + asset_name: string; + quantity: string; +} + export interface TransactionOutput { address: string; amount: string; + multiAssets?: CardanoWasm.MultiAsset; } export interface Witness { @@ -140,6 +147,7 @@ export class Transaction extends BaseTransaction { result.outputs.push({ address: output.address().to_bech32(), amount: output.amount().coin().to_str(), + multiAssets: output.amount().multiasset() || undefined, }); } diff --git a/modules/sdk-coin-ada/src/lib/transactionBuilder.ts b/modules/sdk-coin-ada/src/lib/transactionBuilder.ts index a46555c03a..486c47e55b 100644 --- a/modules/sdk-coin-ada/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-ada/src/lib/transactionBuilder.ts @@ -10,7 +10,7 @@ import { TransactionType, UtilsError, } from '@bitgo/sdk-core'; -import { Transaction, TransactionInput, TransactionOutput, Withdrawal } from './transaction'; +import { Asset, Transaction, TransactionInput, TransactionOutput, Withdrawal } from './transaction'; import { KeyPair } from './keyPair'; import util from './utils'; import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; @@ -29,6 +29,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { protected _certs: CardanoWasm.Certificate[] = []; protected _withdrawals: Withdrawal[] = []; protected _type: TransactionType; + protected _multiAssets: Asset[] = []; private _fee: BigNum; constructor(_coinConfig: Readonly) { @@ -47,6 +48,11 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { return this; } + assets(a: Asset): this { + this._multiAssets.push(a); + return this; + } + ttl(t: number): this { this._ttl = t; return this; @@ -255,6 +261,32 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { outputs.add(changeOutput); } + // support for multi-asset consolidation + if (this._multiAssets !== undefined) { + this._multiAssets.forEach((asset) => { + let txOutputBuilder = CardanoWasm.TransactionOutputBuilder.new(); + const toAddress = CardanoWasm.Address.from_bech32(this._transactionOutputs[0].address); + txOutputBuilder = txOutputBuilder.with_address(toAddress); + let txOutputAmountBuilder = txOutputBuilder.next(); + const assetName = CardanoWasm.AssetName.new(Buffer.from(asset.asset_name, 'hex')); + const policyId = CardanoWasm.ScriptHash.from_bytes(Buffer.from(asset.policy_id, 'hex')); + const multiAsset = CardanoWasm.MultiAsset.new(); + const assets = CardanoWasm.Assets.new(); + assets.insert(assetName, CardanoWasm.BigNum.from_str(asset.quantity)); + multiAsset.insert(policyId, assets); + + // coin value should be zero since this output is related to token + const coinValue = '0'; + txOutputAmountBuilder = txOutputAmountBuilder.with_coin_and_asset( + CardanoWasm.BigNum.from_str(coinValue), + multiAsset + ); + + const txOutput = txOutputAmountBuilder.build(); + outputs.add(txOutput); + }); + } + const txRaw = CardanoWasm.TransactionBody.new_tx_body(inputs, outputs, this._fee); const certs = CardanoWasm.Certificates.new(); diff --git a/modules/sdk-coin-ada/test/resources/index.ts b/modules/sdk-coin-ada/test/resources/index.ts index f1ef1ed6fd..058f9d284b 100644 --- a/modules/sdk-coin-ada/test/resources/index.ts +++ b/modules/sdk-coin-ada/test/resources/index.ts @@ -119,10 +119,16 @@ export const rawTx = { 'a40081825820a71708d13fd0f143dd492540c0ec5fd85011860c2c8823c1facd70afd4d6e15a0101828258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f1a004c4b4082581d60ce3edb7ad0f096553830096453e97919efc0962ed9d09a3a2c82c5e11a0ecd33be0200031a03ba7680', unsignedTx2: '84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba210101828258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f1a00775f1182581d60ce3edb7ad0f096553830096453e97919efc0962ed9d09a3a2c82c5e11a00c6ffe9021a00028d5d031a2faf08000480a10080f5f6', + unsignedTx3: + '84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba210101838258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f1a00775f1182581d60ce3edb7ad0f096553830096453e97919efc0962ed9d09a3a2c82c5e11a00c6ffe98258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f8200a1581c279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3fa144534e454b1a005b8d80021a00028d5d031a2faf08000480a10080f5f6', + unsignedTx4: + '84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba210101848258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f1a00775f1182581d60ce3edb7ad0f096553830096453e97919efc0962ed9d09a3a2c82c5e11a00c6ffe98258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f8200a1581c279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3fa144534e454b1a005b8d808258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f8200a1581c1f7a58a1aa1e6b047a42109ade331ce26c9c2cce027d043ff264fb1fa146425249434b531a004c4b40021a00028d5d031a2faf08000480a10080f5f6', signedTx2: '84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba210101828258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f1a00775f1182581d60ce3edb7ad0f096553830096453e97919efc0962ed9d09a3a2c82c5e11a00c6ffe9021a00028d5d031a2faf08000480a10081825820a5cdaab58f8c0cb82897b855b8a2315fba8061122072fcae1e0790641d9f9e675840765ee4f52bd3a2ae85a7d921ab2068698d70cf4ff291af2cf56f0858548d9e30c37b4f3e44c39ca885f18eb3c3648cbcaf30d1acb650d0a9c7d377591a214404f5f6', txHash: '0933ee2669649595c39150cdad64418303744352e1d315aa2f060f291980639a', txHash2: '1088141814e014e07d5e6c3ffb6c877a5c6ee2210694570e01bfc9a6ee6eedf5', + txHash3: 'e00f11e664ebb12759c413eeeb00e3bf42a6e53849316c850ddea5dbbab10d32', + txHash4: '9913769cf5df070f7c561d3bfca81b2507110f9505ef787d02aeed5280f5e44c', outputAddress1: { address: 'addr_test1qqnnvptrc3rec64q2n9jh572ncu5wvdtt8uvg4g3aj96s5dwu9nj70mlahzglm9939uevupsmj8dcdqv25d5n5r8vw8sn7prey', diff --git a/modules/sdk-coin-ada/test/unit/transactionBuilder.ts b/modules/sdk-coin-ada/test/unit/transactionBuilder.ts index 350d006ee9..e85596d392 100644 --- a/modules/sdk-coin-ada/test/unit/transactionBuilder.ts +++ b/modules/sdk-coin-ada/test/unit/transactionBuilder.ts @@ -39,6 +39,123 @@ describe('ADA Transaction Builder', async () => { should.equal(txBroadcast, testData.rawTx.unsignedTx2); }); + it('build a tx with single asset', async () => { + const txBuilder = factory.getTransferBuilder(); + txBuilder.input({ + transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba21', + transaction_index: 1, + }); + const outputAmount = 7823121; + txBuilder.output({ + address: testData.rawTx.outputAddress1.address, + amount: outputAmount.toString(), + }); + const policyId = '279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f'; + const assetName = '534e454b'; + const quantity = '6000000'; + txBuilder.assets({ + policy_id: policyId, + asset_name: assetName, + quantity: quantity, + }); + const totalInput = 21032023; + txBuilder.changeAddress(testData.rawTx.outputAddress2.address, totalInput.toString()); + txBuilder.ttl(800000000); + const tx = (await txBuilder.build()) as Transaction; + should.equal(tx.type, TransactionType.Send); + const txData = tx.toJson(); + txData.witnesses.length.should.equal(0); + txData.certs.length.should.equal(0); + txData.withdrawals.length.should.equal(0); + txData.outputs.length.should.equal(3); + txData.outputs[0].address.should.equal(testData.rawTx.outputAddress1.address); + txData.outputs[1].address.should.equal(testData.rawTx.outputAddress2.address); + txData.outputs[2].address.should.equal(testData.rawTx.outputAddress1.address); + + // token assertion + const expectedAssetName = CardanoWasm.AssetName.new(Buffer.from(assetName, 'hex')); + const expectedPolicyId = CardanoWasm.ScriptHash.from_bytes(Buffer.from(policyId, 'hex')); + txData.outputs[2].should.have.property('multiAssets'); + (txData.outputs[2].multiAssets as CardanoWasm.MultiAsset) + .get_asset(expectedPolicyId, expectedAssetName) + .to_str() + .should.equal(quantity); + + const fee = tx.getFee; + txData.outputs[1].amount.should.equal((totalInput - outputAmount - Number(fee)).toString()); + fee.should.equal('167261'); + txData.id.should.equal(testData.rawTx.txHash3); + const txBroadcast = tx.toBroadcastFormat(); + should.equal(txBroadcast, testData.rawTx.unsignedTx3); + }); + + it('build a tx with multiple assets', async () => { + const txBuilder = factory.getTransferBuilder(); + txBuilder.input({ + transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba21', + transaction_index: 1, + }); + const outputAmount = 7823121; + txBuilder.output({ + address: testData.rawTx.outputAddress1.address, + amount: outputAmount.toString(), + }); + const asset1_policyId = '279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f'; + const asset1_assetName = '534e454b'; + const asset1_quantity = '6000000'; + const asset2_policyId = '1f7a58a1aa1e6b047a42109ade331ce26c9c2cce027d043ff264fb1f'; + const asset2_assetName = '425249434b53'; + const asset2_quantity = '5000000'; + + txBuilder.assets({ + policy_id: asset1_policyId, + asset_name: asset1_assetName, + quantity: asset1_quantity, + }); + txBuilder.assets({ + policy_id: asset2_policyId, + asset_name: asset2_assetName, + quantity: asset2_quantity, + }); + + const totalInput = 21032023; + txBuilder.changeAddress(testData.rawTx.outputAddress2.address, totalInput.toString()); + txBuilder.ttl(800000000); + const tx = (await txBuilder.build()) as Transaction; + should.equal(tx.type, TransactionType.Send); + const txData = tx.toJson(); + txData.witnesses.length.should.equal(0); + txData.certs.length.should.equal(0); + txData.withdrawals.length.should.equal(0); + txData.outputs.length.should.equal(4); + txData.outputs[0].address.should.equal(testData.rawTx.outputAddress1.address); + txData.outputs[1].address.should.equal(testData.rawTx.outputAddress2.address); + txData.outputs[2].address.should.equal(testData.rawTx.outputAddress1.address); + + // token assertion + const asset1_expectedAssetName = CardanoWasm.AssetName.new(Buffer.from(asset1_assetName, 'hex')); + const asset1_expectedPolicyId = CardanoWasm.ScriptHash.from_bytes(Buffer.from(asset1_policyId, 'hex')); + txData.outputs[2].should.have.property('multiAssets'); + (txData.outputs[2].multiAssets as CardanoWasm.MultiAsset) + .get_asset(asset1_expectedPolicyId, asset1_expectedAssetName) + .to_str() + .should.equal(asset1_quantity); + const asset2_expectedAssetName = CardanoWasm.AssetName.new(Buffer.from(asset2_assetName, 'hex')); + const asset2_expectedPolicyId = CardanoWasm.ScriptHash.from_bytes(Buffer.from(asset2_policyId, 'hex')); + txData.outputs[3].should.have.property('multiAssets'); + (txData.outputs[3].multiAssets as CardanoWasm.MultiAsset) + .get_asset(asset2_expectedPolicyId, asset2_expectedAssetName) + .to_str() + .should.equal(asset2_quantity); + + const fee = tx.getFee; + txData.outputs[1].amount.should.equal((totalInput - outputAmount - Number(fee)).toString()); + fee.should.equal('167261'); + txData.id.should.equal(testData.rawTx.txHash4); + const txBroadcast = tx.toBroadcastFormat(); + should.equal(txBroadcast, testData.rawTx.unsignedTx4); + }); + it('build and sign a transfer tx', async () => { const txBuilder = factory.getTransferBuilder(); txBuilder.input({