Skip to content

Commit 1285579

Browse files
feat(sdk-core): add verification options for signing txHex
TICKET: WP-6188
1 parent 425df98 commit 1285579

File tree

3 files changed

+257
-1
lines changed

3 files changed

+257
-1
lines changed

modules/sdk-core/src/bitgo/wallet/iWallet.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Message,
55
SignedMessage,
66
SignedTransaction,
7+
TransactionParams,
78
TransactionPrebuild,
89
VerificationOptions,
910
TypedData,
@@ -251,6 +252,14 @@ export interface WalletSignTransactionOptions extends WalletSignBaseOptions {
251252
apiVersion?: ApiVersion;
252253
multisigTypeVersion?: 'MPCv2';
253254
walletPassphrase?: string;
255+
/**
256+
* Optional transaction verification parameters. When provided, the transaction will be verified
257+
* using verifyTransaction before signing.
258+
*/
259+
verifyTxParams?: {
260+
txParams: TransactionParams;
261+
verification?: VerificationOptions;
262+
};
254263
[index: string]: unknown;
255264
}
256265

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ import {
8585
GetTransactionOptions,
8686
GetTransferOptions,
8787
GetUserPrvOptions,
88-
IWallet,
88+
type IWallet,
8989
ManageUnspentReservationOptions,
9090
MaximumSpendable,
9191
MaximumSpendableOptions,
@@ -1951,6 +1951,9 @@ export class Wallet implements IWallet {
19511951
* - txPrebuild
19521952
* - [keychain / key] (object) or prv (string)
19531953
* - walletPassphrase
1954+
* - verifyTxParams (optional) - when provided, the transaction will be verified before signing
1955+
* - txParams: transaction parameters used for verification
1956+
* - verification: optional verification options
19541957
* @return {*}
19551958
*/
19561959
async signTransaction(params: WalletSignTransactionOptions = {}): Promise<SignedTransaction | TxRequest> {
@@ -1995,6 +1998,20 @@ export class Wallet implements IWallet {
19951998
params.txPrebuild = { txRequestId };
19961999
}
19972000

2001+
// Verify transaction if verifyTxParams is provided
2002+
if (params.verifyTxParams && txPrebuild?.txHex) {
2003+
const verifyParams = {
2004+
txPrebuild: { ...txPrebuild },
2005+
txParams: params.verifyTxParams.txParams,
2006+
wallet: this as IWallet,
2007+
verification: params.verifyTxParams.verification,
2008+
reqId: params.reqId,
2009+
walletType: this.multisigType() as 'onchain' | 'tss',
2010+
};
2011+
2012+
await this.baseCoin.verifyTransaction(verifyParams);
2013+
}
2014+
19982015
if (
19992016
params.walletPassphrase &&
20002017
!(params.keychain || params.key) &&
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { BitGoAPI } from '@bitgo/sdk-api';
2+
import { TestBitGo } from '@bitgo/sdk-test';
3+
import * as assert from 'assert';
4+
import 'should';
5+
import { Wallet } from '../../../../src/bitgo/wallet/wallet';
6+
import { WalletSignTransactionOptions } from '../../../../src/bitgo/wallet/iWallet';
7+
import { BaseCoin, BitGoBase } from 'modules/sdk-core/src';
8+
import { Tbtc } from '@bitgo/sdk-coin-btc';
9+
10+
describe('Wallet signTransaction with verifyTxParams', function () {
11+
let realWallet: Wallet;
12+
13+
beforeEach(function () {
14+
const bitgo = TestBitGo.decorate(BitGoAPI, { env: 'test' });
15+
bitgo.initializeTestVars();
16+
bitgo.safeRegister('tbtc', Tbtc.createInstance);
17+
const basecoin = bitgo.coin('tbtc');
18+
19+
// Real wallet data from tbtc testnet
20+
const realWalletData = {
21+
id: '6840948b17e91662b782d55bbf988c4e',
22+
coin: 'tbtc',
23+
label: 'Test: User & Backup Signing',
24+
m: 2,
25+
n: 3,
26+
keys: [
27+
'6840947d037fdb798e0bf860e52cc4a8',
28+
'6840947e7c18efe3b0b77e9a75308aab',
29+
'68409480bdf143f4a1d32474acc09baa',
30+
],
31+
multisigType: 'onchain',
32+
type: 'hot',
33+
balance: 329034,
34+
balanceString: '329034',
35+
confirmedBalance: 329034,
36+
confirmedBalanceString: '329034',
37+
spendableBalance: 329034,
38+
spendableBalanceString: '329034',
39+
};
40+
41+
realWallet = new Wallet(bitgo as unknown as BitGoBase, basecoin as unknown as BaseCoin, realWalletData);
42+
});
43+
44+
it('should fail verification when expected recipient does not match actual transaction recipient', async function () {
45+
// Real transaction hex that sends to tb1qvuyyput9dy5j8j8gwzwjw8jx0z2gq778p0xyna28tzyu0z0anjfs3pf2mp
46+
const realTxHex =
47+
'70736274ff0100890100000001e082ee5f3be60a260bd181d86cbc3ed1f2f53f6e33572138c3220a461d64e13d0100000000fdffffff021027000000000000220020670840f165692923c8e8709d271e467894807bc70bcc49f5475889c789fd9c9375d50400000000002251205ae846b2c844e131cefcf635e8566a683a3f96c97410c43413f919cc9247e182000000004f010488b21e000000000000000000940a6f6c2d84214ba69e48354858dd8e4df2b0f36d51b6721516172c4b56922402c8d26710504a5a7965c8fce430057418802bc5a06e1987ede6897fb40e0a66b5044b8de8914f010488b21e0000000000000000009c6426c55cb8e0b186f7f776b42cb4de8118be401cce288bcb1ef70457f4072c0397f25ebd3f03c85333b1ffc14e04d051b476a5e48e4e0a5e999c0cf163c3178704758af64b4f010488b21e000000000000000000ab9a6eea233e963b74fd79afd9c71d467fb1ce1d91fe1681e2b7332e203baae6033e70fb09c6eb45d08aff2f067060c6345143918961d5bee0e1d8037b77001925047b156bd00001012b8e02050000000000225120a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f609001030400000000211686f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d671500758af64b000000000000000029000000110000002116b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14815004b8de89100000000000000002900000011000000011720cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7011820b558f0176c61865e960bbd14bff1e251b99cebb81fea61c0f931452dc029778648fc05424954474f01a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f6090cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7420286f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d6703b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14800000105200fc5866bddca6f779664a6910eda9af586287893c805b717b90af777cb5b2a0501068e01c04420ac366be7d240d82968539bdab6fc81fdb016f0b73d720afd1bfdd50bc8b37317ad206c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf9547245ac01c044206c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf9547245ad2005e1bca7220c83cc2d6a2c596f8fe00b9bc084bee5667cab06fa3354f1565a91ac210705e1bca7220c83cc2d6a2c596f8fe00b9bc084bee5667cab06fa3354f1565a91350177065e65a3639ada742d46b3d282980784549337c12f015385b727711d98b4374b8de8910000000000000000290000001200000021076c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf954724555020e7b30f7ec73ccbc76d8e08bc35c7b4837defce7f184f658c95b94bdec831a3277065e65a3639ada742d46b3d282980784549337c12f015385b727711d98b4374b156bd0000000000000000029000000120000002107ac366be7d240d82968539bdab6fc81fdb016f0b73d720afd1bfdd50bc8b3731735010e7b30f7ec73ccbc76d8e08bc35c7b4837defce7f184f658c95b94bdec831a32758af64b0000000000000000290000001200000000';
48+
49+
const txPrebuild = {
50+
txHex: realTxHex,
51+
walletId: '6840948b17e91662b782d55bbf988c4e',
52+
};
53+
54+
// Verification parameters with wrong expected recipient
55+
const verifyTxParams = {
56+
txParams: {
57+
recipients: [
58+
{
59+
address: '2Muux9UnVFCiGaYbX8D8FTsKaErkhLXRX5n', // Expected recipient
60+
amount: '10000', // Expected amount
61+
},
62+
],
63+
type: 'send',
64+
},
65+
verification: {
66+
disableNetworking: true, // Disable networking for unit test
67+
},
68+
};
69+
70+
const signParams: WalletSignTransactionOptions = {
71+
txPrebuild,
72+
verifyTxParams,
73+
prv: 'test-private-key',
74+
};
75+
76+
try {
77+
await realWallet.signTransaction(signParams);
78+
assert.fail('Should have thrown verification error');
79+
} catch (error) {
80+
assert.ok(
81+
error.message.includes('recipient address mismatch'),
82+
`Error message should contain 'recipient address mismatch', got: ${error.message}`
83+
);
84+
assert.ok(
85+
error.message.includes('2Muux9UnVFCiGaYbX8D8FTsKaErkhLXRX5n'),
86+
`Error message should contain '2Muux9UnVFCiGaYbX8D8FTsKaErkhLXRX5n', got: ${error.message}`
87+
);
88+
assert.ok(
89+
error.message.includes('tb1qvuyyput9dy5j8j8gwzwjw8jx0z2gq778p0xyna28tzyu0z0anjfs3pf2mp'),
90+
`Error message should contain 'tb1qvuyyput9dy5j8j8gwzwjw8jx0z2gq778p0xyna28tzyu0z0anjfs3pf2mp', got: ${error.message}`
91+
);
92+
}
93+
94+
// Verify that verifyTransaction was called with correct parameters
95+
mockBaseCoinReal.verifyTransaction.should.have.been.calledOnce;
96+
const verifyCall = mockBaseCoinReal.verifyTransaction.getCall(0);
97+
const verifyCallArgs = verifyCall.args[0];
98+
99+
verifyCallArgs.should.have.property('txPrebuild');
100+
verifyCallArgs.txPrebuild.should.have.property('txHex', realTxHex);
101+
verifyCallArgs.txPrebuild.should.have.property('walletId', '6840948b17e91662b782d55bbf988c4e');
102+
verifyCallArgs.should.have.property('txParams', verifyTxParams.txParams);
103+
verifyCallArgs.should.have.property('verification', verifyTxParams.verification);
104+
verifyCallArgs.should.have.property('wallet', realWallet);
105+
verifyCallArgs.should.have.property('walletType', 'onchain');
106+
107+
// Verify that signTransaction was not called due to verification failure
108+
mockBaseCoinReal.signTransaction.should.not.have.been.called;
109+
});
110+
111+
it('should pass verification when expected recipient matches actual transaction recipient', async function () {
112+
// Same real transaction hex
113+
const realTxHex =
114+
'70736274ff0100890100000001e082ee5f3be60a260bd181d86cbc3ed1f2f53f6e33572138c3220a461d64e13d0100000000fdffffff021027000000000000220020670840f165692923c8e8709d271e467894807bc70bcc49f5475889c789fd9c9375d50400000000002251205ae846b2c844e131cefcf635e8566a683a3f96c97410c43413f919cc9247e182000000004f010488b21e000000000000000000940a6f6c2d84214ba69e48354858dd8e4df2b0f36d51b6721516172c4b56922402c8d26710504a5a7965c8fce430057418802bc5a06e1987ede6897fb40e0a66b5044b8de8914f010488b21e0000000000000000009c6426c55cb8e0b186f7f776b42cb4de8118be401cce288bcb1ef70457f4072c0397f25ebd3f03c85333b1ffc14e04d051b476a5e48e4e0a5e999c0cf163c3178704758af64b4f010488b21e000000000000000000ab9a6eea233e963b74fd79afd9c71d467fb1ce1d91fe1681e2b7332e203baae6033e70fb09c6eb45d08aff2f067060c6345143918961d5bee0e1d8037b77001925047b156bd00001012b8e02050000000000225120a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f609001030400000000211686f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d671500758af64b000000000000000029000000110000002116b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14815004b8de89100000000000000002900000011000000011720cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7011820b558f0176c61865e960bbd14bff1e251b99cebb81fea61c0f931452dc029778648fc05424954474f01a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f6090cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7420286f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d6703b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14800000105200fc5866bddca6f779664a6910eda9af586287893c805b717b90af777cb5b2a0501068e01c04420ac366be7d240d82968539bdab6fc81fdb016f0b73d720afd1bfdd50bc8b37317ad206c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf9547245ac01c044206c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf9547245ad2005e1bca7220c83cc2d6a2c596f8fe00b9bc084bee5667cab06fa3354f1565a91ac210705e1bca7220c83cc2d6a2c596f8fe00b9bc084bee5667cab06fa3354f1565a91350177065e65a3639ada742d46b3d282980784549337c12f015385b727711d98b4374b8de8910000000000000000290000001200000021076c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf954724555020e7b30f7ec73ccbc76d8e08bc35c7b4837defce7f184f658c95b94bdec831a3277065e65a3639ada742d46b3d282980784549337c12f015385b727711d98b4374b156bd0000000000000000029000000120000002107ac366be7d240d82968539bdab6fc81fdb016f0b73d720afd1bfdd50bc8b3731735010e7b30f7ec73ccbc76d8e08bc35c7b4837defce7f184f658c95b94bdec831a32758af64b0000000000000000290000001200000000';
115+
116+
const txPrebuild = {
117+
txHex: realTxHex,
118+
walletId: '6840948b17e91662b782d55bbf988c4e',
119+
};
120+
121+
// Verification parameters with correct expected recipient
122+
const verifyTxParams = {
123+
txParams: {
124+
recipients: [
125+
{
126+
address: 'tb1qvuyyput9dy5j8j8gwzwjw8jx0z2gq778p0xyna28tzyu0z0anjfs3pf2mp', // Correct recipient
127+
amount: '10000', // Expected amount
128+
},
129+
],
130+
type: 'send',
131+
},
132+
verification: {
133+
disableNetworking: true,
134+
},
135+
};
136+
137+
const signParams: WalletSignTransactionOptions = {
138+
txPrebuild,
139+
verifyTxParams,
140+
prv: 'test-private-key',
141+
};
142+
143+
// Mock presignTransaction to return the same params
144+
mockBaseCoinReal.presignTransaction.resolves(signParams);
145+
146+
// Mock verifyTransaction to succeed
147+
mockBaseCoinReal.verifyTransaction.resolves(true);
148+
149+
// Mock signTransaction to return a signed transaction
150+
mockBaseCoinReal.signTransaction.resolves({
151+
txHex: realTxHex,
152+
halfSigned: {},
153+
});
154+
155+
const result = await realWallet.signTransaction(signParams);
156+
157+
// Verify that verifyTransaction was called
158+
mockBaseCoinReal.verifyTransaction.should.have.been.calledOnce;
159+
160+
// Verify that signTransaction was called after successful verification
161+
mockBaseCoinReal.signTransaction.should.have.been.calledOnce;
162+
163+
// Verify the result
164+
result.should.have.property('txHex', realTxHex);
165+
});
166+
167+
it('should handle amount verification in addition to address verification', async function () {
168+
const realTxHex =
169+
'70736274ff0100890100000001e082ee5f3be60a260bd181d86cbc3ed1f2f53f6e33572138c3220a461d64e13d0100000000fdffffff021027000000000000220020670840f165692923c8e8709d271e467894807bc70bcc49f5475889c789fd9c9375d50400000000002251205ae846b2c844e131cefcf635e8566a683a3f96c97410c43413f919cc9247e182000000004f010488b21e000000000000000000940a6f6c2d84214ba69e48354858dd8e4df2b0f36d51b6721516172c4b56922402c8d26710504a5a7965c8fce430057418802bc5a06e1987ede6897fb40e0a66b5044b8de8914f010488b21e0000000000000000009c6426c55cb8e0b186f7f776b42cb4de8118be401cce288bcb1ef70457f4072c0397f25ebd3f03c85333b1ffc14e04d051b476a5e48e4e0a5e999c0cf163c3178704758af64b4f010488b21e000000000000000000ab9a6eea233e963b74fd79afd9c71d467fb1ce1d91fe1681e2b7332e203baae6033e70fb09c6eb45d08aff2f067060c6345143918961d5bee0e1d8037b77001925047b156bd00001012b8e02050000000000225120a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f609001030400000000211686f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d671500758af64b000000000000000029000000110000002116b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14815004b8de89100000000000000002900000011000000011720cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7011820b558f0176c61865e960bbd14bff1e251b99cebb81fea61c0f931452dc029778648fc05424954474f01a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f6090cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7420286f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d6703b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14800000105200fc5866bddca6f779664a6910eda9af586287893c805b717b90af777cb5b2a0501068e01c04420ac366be7d240d82968539bdab6fc81fdb016f0b73d720afd1bfdd50bc8b37317ad206c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf9547245ac01c044206c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf9547245ad2005e1bca7220c83cc2d6a2c596f8fe00b9bc084bee5667cab06fa3354f1565a91ac210705e1bca7220c83cc2d6a2c596f8fe00b9bc084bee5667cab06fa3354f1565a91350177065e65a3639ada742d46b3d282980784549337c12f015385b727711d98b4374b8de8910000000000000000290000001200000021076c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf954724555020e7b30f7ec73ccbc76d8e08bc35c7b4837defce7f184f658c95b94bdec831a3277065e65a3639ada742d46b3d282980784549337c12f015385b727711d98b4374b156bd0000000000000000029000000120000002107ac366be7d240d82968539bdab6fc81fdb016f0b73d720afd1bfdd50bc8b3731735010e7b30f7ec73ccbc76d8e08bc35c7b4837defce7f184f658c95b94bdec831a32758af64b0000000000000000290000001200000000';
170+
171+
const txPrebuild = {
172+
txHex: realTxHex,
173+
walletId: '6840948b17e91662b782d55bbf988c4e',
174+
};
175+
176+
// Verification parameters with correct address but wrong amount
177+
const verifyTxParams = {
178+
txParams: {
179+
recipients: [
180+
{
181+
address: 'tb1qvuyyput9dy5j8j8gwzwjw8jx0z2gq778p0xyna28tzyu0z0anjfs3pf2mp', // Correct recipient
182+
amount: '50000', // Wrong amount (actual is 10000 satoshis)
183+
},
184+
],
185+
type: 'send',
186+
},
187+
verification: {
188+
disableNetworking: true,
189+
},
190+
};
191+
192+
const signParams: WalletSignTransactionOptions = {
193+
txPrebuild,
194+
verifyTxParams,
195+
prv: 'test-private-key',
196+
};
197+
198+
// Mock presignTransaction to return the same params
199+
mockBaseCoinReal.presignTransaction.resolves(signParams);
200+
201+
// Mock verifyTransaction to fail with amount mismatch error
202+
mockBaseCoinReal.verifyTransaction.rejects(
203+
new Error('Transaction verification failed: amount mismatch. Expected 50000 but transaction sends 10000')
204+
);
205+
206+
try {
207+
await realWallet.signTransaction(signParams);
208+
assert.fail('Should have thrown verification error');
209+
} catch (error) {
210+
assert.ok(
211+
error.message.includes('amount mismatch'),
212+
`Error message should contain 'amount mismatch', got: ${error.message}`
213+
);
214+
assert.ok(
215+
error.message.includes('Expected 50000'),
216+
`Error message should contain 'Expected 50000', got: ${error.message}`
217+
);
218+
assert.ok(
219+
error.message.includes('sends 10000'),
220+
`Error message should contain 'sends 10000', got: ${error.message}`
221+
);
222+
}
223+
224+
// Verify that verifyTransaction was called
225+
mockBaseCoinReal.verifyTransaction.should.have.been.calledOnce;
226+
227+
// Verify that signTransaction was not called due to verification failure
228+
mockBaseCoinReal.signTransaction.should.not.have.been.called;
229+
});
230+
});

0 commit comments

Comments
 (0)