Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sdk-coin-ada): add separate UTXOs for each token with 1 ADA #4007

Merged
merged 1 commit into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading