Skip to content

Commit 946357c

Browse files
committed
fix(sdk-coin-ada): unsigned sweep support for WRW with asset list
Ticket: CSHLD-134 Made-with: Cursor
1 parent d7d12b7 commit 946357c

3 files changed

Lines changed: 125 additions & 11 deletions

File tree

modules/sdk-coin-ada/src/ada.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
TssVerifyAddressOptions,
3636
} from '@bitgo/sdk-core';
3737
import { KeyPair as AdaKeyPair, Transaction, TransactionBuilderFactory, Utils } from './lib';
38+
import type { Asset } from './lib/transaction';
3839
import { BaseCoin as StaticsBaseCoin, CoinFamily, coins } from '@bitgo/statics';
3940
import adaUtils from './lib/utils';
4041
import * as request from 'superagent';
@@ -499,21 +500,25 @@ export class Ada extends BaseCoin {
499500
const transactionPrebuild = { txHex: serializedTx };
500501
const parsedTx = await this.parseTransaction({ txPrebuild: transactionPrebuild });
501502
const walletCoin = this.getChain();
502-
const output = (parsedTx.outputs as ITransactionRecipient)[0];
503+
const parsedOutputs = parsedTx.outputs as (ITransactionRecipient & { multiAssets?: Asset[] })[];
504+
const output = parsedOutputs[0];
505+
// All tokens from the spent UTXOs — shown on the input so WRW can display what is being swept
506+
const inputAssetList = Object.values(aggregatedAssetList);
503507
const inputs = [
504508
{
505509
address: senderAddr,
506510
valueString: output.amount,
507-
value: new BigNumber(output.amount).toNumber(),
508-
},
509-
];
510-
const outputs = [
511-
{
512-
address: output.address,
513-
valueString: output.amount,
514-
coinName: walletCoin,
511+
value: new BigNumber(output.amount as string).toNumber(),
512+
...(inputAssetList.length > 0 && { multiAssets: inputAssetList }),
515513
},
516514
];
515+
// assetList per output comes from explainTransaction which parses multiAssets
516+
const outputs = parsedOutputs.map((o) => ({
517+
address: o.address,
518+
valueString: o.amount,
519+
coinName: walletCoin,
520+
...(o.multiAssets && { multiAssets: o.multiAssets }),
521+
}));
517522
const spendAmount = output.amount;
518523
const completedParsedTx = { inputs: inputs, outputs: outputs, spendAmount: spendAmount, type: '' };
519524
const fee = new BigNumber((parsedTx.fee as { fee: string }).fee);

modules/sdk-coin-ada/src/lib/transaction.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ export class Transaction extends BaseTransaction {
381381

382382
/** @inheritdoc */
383383
explainTransaction(): {
384-
outputs: { amount: string; address: string }[];
384+
outputs: { amount: string; address: string; multiAssets?: Asset[] }[];
385385
certificates: Cert[];
386386
changeOutputs: string[];
387387
outputAmount: string;
@@ -414,7 +414,14 @@ export class Transaction extends BaseTransaction {
414414
return {
415415
displayOrder,
416416
id: txJson.id,
417-
outputs: txJson.outputs.map((o) => ({ address: o.address, amount: o.amount })),
417+
outputs: txJson.outputs.map((o) => {
418+
const multiAssets = Transaction.parseMultiAssets(o.multiAssets);
419+
return {
420+
address: o.address,
421+
amount: o.amount,
422+
...(multiAssets && { multiAssets }),
423+
};
424+
}),
418425
outputAmount: outputAmount,
419426
changeOutputs: [],
420427
changeAmount: '0',
@@ -426,6 +433,29 @@ export class Transaction extends BaseTransaction {
426433
};
427434
}
428435

436+
private static parseMultiAssets(multiAssets: CardanoWasm.MultiAsset | undefined): Asset[] | undefined {
437+
if (!multiAssets) return undefined;
438+
const list: Asset[] = [];
439+
const scriptHashes = multiAssets.keys();
440+
for (let i = 0; i < scriptHashes.len(); i++) {
441+
const scriptHash = scriptHashes.get(i);
442+
const assets = multiAssets.get(scriptHash);
443+
if (!assets) continue;
444+
const assetNames = assets.keys();
445+
for (let j = 0; j < assetNames.len(); j++) {
446+
const assetName = assetNames.get(j);
447+
const quantity = assets.get(assetName);
448+
if (!quantity) continue;
449+
list.push({
450+
policy_id: Buffer.from(scriptHash.to_bytes()).toString('hex'),
451+
asset_name: Buffer.from(assetName.name()).toString('hex'),
452+
quantity: quantity.to_str(),
453+
});
454+
}
455+
}
456+
return list.length > 0 ? list : undefined;
457+
}
458+
429459
getPledgeDetails(): PledgeDetails | undefined {
430460
return this._pledgeDetails;
431461
}

modules/sdk-coin-ada/test/unit/ada.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,85 @@ describe('ADA', function () {
674674
should.deepEqual(Number(txJson.outputs[0].amount) + fee, testnetUTXO.UTXO_1.value);
675675
});
676676

677+
it('should recover ADA plus token UTXOs - token and ADA both appear in outputs (unsigned sweep)', async function () {
678+
callBack
679+
.withArgs('address_info', { _addresses: [wrwUser.walletAddress0] })
680+
.resolves(endpointResponses.addressInfoResponse.ADAAndTokenUTXOs);
681+
682+
const res = await basecoin.recover({
683+
bitgoKey: wrwUser.bitgoKey,
684+
recoveryDestination: destAddr,
685+
});
686+
res.should.not.be.empty();
687+
const unsignedTx = res.txRequests[0].transactions[0].unsignedTx;
688+
unsignedTx.should.hasOwnProperty('serializedTx');
689+
690+
const tx = new Transaction(basecoin);
691+
tx.fromRawTransaction(unsignedTx.serializedTx);
692+
const txJson = tx.toJson();
693+
694+
txJson.inputs.length.should.equal(2);
695+
should.deepEqual(txJson.inputs[0].transaction_id, testnetUTXO.UTXO_1.tx_hash);
696+
should.deepEqual(txJson.inputs[1].transaction_id, testnetUTXO.UTXO_TOKEN.tx_hash);
697+
698+
txJson.outputs.length.should.equal(2);
699+
700+
const tokenPolicyId = '2533cca6eb42076e144e9f2772c390dece9fce173bc38c72294b3924';
701+
const tokenEncodedAssetName = '5741544552';
702+
const tokenQuantity = '111';
703+
const minADAForToken = 1500000;
704+
705+
const tokenOutput = txJson.outputs.find((o) => o.multiAssets !== undefined);
706+
should.exist(tokenOutput);
707+
should.deepEqual(tokenOutput!.address, destAddr);
708+
should.deepEqual(Number(tokenOutput!.amount), minADAForToken);
709+
const expectedPolicyId = CardanoWasm.ScriptHash.from_bytes(Buffer.from(tokenPolicyId, 'hex'));
710+
const expectedAssetName = CardanoWasm.AssetName.new(Buffer.from(tokenEncodedAssetName, 'hex'));
711+
(tokenOutput!.multiAssets as CardanoWasm.MultiAsset)
712+
.get_asset(expectedPolicyId, expectedAssetName)
713+
.to_str()
714+
.should.equal(tokenQuantity);
715+
716+
const adaOutput = txJson.outputs.find((o) => o.multiAssets === undefined);
717+
should.exist(adaOutput);
718+
should.deepEqual(adaOutput!.address, destAddr);
719+
const fee = Number(tx.explainTransaction().fee.fee);
720+
const totalBalance = testnetUTXO.UTXO_1.value + testnetUTXO.UTXO_TOKEN.value;
721+
should.deepEqual(Number(adaOutput!.amount), totalBalance - minADAForToken - fee);
722+
723+
const explained = tx.explainTransaction();
724+
const explainedTokenOutput = explained.outputs.find((o) => o.amount === minADAForToken.toString());
725+
should.exist(explainedTokenOutput);
726+
727+
// inputs and outputs in parsedTx should carry multiAssets
728+
const parsedTxInputs = res.txRequests[0].transactions[0].unsignedTx.parsedTx.inputs as {
729+
address: string;
730+
valueString: string;
731+
multiAssets?: { policy_id: string; asset_name: string; quantity: string }[];
732+
}[];
733+
parsedTxInputs.length.should.equal(1);
734+
should.exist(parsedTxInputs[0].multiAssets);
735+
parsedTxInputs[0].multiAssets!.length.should.equal(1);
736+
should.deepEqual(parsedTxInputs[0].multiAssets![0].policy_id, tokenPolicyId);
737+
should.deepEqual(parsedTxInputs[0].multiAssets![0].asset_name, tokenEncodedAssetName);
738+
should.deepEqual(parsedTxInputs[0].multiAssets![0].quantity, tokenQuantity);
739+
740+
const parsedTxOutputs = res.txRequests[0].transactions[0].unsignedTx.parsedTx.outputs as {
741+
address: string;
742+
valueString: string;
743+
multiAssets?: { policy_id: string; asset_name: string; quantity: string }[];
744+
}[];
745+
const parsedTokenOutput = parsedTxOutputs.find((o) => o.multiAssets !== undefined);
746+
should.exist(parsedTokenOutput);
747+
parsedTokenOutput!.multiAssets!.length.should.equal(1);
748+
should.deepEqual(parsedTokenOutput!.multiAssets![0].policy_id, tokenPolicyId);
749+
should.deepEqual(parsedTokenOutput!.multiAssets![0].asset_name, tokenEncodedAssetName);
750+
should.deepEqual(parsedTokenOutput!.multiAssets![0].quantity, tokenQuantity);
751+
// pure ADA output should not have multiAssets
752+
const parsedAdaOutput = parsedTxOutputs.find((o) => o.multiAssets === undefined);
753+
should.exist(parsedAdaOutput);
754+
});
755+
677756
it('should recover ADA plus token UTXOs - token and ADA both appear in outputs (signed)', async function () {
678757
callBack
679758
.withArgs('address_info', { _addresses: [wrwUser.walletAddress0] })

0 commit comments

Comments
 (0)