Skip to content
Draft
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
3 changes: 2 additions & 1 deletion modules/sdk-coin-tia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 44 additions & 1 deletion modules/sdk-coin-tia/src/tia.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<boolean> {
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;
}
}
34 changes: 34 additions & 0 deletions modules/sdk-coin-tia/test/unit/tia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
Loading