11import assert from 'node:assert/strict' ;
22
3+ import nock = require( 'nock' ) ;
34import { CoinName , fixedScriptWallet , BIP32 , message } from '@bitgo/wasm-utxo' ;
45import * as utxolib from '@bitgo/utxo-lib' ;
56import { testutil } from '@bitgo/utxo-lib' ;
7+ import { common , Wallet } from '@bitgo/sdk-core' ;
8+ import { getSeed } from '@bitgo/sdk-test' ;
69
710import { explainPsbt as explainPsbtUtxolib , explainPsbtWasm } from '../../src/transaction/fixedScript' ;
811import { verifyKeySignature } from '../../src/verifyKey' ;
912import { SdkBackend } from '../../src/transaction' ;
1013
14+ import { defaultBitGo , getUtxoCoin } from './util' ;
15+
1116function explainPsbt (
1217 psbt : utxolib . bitgo . UtxoPsbt | fixedScriptWallet . BitGoPsbt ,
1318 walletKeys : utxolib . bitgo . RootWalletKeys ,
@@ -28,9 +33,25 @@ function explainPsbt(
2833
2934function describeWithBackend ( sdkBackend : SdkBackend ) {
3035 describe ( `Custom Change Wallets (sdkBackend=${ sdkBackend } )` , function ( ) {
36+ const coin = getUtxoCoin ( 'btc' ) ;
3137 const network = utxolib . networks . bitcoin ;
38+ const bgUrl = common . Environments [ defaultBitGo . getEnv ( ) ] . uri ;
3239 const rootWalletKeys = testutil . getDefaultWalletKeys ( ) ;
3340 const customChangeWalletKeys = testutil . getWalletKeysForSeed ( 'custom change' ) ;
41+ const userPrivateKey = BIP32 . fromBase58 ( rootWalletKeys . triple [ 0 ] . toBase58 ( ) ) . privateKey ! ;
42+
43+ const mainKeyIds = rootWalletKeys . triple . map ( ( k ) => getSeed ( k . neutered ( ) . toBase58 ( ) ) . toString ( 'hex' ) ) ;
44+ const customChangeKeyIds = customChangeWalletKeys . triple . map ( ( k ) =>
45+ getSeed ( k . neutered ( ) . toBase58 ( ) ) . toString ( 'hex' )
46+ ) ;
47+ const customChangeKeySignatures = Object . fromEntries (
48+ ( [ 'user' , 'backup' , 'bitgo' ] as const ) . map ( ( name , i ) => [
49+ name ,
50+ Buffer . from (
51+ message . signMessage ( customChangeWalletKeys . triple [ i ] . neutered ( ) . toBase58 ( ) , userPrivateKey )
52+ ) . toString ( 'hex' ) ,
53+ ] )
54+ ) as Record < 'user' | 'backup' | 'bitgo' , string > ;
3455
3556 const inputs : testutil . Input [ ] = [ { scriptType : 'p2sh' , value : BigInt ( 10000 ) } ] ;
3657 const outputs : testutil . Output [ ] = [
@@ -42,21 +63,34 @@ function describeWithBackend(sdkBackend: SdkBackend) {
4263 { scriptType : 'p2sh' , value : BigInt ( 3000 ) , walletKeys : null } ,
4364 ] ;
4465
45- let psbt : utxolib . bitgo . UtxoPsbt | fixedScriptWallet . BitGoPsbt = testutil . constructPsbt (
46- inputs ,
47- outputs ,
48- network ,
49- rootWalletKeys ,
50- 'unsigned' ,
51- {
52- addGlobalXPubs : true ,
53- }
54- ) ;
66+ const utxolibPsbt = testutil . constructPsbt ( inputs , outputs , network , rootWalletKeys , 'unsigned' , {
67+ addGlobalXPubs : true ,
68+ } ) ;
69+ const psbt : utxolib . bitgo . UtxoPsbt | fixedScriptWallet . BitGoPsbt =
70+ sdkBackend === 'wasm-utxo' ? fixedScriptWallet . BitGoPsbt . fromBytes ( utxolibPsbt . toBuffer ( ) , 'btc' ) : utxolibPsbt ;
71+
72+ const externalAddress = utxolib . address . fromOutputScript ( utxolibPsbt . txOutputs [ 2 ] . script , network ) ;
73+ const customChangeWalletId = 'custom-change-wallet-id' ;
74+ const mainWalletId = 'main-wallet-id' ;
75+
76+ function nockKeyFetch ( keyIds : string [ ] , keys : utxolib . bitgo . RootWalletKeys ) : nock . Scope [ ] {
77+ return keyIds . map ( ( id , i ) =>
78+ nock ( bgUrl )
79+ . get ( `/api/v2/${ coin . getChain ( ) } /key/${ id } ` )
80+ . reply ( 200 , { pub : keys . triple [ i ] . neutered ( ) . toBase58 ( ) } )
81+ ) ;
82+ }
5583
56- if ( sdkBackend === 'wasm-utxo' ) {
57- psbt = fixedScriptWallet . BitGoPsbt . fromBytes ( psbt . toBuffer ( ) , 'btc' ) ;
84+ function nockCustomChangeWallet ( ) : nock . Scope {
85+ return nock ( bgUrl ) . get ( `/api/v2/${ coin . getChain ( ) } /wallet/${ customChangeWalletId } ` ) . reply ( 200 , {
86+ id : customChangeWalletId ,
87+ keys : customChangeKeyIds ,
88+ coin : coin . getChain ( ) ,
89+ } ) ;
5890 }
5991
92+ afterEach ( ( ) => nock . cleanAll ( ) ) ;
93+
6094 it ( 'classifies custom change output when customChangePubs is provided' , function ( ) {
6195 const explanation = explainPsbt ( psbt , rootWalletKeys , customChangeWalletKeys , 'btc' ) ;
6296
@@ -85,7 +119,6 @@ function describeWithBackend(sdkBackend: SdkBackend) {
85119 } ) ;
86120
87121 it ( 'verifies valid custom change key signatures' , function ( ) {
88- const userPrivateKey = BIP32 . fromBase58 ( rootWalletKeys . triple [ 0 ] . toBase58 ( ) ) . privateKey ! ;
89122 const userPub = rootWalletKeys . triple [ 0 ] . neutered ( ) . toBase58 ( ) ;
90123
91124 for ( const key of customChangeWalletKeys . triple ) {
@@ -110,6 +143,97 @@ function describeWithBackend(sdkBackend: SdkBackend) {
110143 ) ;
111144 }
112145 } ) ;
146+
147+ describe ( 'parseTransaction' , function ( ) {
148+ it ( 'fetches custom change wallet keys and verifies signatures' , async function ( ) {
149+ const wallet = new Wallet ( defaultBitGo , coin , {
150+ id : mainWalletId ,
151+ keys : mainKeyIds ,
152+ coin : coin . getChain ( ) ,
153+ coinSpecific : { customChangeWalletId } ,
154+ customChangeKeySignatures,
155+ } ) ;
156+
157+ const nocks = [
158+ ...nockKeyFetch ( mainKeyIds , rootWalletKeys ) ,
159+ nockCustomChangeWallet ( ) ,
160+ ...nockKeyFetch ( customChangeKeyIds , customChangeWalletKeys ) ,
161+ ] ;
162+
163+ const parsed = await coin . parseTransaction ( {
164+ txParams : { recipients : [ { address : externalAddress , amount : '3000' } ] } ,
165+ txPrebuild : { txHex : utxolibPsbt . toHex ( ) , decodeWith : sdkBackend } ,
166+ wallet : wallet as unknown as import ( '../../src' ) . UtxoWallet ,
167+ } ) ;
168+
169+ for ( const n of nocks ) assert . ok ( n . isDone ( ) ) ;
170+
171+ assert . ok ( parsed . customChange ) ;
172+ assert . strictEqual ( parsed . customChange . keys . length , 3 ) ;
173+ for ( let i = 0 ; i < 3 ; i ++ ) {
174+ assert . strictEqual ( parsed . customChange . keys [ i ] . pub , customChangeWalletKeys . triple [ i ] . neutered ( ) . toBase58 ( ) ) ;
175+ }
176+
177+ assert . strictEqual ( parsed . explicitExternalOutputs . length , 1 ) ;
178+ assert . strictEqual ( parsed . explicitExternalOutputs [ 0 ] . amount , '3000' ) ;
179+ } ) ;
180+
181+ it ( 'has no custom change when wallet lacks customChangeWalletId' , async function ( ) {
182+ const wallet = new Wallet ( defaultBitGo , coin , {
183+ id : mainWalletId ,
184+ keys : mainKeyIds ,
185+ coin : coin . getChain ( ) ,
186+ coinSpecific : { } ,
187+ } ) ;
188+
189+ const nocks = nockKeyFetch ( mainKeyIds , rootWalletKeys ) ;
190+
191+ const parsed = await coin . parseTransaction ( {
192+ txParams : { recipients : [ { address : externalAddress , amount : '3000' } ] } ,
193+ txPrebuild : { txHex : utxolibPsbt . toHex ( ) , decodeWith : sdkBackend } ,
194+ wallet : wallet as unknown as import ( '../../src' ) . UtxoWallet ,
195+ } ) ;
196+
197+ for ( const n of nocks ) assert . ok ( n . isDone ( ) ) ;
198+
199+ assert . strictEqual ( parsed . customChange , undefined ) ;
200+ assert . strictEqual ( parsed . needsCustomChangeKeySignatureVerification , false ) ;
201+ } ) ;
202+
203+ it ( 'rejects invalid custom change key signatures' , async function ( ) {
204+ const wrongKey = BIP32 . fromBase58 ( testutil . getWalletKeysForSeed ( 'wrong' ) . triple [ 0 ] . toBase58 ( ) ) ;
205+ const badSignatures = Object . fromEntries (
206+ ( [ 'user' , 'backup' , 'bitgo' ] as const ) . map ( ( name , i ) => [
207+ name ,
208+ Buffer . from (
209+ message . signMessage ( customChangeWalletKeys . triple [ i ] . neutered ( ) . toBase58 ( ) , wrongKey . privateKey ! )
210+ ) . toString ( 'hex' ) ,
211+ ] )
212+ ) as Record < 'user' | 'backup' | 'bitgo' , string > ;
213+
214+ const wallet = new Wallet ( defaultBitGo , coin , {
215+ id : mainWalletId ,
216+ keys : mainKeyIds ,
217+ coin : coin . getChain ( ) ,
218+ coinSpecific : { customChangeWalletId } ,
219+ customChangeKeySignatures : badSignatures ,
220+ } ) ;
221+
222+ nockKeyFetch ( mainKeyIds , rootWalletKeys ) ;
223+ nockCustomChangeWallet ( ) ;
224+ nockKeyFetch ( customChangeKeyIds , customChangeWalletKeys ) ;
225+
226+ await assert . rejects (
227+ ( ) =>
228+ coin . parseTransaction ( {
229+ txParams : { recipients : [ { address : externalAddress , amount : '3000' } ] } ,
230+ txPrebuild : { txHex : utxolibPsbt . toHex ( ) , decodeWith : sdkBackend } ,
231+ wallet : wallet as unknown as import ( '../../src' ) . UtxoWallet ,
232+ } ) ,
233+ / f a i l e d t o v e r i f y c u s t o m c h a n g e .* k e y s i g n a t u r e /
234+ ) ;
235+ } ) ;
236+ } ) ;
113237 } ) ;
114238}
115239
0 commit comments