Skip to content

Commit

Permalink
fix(sdk-coin-ada): add separate UTXOs for each token with 1 ADA
Browse files Browse the repository at this point in the history
Ticket: WIN-736
  • Loading branch information
Vijay-Jagannathan committed Oct 23, 2023
1 parent f65ff46 commit 05ec950
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 62 deletions.
81 changes: 53 additions & 28 deletions modules/sdk-coin-ada/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,34 +257,59 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
});
}

const changeOutput = CardanoWasm.TransactionOutput.new(changeAddress, CardanoWasm.Value.new(change));
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);
});
// If totalAmountToSend is 0, its consolidation
if (totalAmountToSend.to_str() == '0') {
// support for multi-asset consolidation
if (this._multiAssets !== undefined) {
const totalNumberOfAssets = CardanoWasm.BigNum.from_str(this._multiAssets.length.toString());
const minAmountNeededForOneAssetOutput = CardanoWasm.BigNum.from_str('1000000');
const minAmountNeededForTotalAssetOutputs = minAmountNeededForOneAssetOutput.checked_mul(totalNumberOfAssets);

if (!change.less_than(minAmountNeededForTotalAssetOutputs)) {
this._multiAssets.forEach((asset) => {
let txOutputBuilder = CardanoWasm.TransactionOutputBuilder.new();
// changeAddress is the root address, which is where we want the tokens assets to be sent to
const toAddress = CardanoWasm.Address.from_bech32(this._changeAddress);
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);

txOutputAmountBuilder = txOutputAmountBuilder.with_coin_and_asset(
minAmountNeededForOneAssetOutput,
multiAsset
);

const txOutput = txOutputAmountBuilder.build();
outputs.add(txOutput);
});

// finally send the remaining ADA in its own output
const remainingOutputAmount = change.checked_sub(minAmountNeededForTotalAssetOutputs);
const changeOutput = CardanoWasm.TransactionOutput.new(
changeAddress,
CardanoWasm.Value.new(remainingOutputAmount)
);
outputs.add(changeOutput);
} else {
throw new BuildTransactionError(
'Insufficient funds: need a minimum of 1 ADA per output to construct token consolidation'
);
}
} else {
// If there are no tokens to consolidate, you only have 1 output which is ADA alone
const changeOutput = CardanoWasm.TransactionOutput.new(changeAddress, CardanoWasm.Value.new(change));
outputs.add(changeOutput);
}
} else {
// If this isn't a consolidate request, whatever change that needs to be sent back to the rootaddress is added as a separate output here
const changeOutput = CardanoWasm.TransactionOutput.new(changeAddress, CardanoWasm.Value.new(change));
outputs.add(changeOutput);
}
}

const txRaw = CardanoWasm.TransactionBody.new_tx_body(inputs, outputs, this._fee);
Expand Down
11 changes: 7 additions & 4 deletions modules/sdk-coin-ada/test/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,18 @@ export const rawTx = {
unsignedTx2:
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba210101828258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f1a00775f1182581d60ce3edb7ad0f096553830096453e97919efc0962ed9d09a3a2c82c5e11a00c6ffe9021a00028d5d031a2faf08000480a10080f5f6',
unsignedTx3:
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba210101838258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f1a00775f1182581d60ce3edb7ad0f096553830096453e97919efc0962ed9d09a3a2c82c5e11a00c6ffe98258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f8200a1581c279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3fa144534e454b1a005b8d80021a00028d5d031a2faf08000480a10080f5f6',
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba210101828258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f821a000f4240a1581c279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3fa144534e454b1a005b8d808258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f1a011f63bf021a00028701031a2faf08000480a10080f5f6',
unsignedTx4:
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba210101848258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f1a00775f1182581d60ce3edb7ad0f096553830096453e97919efc0962ed9d09a3a2c82c5e11a00c6ffe98258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f8200a1581c279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3fa144534e454b1a005b8d808258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f8200a1581c1f7a58a1aa1e6b047a42109ade331ce26c9c2cce027d043ff264fb1fa146425249434b531a004c4b40021a00028d5d031a2faf08000480a10080f5f6',
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba210101838258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f821a000f4240a1581c279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3fa144534e454b1a005b8d808258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f821a000f4240a1581c1f7a58a1aa1e6b047a42109ade331ce26c9c2cce027d043ff264fb1fa146425249434b531a004c4b408258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f1a0110217f021a00028701031a2faf08000480a10080f5f6',
unsignedTx5:
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba210101818258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f1a012ea5ff021a00028701031a2faf08000480a10080f5f6',
signedTx2:
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba210101828258390027360563c4479c6aa054cb2bd3ca9e394731ab59f8c45511ec8ba851aee1672f3f7fedc48feca58979967030dc8edc340c551b49d067638f1a00775f1182581d60ce3edb7ad0f096553830096453e97919efc0962ed9d09a3a2c82c5e11a00c6ffe9021a00028d5d031a2faf08000480a10081825820a5cdaab58f8c0cb82897b855b8a2315fba8061122072fcae1e0790641d9f9e675840765ee4f52bd3a2ae85a7d921ab2068698d70cf4ff291af2cf56f0858548d9e30c37b4f3e44c39ca885f18eb3c3648cbcaf30d1acb650d0a9c7d377591a214404f5f6',
txHash: '0933ee2669649595c39150cdad64418303744352e1d315aa2f060f291980639a',
txHash2: '1088141814e014e07d5e6c3ffb6c877a5c6ee2210694570e01bfc9a6ee6eedf5',
txHash3: 'e00f11e664ebb12759c413eeeb00e3bf42a6e53849316c850ddea5dbbab10d32',
txHash4: '9913769cf5df070f7c561d3bfca81b2507110f9505ef787d02aeed5280f5e44c',
txHash3: '5c45b86ce5df2f4aa20aa38ffb8428b9cc09ad8b6de0b30e81dea6ef786723e7',
txHash4: '1abec9dd3798daf69245fa99bd29729de868113815641298b86735b29435e4a3',
txHash5: '65c0b30500a9283fd787bdcd98c2a44cbeaeb3c3768e36f555adb257c00c9299',
outputAddress1: {
address:
'addr_test1qqnnvptrc3rec64q2n9jh572ncu5wvdtt8uvg4g3aj96s5dwu9nj70mlahzglm9939uevupsmj8dcdqv25d5n5r8vw8sn7prey',
Expand Down
115 changes: 85 additions & 30 deletions modules/sdk-coin-ada/test/unit/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,41 @@ describe('ADA Transaction Builder', async () => {
should.equal(txBroadcast, testData.rawTx.unsignedTx2);
});

it('build a tx with single asset', async () => {
it('should build a consolidate tx with no 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 outputAmount = 0;
const totalInput = 20000000;
txBuilder.changeAddress(testData.rawTx.outputAddress1.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(1);
txData.outputs[0].address.should.equal(testData.rawTx.outputAddress1.address);
const fee = tx.getFee;
txData.outputs[0].amount.should.equal((totalInput - outputAmount - Number(fee)).toString());
fee.should.equal('165633');
txData.id.should.equal(testData.rawTx.txHash5);
const txBroadcast = tx.toBroadcastFormat();
should.equal(txBroadcast, testData.rawTx.unsignedTx5);
});

it('should build a consolidate tx with single asset', async () => {
const txBuilder = factory.getTransferBuilder();
txBuilder.input({
transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba21',
transaction_index: 1,
});

const outputAmount = 0;
const policyId = '279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f';
const assetName = '534e454b';
const quantity = '6000000';
Expand All @@ -58,48 +82,48 @@ describe('ADA Transaction Builder', async () => {
asset_name: assetName,
quantity: quantity,
});
const totalInput = 21032023;
txBuilder.changeAddress(testData.rawTx.outputAddress2.address, totalInput.toString());
const minAmountForSingleAsset = 1000000;
const totalInput = 20000000;
txBuilder.changeAddress(testData.rawTx.outputAddress1.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.length.should.equal(2);
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);
txData.outputs[1].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)
txData.outputs[0].amount.should.equal(minAmountForSingleAsset.toString());
txData.outputs[0].should.have.property('multiAssets');
(txData.outputs[0].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.outputs[1].amount.should.equal(
(totalInput - minAmountForSingleAsset - outputAmount - Number(fee)).toString()
);
fee.should.equal('165633');
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 () => {
it('should build a consolidate 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 outputAmount = 0;
const asset1_policyId = '279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f';
const asset1_assetName = '534e454b';
const asset1_quantity = '6000000';
Expand All @@ -118,44 +142,75 @@ describe('ADA Transaction Builder', async () => {
quantity: asset2_quantity,
});

const totalInput = 21032023;
txBuilder.changeAddress(testData.rawTx.outputAddress2.address, totalInput.toString());
const totalInput = 20000000;
const minAmountForSingleAsset = 1000000;
txBuilder.changeAddress(testData.rawTx.outputAddress1.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.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[1].address.should.equal(testData.rawTx.outputAddress1.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)
txData.outputs[0].amount.should.equal(minAmountForSingleAsset.toString());
txData.outputs[0].should.have.property('multiAssets');
(txData.outputs[0].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)
txData.outputs[1].amount.should.equal(minAmountForSingleAsset.toString());
txData.outputs[1].should.have.property('multiAssets');
(txData.outputs[1].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.outputs[2].amount.should.equal(
(totalInput - minAmountForSingleAsset * 2 - outputAmount - Number(fee)).toString()
);
fee.should.equal('165633');
txData.id.should.equal(testData.rawTx.txHash4);
const txBroadcast = tx.toBroadcastFormat();
should.equal(txBroadcast, testData.rawTx.unsignedTx4);
});

it('should fail to build a consolidate tx with single asset and insufficient minimum ADA', async () => {
const txBuilder = factory.getTransferBuilder();
txBuilder.input({
transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba21',
transaction_index: 1,
});

const policyId = '279c909f348e533da5808898f87f9a14bb2c3dfbbacccd631d927a3f';
const assetName = '534e454b';
const quantity = '6000000';
txBuilder.assets({
policy_id: policyId,
asset_name: assetName,
quantity: quantity,
});

// change = input amt - fee - output amt => change will be less than 1000000
// even if one output has less than 1 ADA, tx will fail
const totalInput = 1000000;
txBuilder.changeAddress(testData.rawTx.outputAddress1.address, totalInput.toString());
txBuilder.ttl(800000000);
await txBuilder
.build()
.should.rejectedWith('Insufficient funds: need a minimum of 1 ADA per output to construct token consolidation');
});

it('build and sign a transfer tx', async () => {
const txBuilder = factory.getTransferBuilder();
txBuilder.input({
Expand Down

0 comments on commit 05ec950

Please sign in to comment.