Skip to content
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
25 changes: 15 additions & 10 deletions modules/sdk-coin-ada/src/ada.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
TssVerifyAddressOptions,
} from '@bitgo/sdk-core';
import { KeyPair as AdaKeyPair, Transaction, TransactionBuilderFactory, Utils } from './lib';
import type { Asset } from './lib/transaction';
import { BaseCoin as StaticsBaseCoin, CoinFamily, coins } from '@bitgo/statics';
import adaUtils from './lib/utils';
import * as request from 'superagent';
Expand Down Expand Up @@ -213,7 +214,7 @@ export class Ada extends BaseCoin {
throw new Error('Invalid transaction');
}

return rebuiltTransaction.explainTransaction();
return rebuiltTransaction.explainTransaction() as unknown as AdaTransactionExplanation;
}

async parseTransaction(params: AdaParseTransactionOptions): Promise<ParsedTransaction> {
Expand Down Expand Up @@ -499,21 +500,25 @@ export class Ada extends BaseCoin {
const transactionPrebuild = { txHex: serializedTx };
const parsedTx = await this.parseTransaction({ txPrebuild: transactionPrebuild });
const walletCoin = this.getChain();
const output = (parsedTx.outputs as ITransactionRecipient)[0];
const parsedOutputs = parsedTx.outputs as (ITransactionRecipient & { multiAssets?: Asset[] })[];
const output = parsedOutputs[0];
// All tokens from the spent UTXOs — shown on the input so WRW can display what is being swept
const inputAssetList = Object.values(aggregatedAssetList);
const inputs = [
{
address: senderAddr,
valueString: output.amount,
value: new BigNumber(output.amount).toNumber(),
},
];
const outputs = [
{
address: output.address,
valueString: output.amount,
coinName: walletCoin,
value: new BigNumber(output.amount as string).toNumber(),
...(inputAssetList.length > 0 && { multiAssets: inputAssetList }),
},
];
// assetList per output comes from explainTransaction which parses multiAssets
const outputs = parsedOutputs.map((o) => ({
address: o.address,
valueString: o.amount,
coinName: walletCoin,
...(o.multiAssets && { multiAssets: o.multiAssets }),
}));
const spendAmount = output.amount;
const completedParsedTx = { inputs: inputs, outputs: outputs, spendAmount: spendAmount, type: '' };
const fee = new BigNumber((parsedTx.fee as { fee: string }).fee);
Expand Down
34 changes: 32 additions & 2 deletions modules/sdk-coin-ada/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ export class Transaction extends BaseTransaction {

/** @inheritdoc */
explainTransaction(): {
outputs: { amount: string; address: string }[];
outputs: { amount: string; address: string; multiAssets?: Asset[] }[];
certificates: Cert[];
changeOutputs: string[];
outputAmount: string;
Expand Down Expand Up @@ -414,7 +414,14 @@ export class Transaction extends BaseTransaction {
return {
displayOrder,
id: txJson.id,
outputs: txJson.outputs.map((o) => ({ address: o.address, amount: o.amount })),
outputs: txJson.outputs.map((o) => {
const multiAssets = Transaction.parseMultiAssets(o.multiAssets as CardanoWasm.MultiAsset | undefined);
return {
address: o.address,
amount: o.amount,
...(multiAssets && { multiAssets }),
};
}),
outputAmount: outputAmount,
changeOutputs: [],
changeAmount: '0',
Expand All @@ -426,6 +433,29 @@ export class Transaction extends BaseTransaction {
};
}

private static parseMultiAssets(multiAssets: CardanoWasm.MultiAsset | undefined): Asset[] | undefined {
if (!multiAssets) return undefined;
const list: Asset[] = [];
const scriptHashes = multiAssets.keys();
for (let i = 0; i < scriptHashes.len(); i++) {
const scriptHash = scriptHashes.get(i);
const assets = multiAssets.get(scriptHash);
if (!assets) continue;
const assetNames = assets.keys();
for (let j = 0; j < assetNames.len(); j++) {
const assetName = assetNames.get(j);
const quantity = assets.get(assetName);
if (!quantity) continue;
list.push({
policy_id: Buffer.from(scriptHash.to_bytes()).toString('hex'),
asset_name: Buffer.from(assetName.name()).toString('hex'),
quantity: quantity.to_str(),
});
}
}
return list.length > 0 ? list : undefined;
}

getPledgeDetails(): PledgeDetails | undefined {
return this._pledgeDetails;
}
Expand Down
79 changes: 79 additions & 0 deletions modules/sdk-coin-ada/test/unit/ada.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,85 @@ describe('ADA', function () {
should.deepEqual(Number(txJson.outputs[0].amount) + fee, testnetUTXO.UTXO_1.value);
});

it('should recover ADA plus token UTXOs - token and ADA both appear in outputs (unsigned sweep)', async function () {
callBack
.withArgs('address_info', { _addresses: [wrwUser.walletAddress0] })
.resolves(endpointResponses.addressInfoResponse.ADAAndTokenUTXOs);

const res = await basecoin.recover({
bitgoKey: wrwUser.bitgoKey,
recoveryDestination: destAddr,
});
res.should.not.be.empty();
const unsignedTx = res.txRequests[0].transactions[0].unsignedTx;
unsignedTx.should.hasOwnProperty('serializedTx');

const tx = new Transaction(basecoin);
tx.fromRawTransaction(unsignedTx.serializedTx);
const txJson = tx.toJson();

txJson.inputs.length.should.equal(2);
should.deepEqual(txJson.inputs[0].transaction_id, testnetUTXO.UTXO_1.tx_hash);
should.deepEqual(txJson.inputs[1].transaction_id, testnetUTXO.UTXO_TOKEN.tx_hash);

txJson.outputs.length.should.equal(2);

const tokenPolicyId = '2533cca6eb42076e144e9f2772c390dece9fce173bc38c72294b3924';
const tokenEncodedAssetName = '5741544552';
const tokenQuantity = '111';
const minADAForToken = 1500000;

const tokenOutput = txJson.outputs.find((o) => o.multiAssets !== undefined);
should.exist(tokenOutput);
should.deepEqual(tokenOutput!.address, destAddr);
should.deepEqual(Number(tokenOutput!.amount), minADAForToken);
const expectedPolicyId = CardanoWasm.ScriptHash.from_bytes(Buffer.from(tokenPolicyId, 'hex'));
const expectedAssetName = CardanoWasm.AssetName.new(Buffer.from(tokenEncodedAssetName, 'hex'));
(tokenOutput!.multiAssets as CardanoWasm.MultiAsset)
.get_asset(expectedPolicyId, expectedAssetName)
.to_str()
.should.equal(tokenQuantity);

const adaOutput = txJson.outputs.find((o) => o.multiAssets === undefined);
should.exist(adaOutput);
should.deepEqual(adaOutput!.address, destAddr);
const fee = Number(tx.explainTransaction().fee.fee);
const totalBalance = testnetUTXO.UTXO_1.value + testnetUTXO.UTXO_TOKEN.value;
should.deepEqual(Number(adaOutput!.amount), totalBalance - minADAForToken - fee);

const explained = tx.explainTransaction();
const explainedTokenOutput = explained.outputs.find((o) => o.amount === minADAForToken.toString());
should.exist(explainedTokenOutput);

// inputs and outputs in parsedTx should carry multiAssets
const parsedTxInputs = res.txRequests[0].transactions[0].unsignedTx.parsedTx.inputs as {
address: string;
valueString: string;
multiAssets?: { policy_id: string; asset_name: string; quantity: string }[];
}[];
parsedTxInputs.length.should.equal(1);
should.exist(parsedTxInputs[0].multiAssets);
parsedTxInputs[0].multiAssets!.length.should.equal(1);
should.deepEqual(parsedTxInputs[0].multiAssets![0].policy_id, tokenPolicyId);
should.deepEqual(parsedTxInputs[0].multiAssets![0].asset_name, tokenEncodedAssetName);
should.deepEqual(parsedTxInputs[0].multiAssets![0].quantity, tokenQuantity);

const parsedTxOutputs = res.txRequests[0].transactions[0].unsignedTx.parsedTx.outputs as {
address: string;
valueString: string;
multiAssets?: { policy_id: string; asset_name: string; quantity: string }[];
}[];
const parsedTokenOutput = parsedTxOutputs.find((o) => o.multiAssets !== undefined);
should.exist(parsedTokenOutput);
parsedTokenOutput!.multiAssets!.length.should.equal(1);
should.deepEqual(parsedTokenOutput!.multiAssets![0].policy_id, tokenPolicyId);
should.deepEqual(parsedTokenOutput!.multiAssets![0].asset_name, tokenEncodedAssetName);
should.deepEqual(parsedTokenOutput!.multiAssets![0].quantity, tokenQuantity);
// pure ADA output should not have multiAssets
const parsedAdaOutput = parsedTxOutputs.find((o) => o.multiAssets === undefined);
should.exist(parsedAdaOutput);
});

it('should recover ADA plus token UTXOs - token and ADA both appear in outputs (signed)', async function () {
callBack
.withArgs('address_info', { _addresses: [wrwUser.walletAddress0] })
Expand Down
Loading