Skip to content

Commit c4e79ce

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

File tree

3 files changed

+277
-1
lines changed

3 files changed

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

0 commit comments

Comments
 (0)