From 6060d22dae9854b773f93bf2cd62940071e9ec20 Mon Sep 17 00:00:00 2001 From: Luis Covarrubias Date: Wed, 18 Mar 2026 17:07:29 -0700 Subject: [PATCH] feat: simplify verifyTransaction using signing payload format Both wasm-dot and legacy now produce signing payload format for unsigned transactions, so the format detection hack (compactByte, versionByte, isStandardFormat) is no longer needed. Use chain === 'tdot' as the gate for the wasm path instead. Update WASM test fixtures from old standard extrinsic format to signing payload format to match the new wasm-dot output. BTC-3161 TICKET: BTC-3161 --- modules/sdk-coin-dot/package.json | 2 +- modules/sdk-coin-dot/src/dot.ts | 42 ++++++++++----- modules/sdk-coin-dot/test/unit/dot.ts | 73 +++++++++++++++++++++++++++ yarn.lock | 8 +-- 4 files changed, 106 insertions(+), 19 deletions(-) diff --git a/modules/sdk-coin-dot/package.json b/modules/sdk-coin-dot/package.json index c8754b4f64..a1babb2ae3 100644 --- a/modules/sdk-coin-dot/package.json +++ b/modules/sdk-coin-dot/package.json @@ -59,7 +59,7 @@ "@bitgo/sdk-core": "^36.35.0", "@bitgo/sdk-lib-mpc": "^10.9.0", "@bitgo/statics": "^58.31.0", - "@bitgo/wasm-dot": "^1.5.0", + "@bitgo/wasm-dot": "^1.7.0", "@polkadot/api": "14.1.1", "@polkadot/api-augment": "14.1.1", "@polkadot/keyring": "13.5.6", diff --git a/modules/sdk-coin-dot/src/dot.ts b/modules/sdk-coin-dot/src/dot.ts index f45f63e4a5..d634d925ff 100644 --- a/modules/sdk-coin-dot/src/dot.ts +++ b/modules/sdk-coin-dot/src/dot.ts @@ -39,6 +39,7 @@ import { Transaction, TransactionBuilderFactory, Utils, + explainDotTransaction, } from './lib'; import '@polkadot/api-augment'; import { ApiPromise, WsProvider } from '@polkadot/api'; @@ -664,22 +665,37 @@ export class Dot extends BaseCoin { throw new Error('missing txHex in txPrebuild'); } - const factory = this.getBuilder(); - const txBuilder = factory.from(txPrebuild.txHex) as unknown as NativeTransferBuilder; + const chain = this.getChain(); + let txTo: string; + let txAmount: string; + let isSweep: boolean; + + if (chain === 'tdot') { + const material = Utils.default.getMaterial(coins.get(chain)); + const explained = explainDotTransaction({ txHex: txPrebuild.txHex, material }); + txTo = explained.outputs[0]?.address ?? ''; + txAmount = String(explained.outputs[0]?.amount ?? '0'); + isSweep = explained.methodName === 'balances.transferAll'; + } else { + const factory = this.getBuilder(); + const txBuilder = factory.from(txPrebuild.txHex) as unknown as NativeTransferBuilder; + txTo = txBuilder['_to']; + txAmount = txBuilder['_amount']; + isSweep = txBuilder['_sweepFreeBalance'] === true; + } if (verification?.consolidationToBaseAddress) { - // Verify funds are sent to wallet's base address for consolidation const baseAddress = wallet?.coinSpecific()?.rootAddress || wallet?.coinSpecific()?.baseAddress; if (!baseAddress) { throw new Error('Unable to determine base address for consolidation'); } - if (txBuilder['_to'] !== baseAddress) { + if (txTo !== baseAddress) { throw new TxIntentMismatchRecipientError( - `Transaction destination address ${txBuilder['_to']} does not match wallet base address ${baseAddress}`, + `Transaction destination address ${txTo} does not match wallet base address ${baseAddress}`, reqId, [txParams], txPrebuild.txHex, - [{ address: txBuilder['_to'], amount: txBuilder['_amount'] }] + [{ address: txTo, amount: txAmount }] ); } } @@ -695,25 +711,23 @@ export class Dot extends BaseCoin { ); } - // validate recipient is same as txBuilder['_to'] - if (txParams.recipients[0].address !== txBuilder['_to']) { + if (txParams.recipients[0].address !== txTo) { throw new TxIntentMismatchRecipientError( - `Recipient address ${txParams.recipients[0].address} does not match transaction destination address ${txBuilder['_to']}`, + `Recipient address ${txParams.recipients[0].address} does not match transaction destination address ${txTo}`, reqId, [txParams], txPrebuild.txHex, - [{ address: txBuilder['_to'], amount: txBuilder['_amount'] }] + [{ address: txTo, amount: txAmount }] ); } - // validate amount is same as txBuilder['_amount'] - if (!txBuilder['_sweepFreeBalance'] && txParams.recipients[0].amount !== txBuilder['_amount']) { + if (!isSweep && txParams.recipients[0].amount !== txAmount) { throw new TxIntentMismatchRecipientError( - `Recipient amount ${txParams.recipients[0].amount} does not match transaction amount ${txBuilder['_amount']}`, + `Recipient amount ${txParams.recipients[0].amount} does not match transaction amount ${txAmount}`, reqId, [txParams], txPrebuild.txHex, - [{ address: txBuilder['_to'], amount: txBuilder['_amount'] }] + [{ address: txTo, amount: txAmount }] ); } } diff --git a/modules/sdk-coin-dot/test/unit/dot.ts b/modules/sdk-coin-dot/test/unit/dot.ts index 1581c5bf40..f0c5600297 100644 --- a/modules/sdk-coin-dot/test/unit/dot.ts +++ b/modules/sdk-coin-dot/test/unit/dot.ts @@ -895,6 +895,79 @@ describe('DOT:', function () { }); assert.strictEqual(result, true); }); + + const wasmTransferKeepAliveHex = + '0xa80a0300161b969b6b53ef81225feea3882284c778cd4a406d23215fcf492e83f75d42960b00204aa9d101eb600400000065900f001000000067f9723393ef76214df0118c34bbbd3dbebc8ed46a10973a8c969d48fe7598c9a7b7420ee3e4fe2b88da0fc42b30897e18d56d8b56a1934211d9de730cf96de300'; + const wasmTransferAllHex = + '0x900a04009f7b0675db59d19b4bd9c8c72eaabba75a9863d02b30115b8b3c3ca5c20f025401d50121030000009d880f001000000067f9723393ef76214df0118c34bbbd3dbebc8ed46a10973a8c969d48fe7598c9149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d00'; + + it('should accept a valid WASM-built payment (tdot)', async function () { + const txParams = { + recipients: [{ amount: '2000000000000', address: '5CZh773vKGwKFCYUjGc31AwXCbf7TPkavdeuk2XoujJMjbBD' }], + }; + const result = await basecoin.verifyTransaction({ txPrebuild: { txHex: wasmTransferKeepAliveHex }, txParams }); + assert.strictEqual(result, true); + }); + + it('should reject WASM-built payment with wrong amount (tdot)', async function () { + const txParams = { + recipients: [{ amount: '9999', address: '5CZh773vKGwKFCYUjGc31AwXCbf7TPkavdeuk2XoujJMjbBD' }], + }; + await basecoin + .verifyTransaction({ txPrebuild: { txHex: wasmTransferKeepAliveHex }, txParams }) + .should.be.rejectedWith(/does not match transaction amount/); + }); + + it('should reject WASM-built payment with wrong recipient (tdot)', async function () { + const txParams = { + recipients: [{ amount: '2000000000000', address: '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74' }], + }; + await basecoin + .verifyTransaction({ txPrebuild: { txHex: wasmTransferKeepAliveHex }, txParams }) + .should.be.rejectedWith(/does not match transaction destination address/); + }); + + it('should verify WASM-built transferAll consolidation (tdot)', async function () { + const mockedWallet = { + coinSpecific: () => ({ baseAddress: '5Ffp1wJCPu4hzVDTo7XaMLqZSvSadyUQmxWPDw74CBjECSoq' }), + }; + const result = await basecoin.verifyTransaction({ + txPrebuild: { txHex: wasmTransferAllHex }, + txParams: {}, + wallet: mockedWallet as any, + verification: { consolidationToBaseAddress: true }, + }); + assert.strictEqual(result, true); + }); + + it('should verify WASM-built transferAll with recipients skips amount check (tdot)', async function () { + const mockedWallet = { + coinSpecific: () => ({ baseAddress: '5Ffp1wJCPu4hzVDTo7XaMLqZSvSadyUQmxWPDw74CBjECSoq' }), + }; + const txParams = { + recipients: [{ address: '5Ffp1wJCPu4hzVDTo7XaMLqZSvSadyUQmxWPDw74CBjECSoq', amount: '999999' }], + }; + const result = await basecoin.verifyTransaction({ + txPrebuild: { txHex: wasmTransferAllHex }, + txParams, + wallet: mockedWallet as any, + verification: { consolidationToBaseAddress: true }, + }); + assert.strictEqual(result, true); + }); + + it('should decode wasm-built signing payload with legacy TransactionBuilderFactory', async function () { + const { TransactionBuilderFactory } = await import('../../src/lib'); + const factory = new TransactionBuilderFactory(coins.get('tdot')); + const txBuilder = factory.from(wasmTransferKeepAliveHex); + txBuilder.sender({ address: '5EGoFA95omzemRssELLDjVenNZ68aXyUeqtKQScXSEBvVJkr' }); + txBuilder.validity({ firstValid: 1000 }); + txBuilder.referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d'); + const tx = await txBuilder.build(); + const json = tx.toJson(); + assert.strictEqual((json as any).to, '5CZh773vKGwKFCYUjGc31AwXCbf7TPkavdeuk2XoujJMjbBD'); + assert.strictEqual((json as any).amount, '2000000000000'); + }); }); describe('isWalletAddress', () => { diff --git a/yarn.lock b/yarn.lock index 41d0dcd2be..f2e0dcba1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -985,10 +985,10 @@ monocle-ts "^2.3.13" newtype-ts "^0.3.5" -"@bitgo/wasm-dot@^1.5.0": - version "1.6.0" - resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.6.0.tgz#4a0a3e1447e1ee112d11f01399645c8e2b5d573c" - integrity sha512-YYvvmMz4OQRLq0OYdGIkNn9L32Uzi7cHaGBfuxtvVEf14w8VlA/gU/UdC6dat+oitVB4jHEyM54rltqNoBkKsA== +"@bitgo/wasm-dot@^1.7.0": + version "1.7.0" + resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.7.0.tgz#d22aafea9d38ebcb4b75d38538202237eadac685" + integrity sha512-KoXavJvyDHlEN+sWcigbgxYJtdFaU7gS0EkYQbNH4npVjNlzo6rL6gwjyWbyOy7oEs65DhpJ9vY5kRbE/bKiTQ== "@bitgo/wasm-solana@^2.6.0": version "2.6.0"