diff --git a/modules/sdk-coin-tia/package.json b/modules/sdk-coin-tia/package.json index 3d373cc502..f61a147190 100644 --- a/modules/sdk-coin-tia/package.json +++ b/modules/sdk-coin-tia/package.json @@ -47,7 +47,8 @@ "@cosmjs/amino": "^0.29.5", "@cosmjs/encoding": "^0.29.5", "@cosmjs/stargate": "^0.29.5", - "bignumber.js": "^9.1.1" + "bignumber.js": "^9.1.1", + "lodash": "^4.18.0" }, "devDependencies": { "@bitgo/sdk-api": "^3.0.7", diff --git a/modules/sdk-coin-tia/src/tia.ts b/modules/sdk-coin-tia/src/tia.ts index dcae42c328..1a4e8b022e 100644 --- a/modules/sdk-coin-tia/src/tia.ts +++ b/modules/sdk-coin-tia/src/tia.ts @@ -1,6 +1,8 @@ import { CosmosCoin, CosmosKeyPair, GasAmountDetails } from '@bitgo/abstract-cosmos'; -import { BaseCoin, BitGoBase, Environments } from '@bitgo/sdk-core'; +import { BaseCoin, BitGoBase, Environments, TransactionType, VerifyTransactionOptions } from '@bitgo/sdk-core'; import { BaseUnit, BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics'; +import BigNumber from 'bignumber.js'; +import * as _ from 'lodash'; import { KeyPair, TransactionBuilderFactory } from './lib'; import utils from './lib/utils'; @@ -63,4 +65,45 @@ export class Tia extends CosmosCoin { getAddressFromPublicKey(publicKey: string): string { return new KeyPair({ pub: publicKey }).getAddress(); } + + /** + * @inheritDoc + * + * Overrides CosmosCoin.verifyTransaction because recipients can carry their memo folded into + * the address as `address?memoId=X` (see getAddressDetails), while explainTransaction() reports + * the plain on-chain address plus a separate `memo` field. The base class compares the two + * as raw strings, so any memo-tagged recipient fails verification even when the transaction was + * built correctly. Normalize both sides to the same {address, amount, memo?} shape first. + */ + async verifyTransaction(params: VerifyTransactionOptions): Promise { + let totalAmount = new BigNumber(0); + const { txPrebuild, txParams } = params; + const rawTx = txPrebuild.txHex; + if (!rawTx) { + throw new Error('missing required tx prebuild property txHex'); + } + const transaction = await this.getBuilder().from(rawTx).build(); + const explainedTx = transaction.explainTransaction(); + if (txParams.recipients && txParams.recipients.length > 0) { + const filteredRecipients = txParams.recipients.map((recipient) => { + const { address, memoId } = this.getAddressDetails(recipient.address); + return { address, amount: recipient.amount, ...(memoId !== undefined ? { memo: memoId } : {}) }; + }); + const filteredOutputs = explainedTx.outputs.map((output) => _.pick(output, ['address', 'amount', 'memo'])); + + if (!_.isEqual(filteredOutputs, filteredRecipients)) { + throw new Error('Tx outputs does not match with expected txParams recipients'); + } + // WithdrawDelegatorRewards and ContractCall transaction don't have amount + if (transaction.type !== TransactionType.StakingWithdraw && transaction.type !== TransactionType.ContractCall) { + for (const recipient of txParams.recipients) { + totalAmount = totalAmount.plus(recipient.amount); + } + if (!totalAmount.isEqualTo(explainedTx.outputAmount)) { + throw new Error('Tx total amount does not match with expected total amount field'); + } + } + } + return true; + } } diff --git a/modules/sdk-coin-tia/test/unit/tia.ts b/modules/sdk-coin-tia/test/unit/tia.ts index 7c41b3d201..5c00e502aa 100644 --- a/modules/sdk-coin-tia/test/unit/tia.ts +++ b/modules/sdk-coin-tia/test/unit/tia.ts @@ -199,6 +199,40 @@ describe('TIA', function () { isTransactionVerified.should.equal(true); }); + it('should succeed to verify transaction with a memo-suffixed recipient address', async function () { + const txPrebuild = { txHex: TEST_TX_WITH_MEMO.signedTxBase64, txInfo: {} }; + const txParams = { + recipients: [ + { + address: `${TEST_TX_WITH_MEMO.to}?memoId=${TEST_TX_WITH_MEMO.memo}`, + amount: TEST_TX_WITH_MEMO.sendAmount, + }, + ], + }; + const isTransactionVerified = await basecoin.verifyTransaction({ txParams, txPrebuild, verification: {} }); + isTransactionVerified.should.equal(true); + }); + + it('should fail to verify transaction when the recipient memoId does not match the tx memo', async function () { + const txPrebuild = { txHex: TEST_TX_WITH_MEMO.signedTxBase64, txInfo: {} }; + const txParams = { + recipients: [{ address: `${TEST_TX_WITH_MEMO.to}?memoId=999999`, amount: TEST_TX_WITH_MEMO.sendAmount }], + }; + await basecoin + .verifyTransaction({ txParams, txPrebuild, verification: {} }) + .should.be.rejectedWith('Tx outputs does not match with expected txParams recipients'); + }); + + it('should fail to verify transaction when the recipient has no memoId but the tx has one', async function () { + const txPrebuild = { txHex: TEST_TX_WITH_MEMO.signedTxBase64, txInfo: {} }; + const txParams = { + recipients: [{ address: TEST_TX_WITH_MEMO.to, amount: TEST_TX_WITH_MEMO.sendAmount }], + }; + await basecoin + .verifyTransaction({ txParams, txPrebuild, verification: {} }) + .should.be.rejectedWith('Tx outputs does not match with expected txParams recipients'); + }); + it('should fail to verify transaction with invalid param', async function () { const txPrebuild = {}; const txParams = { recipients: undefined };