Skip to content

Commit

Permalink
Merge pull request #3994 from BitGo/add-support-for-multi-asset
Browse files Browse the repository at this point in the history
feat(sdk-coin-ada): add support for multi asset in tx building
  • Loading branch information
Vijay-Jagannathan authored Oct 19, 2023
2 parents 25ade2c + c2e3d3e commit 844aae4
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 1 deletion.
8 changes: 8 additions & 0 deletions modules/sdk-coin-ada/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
});
}

Expand Down
34 changes: 33 additions & 1 deletion modules/sdk-coin-ada/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<CoinConfig>) {
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions modules/sdk-coin-ada/test/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
117 changes: 117 additions & 0 deletions modules/sdk-coin-ada/test/unit/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down

0 comments on commit 844aae4

Please sign in to comment.