diff --git a/modules/abstract-substrate/src/abstractSubstrateCoin.ts b/modules/abstract-substrate/src/abstractSubstrateCoin.ts index 49b60a4269..da96e0061c 100644 --- a/modules/abstract-substrate/src/abstractSubstrateCoin.ts +++ b/modules/abstract-substrate/src/abstractSubstrateCoin.ts @@ -20,12 +20,14 @@ import { RecoveryTxRequest, SignedTransaction, TssVerifyAddressOptions, + TxIntentMismatchRecipientError, UnexpectedAddressError, verifyEddsaTssWalletAddress, VerifyTransactionOptions, } from '@bitgo/sdk-core'; import { CoinFamily, BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; import { KeyPair as SubstrateKeyPair, Transaction } from './lib'; +import { NativeTransferBuilder } from './lib/nativeTransferBuilder'; import { DEFAULT_SUBSTRATE_PREFIX } from './lib/constants'; import { SignTransactionOptions, VerifiedTransactionParameters, Material } from './lib/iface'; import utils from './lib/utils'; @@ -132,12 +134,55 @@ export class SubstrateCoin extends BaseCoin { /** @inheritDoc **/ async verifyTransaction(params: VerifyTransactionOptions): Promise { - const { txParams } = params; - if (Array.isArray(txParams.recipients) && txParams.recipients.length > 1) { - throw new Error( - `${this.getChain()} doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.` - ); + const { txParams, txPrebuild } = params; + + if (!txParams) { + throw new Error('missing txParams'); + } + if (!txPrebuild) { + throw new Error('missing txPrebuild'); } + if (!txPrebuild.txHex) { + throw new Error('missing txHex in txPrebuild'); + } + + const factory = this.getBuilder(); + const txBuilder = factory.from(txPrebuild.txHex) as unknown as NativeTransferBuilder; + const txTo: string = txBuilder['_to']; + const txAmount: string = txBuilder['_amount']; + const isSweep: boolean = txBuilder['_sweepFreeBalance'] === true; + + if (txParams.recipients !== undefined) { + if (txParams.recipients.length === 0) { + throw new Error('missing recipients in txParams'); + } + if (Array.isArray(txParams.recipients) && txParams.recipients.length > 1) { + throw new Error( + `${this.getChain()} doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.` + ); + } + + if (txParams.recipients[0].address !== txTo) { + throw new TxIntentMismatchRecipientError( + `Recipient address ${txParams.recipients[0].address} does not match transaction destination address ${txTo}`, + params.reqId, + [txParams], + txPrebuild.txHex, + [{ address: txTo, amount: txAmount }] + ); + } + + if (!isSweep && txParams.recipients[0].amount !== txAmount) { + throw new TxIntentMismatchRecipientError( + `Recipient amount ${txParams.recipients[0].amount} does not match transaction amount ${txAmount}`, + params.reqId, + [txParams], + txPrebuild.txHex, + [{ address: txTo, amount: txAmount }] + ); + } + } + return true; } diff --git a/modules/sdk-coin-polyx/test/unit/polyx.ts b/modules/sdk-coin-polyx/test/unit/polyx.ts index a8a6c6f4d4..46f9617253 100644 --- a/modules/sdk-coin-polyx/test/unit/polyx.ts +++ b/modules/sdk-coin-polyx/test/unit/polyx.ts @@ -6,7 +6,8 @@ import { POLYX_ADDRESS_FORMAT, TPOLYX_ADDRESS_FORMAT } from '../../src/lib/const import * as sinon from 'sinon'; import * as testData from '../resources/wrwUsers'; import { afterEach } from 'mocha'; -import { genesisHash, specVersion, txVersion } from '../resources'; +import { genesisHash, specVersion, txVersion, rawTx } from '../resources'; +import { TxIntentMismatchRecipientError } from '@bitgo/sdk-core'; describe('Polyx:', function () { let bitgo: TestBitGoAPI; @@ -199,4 +200,72 @@ describe('Polyx:', function () { ); }); }); + + describe('verifyTransaction', function () { + const transferTo = '5F8jxKE81GhFrphyfMFr5UjeAz5wS4AaZFmeFPnf8wTetD72'; + const transferAmount = '2000000000'; + const wrongAddress = '5GhbC6n2pUFrX98DwyPit67fB5AwQvVCwZ4j2HKA7a4dUK4y'; + + describe('transfer transaction', function () { + it('should return true when address and amount match', async function () { + const result = await baseCoin.verifyTransaction({ + txPrebuild: { txHex: rawTx.transfer.signed }, + txParams: { recipients: [{ address: transferTo, amount: transferAmount }] }, + }); + result.should.be.true(); + }); + + it('should throw TxIntentMismatchRecipientError for address mismatch', async function () { + await baseCoin + .verifyTransaction({ + txPrebuild: { txHex: rawTx.transfer.signed }, + txParams: { recipients: [{ address: wrongAddress, amount: transferAmount }] }, + }) + .should.be.rejectedWith(TxIntentMismatchRecipientError); + }); + + it('should throw TxIntentMismatchRecipientError for amount mismatch', async function () { + await baseCoin + .verifyTransaction({ + txPrebuild: { txHex: rawTx.transfer.signed }, + txParams: { recipients: [{ address: transferTo, amount: '1' }] }, + }) + .should.be.rejectedWith(TxIntentMismatchRecipientError); + }); + }); + + describe('guard cases', function () { + it('should throw when txHex is missing', async function () { + await baseCoin + .verifyTransaction({ + txPrebuild: {}, + txParams: { recipients: [{ address: transferTo, amount: transferAmount }] }, + }) + .should.be.rejectedWith('missing txHex in txPrebuild'); + }); + + it('should throw when recipients has more than 1 entry', async function () { + await baseCoin + .verifyTransaction({ + txPrebuild: { txHex: rawTx.transfer.signed }, + txParams: { + recipients: [ + { address: transferTo, amount: transferAmount }, + { address: wrongAddress, amount: transferAmount }, + ], + }, + }) + .should.be.rejectedWith(/support sending to more than 1 destination address/); + }); + + it('should throw when recipients is an empty array', async function () { + await baseCoin + .verifyTransaction({ + txPrebuild: { txHex: rawTx.transfer.signed }, + txParams: { recipients: [] }, + }) + .should.be.rejectedWith('missing recipients in txParams'); + }); + }); + }); }); diff --git a/modules/sdk-coin-tao/test/unit/tao.ts b/modules/sdk-coin-tao/test/unit/tao.ts index 4d00533d70..29336e9e81 100644 --- a/modules/sdk-coin-tao/test/unit/tao.ts +++ b/modules/sdk-coin-tao/test/unit/tao.ts @@ -4,8 +4,9 @@ import { BitGoAPI } from '@bitgo/sdk-api'; import { Tao, Ttao } from '../../src'; import * as sinon from 'sinon'; import * as testData from './fixtures'; -import { txVersion, genesisHash, specVersion } from '../resources'; +import { txVersion, genesisHash, specVersion, rawTx } from '../resources'; import { afterEach } from 'mocha'; +import { TxIntentMismatchRecipientError } from '@bitgo/sdk-core'; describe('Tao:', function () { let bitgo: TestBitGoAPI; @@ -508,4 +509,92 @@ describe('Tao:', function () { ); }); }); + + describe('verifyTransaction', function () { + const transferTo = '5EQZSJmHuFH8asYYJruSRwpJmE5aqSdhdiX9oxRbxujKUkTe'; + const transferAmount = '2'; + const sweepTo = '5EQZSJmHuFH8asYYJruSRwpJmE5aqSdhdiX9oxRbxujKUkTe'; + const wrongAddress = '5Ffp1wJCPu4hzVDTo7XaMLqZSvSadyUQmxWPDw74CBjECSoq'; + + describe('transfer transaction', function () { + it('should return true when address and amount match', async function () { + const result = await baseCoin.verifyTransaction({ + txPrebuild: { txHex: rawTx.transfer.signed }, + txParams: { recipients: [{ address: transferTo, amount: transferAmount }] }, + }); + result.should.be.true(); + }); + + it('should throw TxIntentMismatchRecipientError for address mismatch', async function () { + await baseCoin + .verifyTransaction({ + txPrebuild: { txHex: rawTx.transfer.signed }, + txParams: { recipients: [{ address: wrongAddress, amount: transferAmount }] }, + }) + .should.be.rejectedWith(TxIntentMismatchRecipientError); + }); + + it('should throw TxIntentMismatchRecipientError for amount mismatch', async function () { + await baseCoin + .verifyTransaction({ + txPrebuild: { txHex: rawTx.transfer.signed }, + txParams: { recipients: [{ address: transferTo, amount: '9999' }] }, + }) + .should.be.rejectedWith(TxIntentMismatchRecipientError); + }); + }); + + describe('sweep transaction', function () { + it('should return true when address matches (amount check skipped for sweep)', async function () { + const result = await baseCoin.verifyTransaction({ + txPrebuild: { txHex: rawTx.transferAll.signed }, + txParams: { recipients: [{ address: sweepTo, amount: '9999999' }] }, + }); + result.should.be.true(); + }); + + it('should throw TxIntentMismatchRecipientError when sweep address does not match', async function () { + await baseCoin + .verifyTransaction({ + txPrebuild: { txHex: rawTx.transferAll.signed }, + txParams: { recipients: [{ address: wrongAddress, amount: '0' }] }, + }) + .should.be.rejectedWith(TxIntentMismatchRecipientError); + }); + }); + + describe('guard cases', function () { + it('should throw when txHex is missing', async function () { + await baseCoin + .verifyTransaction({ + txPrebuild: {}, + txParams: { recipients: [{ address: transferTo, amount: transferAmount }] }, + }) + .should.be.rejectedWith('missing txHex in txPrebuild'); + }); + + it('should throw when recipients has more than 1 entry', async function () { + await baseCoin + .verifyTransaction({ + txPrebuild: { txHex: rawTx.transfer.signed }, + txParams: { + recipients: [ + { address: transferTo, amount: transferAmount }, + { address: wrongAddress, amount: transferAmount }, + ], + }, + }) + .should.be.rejectedWith(/doesn't support sending to more than 1 destination address/); + }); + + it('should throw when recipients is an empty array', async function () { + await baseCoin + .verifyTransaction({ + txPrebuild: { txHex: rawTx.transfer.signed }, + txParams: { recipients: [] }, + }) + .should.be.rejectedWith('missing recipients in txParams'); + }); + }); + }); });