Skip to content

Commit cb4aa9f

Browse files
feat(sdk-core): add verification options for signing txHex
TICKET: WP-6188
1 parent 60cdd6a commit cb4aa9f

File tree

3 files changed

+188
-1
lines changed

3 files changed

+188
-1
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import nock = require('nock');
2+
import * as sinon from 'sinon';
3+
import * as assert from 'assert';
4+
import 'should';
5+
6+
import { BitGoAPI } from '@bitgo/sdk-api';
7+
import { TestBitGo } from '@bitgo/sdk-test';
8+
import { Tbtc } from '@bitgo/sdk-coin-btc';
9+
import { common, BaseCoin, BitGoBase, Wallet, WalletSignTransactionOptions } from '@bitgo/sdk-core';
10+
11+
describe('Wallet signTransaction with verifyTxParams', function () {
12+
let realWallet: Wallet;
13+
let basecoin: BaseCoin;
14+
15+
beforeEach(function () {
16+
const bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
17+
bitgo.initializeTestVars();
18+
bitgo.safeRegister('tbtc', Tbtc.createInstance);
19+
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+
sinon.stub(basecoin, 'signTransaction').resolves({ txHex: 'signed' });
43+
44+
realWallet = new Wallet(bitgo as unknown as BitGoBase, basecoin as unknown as BaseCoin, realWalletData);
45+
});
46+
47+
afterEach(function () {
48+
nock.cleanAll();
49+
});
50+
51+
it('should fail verification when expected recipient does not match actual transaction recipient', async function () {
52+
// Create a spy for verifyUserPublicKey to always return true
53+
54+
// Real transaction hex that sends to tb1p3dcj78muwwac8fvas43zr48vca70pue3eku6txvju0nxnhm0rgwsgv0k3v
55+
const realTxHex =
56+
'70736274ff0100890100000001e082ee5f3be60a260bd181d86cbc3ed1f2f53f6e33572138c3220a461d64e13d0100000000fdffffff0210270000000000002251208b712f1f7c73bb83a59d856221d4ecc77cf0f331cdb9a59992e3e669df6f1a1de3d5040000000000225120135ddb1d9e6e1987adacd341cb2e146544e69e9b96eed7aec27a4a03d83ac324000000004f010488b21e000000000000000000940a6f6c2d84214ba69e48354858dd8e4df2b0f36d51b6721516172c4b56922402c8d26710504a5a7965c8fce430057418802bc5a06e1987ede6897fb40e0a66b5044b8de8914f010488b21e0000000000000000009c6426c55cb8e0b186f7f776b42cb4de8118be401cce288bcb1ef70457f4072c0397f25ebd3f03c85333b1ffc14e04d051b476a5e48e4e0a5e999c0cf163c3178704758af64b4f010488b21e000000000000000000ab9a6eea233e963b74fd79afd9c71d467fb1ce1d91fe1681e2b7332e203baae6033e70fb09c6eb45d08aff2f067060c6345143918961d5bee0e1d8037b77001925047b156bd00001012b8e02050000000000225120a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f609001030400000000211686f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d671500758af64b000000000000000029000000110000002116b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14815004b8de89100000000000000002900000011000000011720cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7011820b558f0176c61865e960bbd14bff1e251b99cebb81fea61c0f931452dc029778648fc05424954474f01a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f6090cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7420286f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d6703b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd148000001052054977fb64552af2ac9901f8ca6d43e9980c64af82fa0bc24f01a7a95328f4d6901068e01c044202a24989f02cd1e9cf2560b48b3c7122c8a643e9ce22c6406a0d65b8f75053c9fad200665676f5990a63c2c73e626b3fc74156c4ef9a909ebd66ca83f7797602dcd26ac01c044200665676f5990a63c2c73e626b3fc74156c4ef9a909ebd66ca83f7797602dcd26ad2002617797bd49c31b5aa0c954b88b6f0cf366d876863db7a94285a2ed1f22b3c3ac210702617797bd49c31b5aa0c954b88b6f0cf366d876863db7a94285a2ed1f22b3c33501df60ca0827941d266e210000354802a610ce1c59b91616c99c795c74eb230ef94b8de8910000000000000000290000001500000021070665676f5990a63c2c73e626b3fc74156c4ef9a909ebd66ca83f7797602dcd26550246660e42faa7f1e691d65610dca0ca5e16a9c2230ac1188e164539092e81d885df60ca0827941d266e210000354802a610ce1c59b91616c99c795c74eb230ef97b156bd00000000000000000290000001500000021072a24989f02cd1e9cf2560b48b3c7122c8a643e9ce22c6406a0d65b8f75053c9f350146660e42faa7f1e691d65610dca0ca5e16a9c2230ac1188e164539092e81d885758af64b0000000000000000290000001500000000';
57+
58+
const txPrebuild = {
59+
txHex: realTxHex,
60+
walletId: '6840948b17e91662b782d55bbf988c4e',
61+
};
62+
63+
// Verification parameters with wrong expected recipient
64+
const verifyTxParams = {
65+
txParams: {
66+
recipients: [
67+
{
68+
address: '2Muux9UnVFCiGaYbX8D8FTsKaErkhLXRX5n', // Expected recipient
69+
amount: '10000', // Expected amount
70+
},
71+
],
72+
type: 'send',
73+
},
74+
};
75+
const bgUrl = common.Environments['mock'].uri;
76+
nock(bgUrl).get(`/api/v2/tbtc/key/6840947d037fdb798e0bf860e52cc4a8`).reply(200, {
77+
id: '6840947d037fdb798e0bf860e52cc4a8',
78+
pub: 'xpub661MyMwAqRbcG6k1Jmauo273gsqQSv8z94FHSkkPVbxZCPcCkVvCTVCBe1VF6A8rc4Dzo31LCB8rx2L3LedefymRcsmQTVNbH7QNvj2HY9B',
79+
});
80+
81+
nock(bgUrl).get(`/api/v2/tbtc/key/6840947e7c18efe3b0b77e9a75308aab`).reply(200, {
82+
id: '6840947e7c18efe3b0b77e9a75308aab',
83+
pub: 'xpub661MyMwAqRbcGFXVKNXUMN5UEy7kYYGjg8AN4HXVndnkXRrFnWPGtagUmcFDUhMy9RcxC65eQp5cSaGMhRfEfkt7sfU6cYmiakafzwGLRkq',
84+
});
85+
86+
nock(bgUrl).get(`/api/v2/tbtc/key/68409480bdf143f4a1d32474acc09baa`).reply(200, {
87+
id: '68409480bdf143f4a1d32474acc09baa',
88+
pub: 'xpub661MyMwAqRbcG1vLCx1SHbu94HL7LwAtguFD7bcwYSPbPMexPZbKEgNcWk3QREc8s4mV5MPh9ZX8UbQVxzY3no9GUo4HwCQdY8u3U4Hx5vR',
89+
});
90+
91+
const signParams: WalletSignTransactionOptions = {
92+
txPrebuild,
93+
verifyTxParams,
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('expected outputs missing in transaction prebuild'),
102+
`Error message should contain 'expected outputs missing in transaction prebuild', got: ${error.message}`
103+
);
104+
}
105+
});
106+
107+
it('should pass verification when expected recipient matches actual transaction recipient', async function () {
108+
// Same real transaction hex
109+
const realTxHex =
110+
'70736274ff0100890100000001c30076456d211a7c951bdfce1ee3c51d4fbba63effe7b72a7e598355360258810100000000fdffffff02e8030000000000002251203c8294de47961d271c0cc3a1d2a45f9debb4ff1414a58b1c63fb685ab27e47bd60cc0400000000002251206c59f342df3d02bdc20820489c963d38ac799f7559d6fa66635a65b017b51a55000000004f010488b21e000000000000000000940a6f6c2d84214ba69e48354858dd8e4df2b0f36d51b6721516172c4b56922402c8d26710504a5a7965c8fce430057418802bc5a06e1987ede6897fb40e0a66b5044b8de8914f010488b21e0000000000000000009c6426c55cb8e0b186f7f776b42cb4de8118be401cce288bcb1ef70457f4072c0397f25ebd3f03c85333b1ffc14e04d051b476a5e48e4e0a5e999c0cf163c3178704758af64b4f010488b21e000000000000000000ab9a6eea233e963b74fd79afd9c71d467fb1ce1d91fe1681e2b7332e203baae6033e70fb09c6eb45d08aff2f067060c6345143918961d5bee0e1d8037b77001925047b156bd00001012be3d504000000000022512056300043d90ac87b8cec52c013e4f4e005673efdb4f81187a686ab2e5312e013010304000000002116174f8b9e8e39aa567741a082d6aa3dc1c54eeef21a129cb6c835d0d00cb8f6f715004b8de89100000000000000002900000016000000211669a5e6ddfd5c5b7ef2ab67f945f2f80b4bb31975d66835dd388812bf76a2c1f91500758af64b00000000000000002900000016000000011720674c90f97968e7cb8ea607836bd032c5091ad04f6705b017c4fa640e0b977528011820b13af832b77b05ad8ec03628e865fe1543666d1db165c2c5be0eee2f2be911c048fc05424954474f0156300043d90ac87b8cec52c013e4f4e005673efdb4f81187a686ab2e5312e013674c90f97968e7cb8ea607836bd032c5091ad04f6705b017c4fa640e0b977528420269a5e6ddfd5c5b7ef2ab67f945f2f80b4bb31975d66835dd388812bf76a2c1f903174f8b9e8e39aa567741a082d6aa3dc1c54eeef21a129cb6c835d0d00cb8f6f7000001052058d7526ec06446670ed4a962867a23107515aba6c112d492a7f020eea7a85cdb01068e01c04420b7a3d89bb4d477aa4b47e393bace553aefd17bd1f8c51f1810dac3b8aa517f4ead204532ea3e0b6d89457d1ffe4b44cd9f0028f43a1f40f0e91e8306745fbcf86fb8ac01c044204532ea3e0b6d89457d1ffe4b44cd9f0028f43a1f40f0e91e8306745fbcf86fb8ad2087c6ec2bd90ee3141494456ff1525f5bf62381e9a66630ee2ce2f71398b1b4c0ac21074532ea3e0b6d89457d1ffe4b44cd9f0028f43a1f40f0e91e8306745fbcf86fb855020e054d24e61547905ebdad16202ad61cdc52ff424001d211bfec1c954f6cbcee793c053d56fe57a1d343047cb823edb85ba32c742fc0e39d921edf776a38427b7b156bd000000000000000002900000017000000210787c6ec2bd90ee3141494456ff1525f5bf62381e9a66630ee2ce2f71398b1b4c03501793c053d56fe57a1d343047cb823edb85ba32c742fc0e39d921edf776a38427b4b8de891000000000000000029000000170000002107b7a3d89bb4d477aa4b47e393bace553aefd17bd1f8c51f1810dac3b8aa517f4e35010e054d24e61547905ebdad16202ad61cdc52ff424001d211bfec1c954f6cbcee758af64b0000000000000000290000001700000000';
111+
const bgUrl = common.Environments['mock'].uri;
112+
nock(bgUrl).get(`/api/v2/tbtc/key/6840947d037fdb798e0bf860e52cc4a8`).reply(200, {
113+
id: '6840947d037fdb798e0bf860e52cc4a8',
114+
pub: 'xpub661MyMwAqRbcG6k1Jmauo273gsqQSv8z94FHSkkPVbxZCPcCkVvCTVCBe1VF6A8rc4Dzo31LCB8rx2L3LedefymRcsmQTVNbH7QNvj2HY9B',
115+
});
116+
117+
nock(bgUrl).get(`/api/v2/tbtc/key/6840947e7c18efe3b0b77e9a75308aab`).reply(200, {
118+
id: '6840947e7c18efe3b0b77e9a75308aab',
119+
pub: 'xpub661MyMwAqRbcGFXVKNXUMN5UEy7kYYGjg8AN4HXVndnkXRrFnWPGtagUmcFDUhMy9RcxC65eQp5cSaGMhRfEfkt7sfU6cYmiakafzwGLRkq',
120+
});
121+
122+
nock(bgUrl).get(`/api/v2/tbtc/key/68409480bdf143f4a1d32474acc09baa`).reply(200, {
123+
id: '68409480bdf143f4a1d32474acc09baa',
124+
pub: 'xpub661MyMwAqRbcG1vLCx1SHbu94HL7LwAtguFD7bcwYSPbPMexPZbKEgNcWk3QREc8s4mV5MPh9ZX8UbQVxzY3no9GUo4HwCQdY8u3U4Hx5vR',
125+
});
126+
127+
const txPrebuild = {
128+
txHex: realTxHex,
129+
walletId: '6840948b17e91662b782d55bbf988c4e',
130+
};
131+
132+
// Verification parameters with correct expected recipient
133+
const verifyTxParams: WalletSignTransactionOptions['verifyTxParams'] = {
134+
txParams: {
135+
recipients: [
136+
{
137+
address: 'tb1p8jpffhj8jcwjw8qvcwsa9fzlnh4mflc5zjjck8rrld594vn7g77slm250x', // Correct recipient
138+
amount: '1000', // Expected amount
139+
},
140+
],
141+
type: 'send',
142+
},
143+
};
144+
145+
const signParams: WalletSignTransactionOptions = {
146+
txPrebuild,
147+
verifyTxParams,
148+
pubs: [
149+
'xpub661MyMwAqRbcG6k1Jmauo273gsqQSv8z94FHSkkPVbxZCPcCkVvCTVCBe1VF6A8rc4Dzo31LCB8rx2L3LedefymRcsmQTVNbH7QNvj2HY9B',
150+
'xpub661MyMwAqRbcGFXVKNXUMN5UEy7kYYGjg8AN4HXVndnkXRrFnWPGtagUmcFDUhMy9RcxC65eQp5cSaGMhRfEfkt7sfU6cYmiakafzwGLRkq',
151+
'xpub661MyMwAqRbcG1vLCx1SHbu94HL7LwAtguFD7bcwYSPbPMexPZbKEgNcWk3QREc8s4mV5MPh9ZX8UbQVxzY3no9GUo4HwCQdY8u3U4Hx5vR',
152+
],
153+
prv: 'prv',
154+
};
155+
156+
const result = await realWallet.signTransaction(signParams);
157+
158+
// Verify the result
159+
result.should.have.property('txHex', 'signed');
160+
});
161+
});

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,
@@ -274,6 +275,14 @@ export interface WalletSignTransactionOptions extends WalletSignBaseOptions {
274275
apiVersion?: ApiVersion;
275276
multisigTypeVersion?: 'MPCv2';
276277
walletPassphrase?: string;
278+
/**
279+
* Optional transaction verification parameters. When provided, the transaction will be verified
280+
* using verifyTransaction before signing.
281+
*/
282+
verifyTxParams?: {
283+
txParams: TransactionParams;
284+
verification?: VerificationOptions;
285+
};
277286
[index: string]: unknown;
278287
}
279288

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,
@@ -1953,6 +1953,9 @@ export class Wallet implements IWallet {
19531953
* - txPrebuild
19541954
* - [keychain / key] (object) or prv (string)
19551955
* - walletPassphrase
1956+
* - verifyTxParams (optional) - when provided, the transaction will be verified before signing
1957+
* - txParams: transaction parameters used for verification
1958+
* - verification: optional verification options
19561959
* @return {*}
19571960
*/
19581961
async signTransaction(params: WalletSignTransactionOptions = {}): Promise<SignedTransaction | TxRequest> {
@@ -1997,6 +2000,20 @@ export class Wallet implements IWallet {
19972000
params.txPrebuild = { txRequestId };
19982001
}
19992002

2003+
// Verify transaction if verifyTxParams is provided
2004+
if (params.verifyTxParams && txPrebuild?.txHex) {
2005+
const verifyParams = {
2006+
txPrebuild: { ...txPrebuild },
2007+
txParams: params.verifyTxParams.txParams,
2008+
wallet: this as IWallet,
2009+
verification: params.verifyTxParams.verification,
2010+
reqId: params.reqId,
2011+
walletType: this.multisigType() as 'onchain' | 'tss',
2012+
};
2013+
2014+
await this.baseCoin.verifyTransaction(verifyParams);
2015+
}
2016+
20002017
if (
20012018
params.walletPassphrase &&
20022019
!(params.keychain || params.key) &&

0 commit comments

Comments
 (0)