@@ -92,15 +92,18 @@ function explainCommon<TNumber extends number | bigint>(
9292 return { displayOrder, id : tx . getId ( ) , ...outputDetails , fee, locktime } ;
9393}
9494
95- function getRootWalletKeys ( params : { pubs ?: string [ ] } ) {
95+ function getRootWalletKeys ( params : { pubs ?: bitgo . RootWalletKeys | string [ ] } ) : bitgo . RootWalletKeys | undefined {
96+ if ( params . pubs instanceof bitgo . RootWalletKeys ) {
97+ return params . pubs ;
98+ }
9699 const keys = params . pubs ?. map ( ( xpub ) => bip32 . fromBase58 ( xpub ) ) ;
97100 return keys && keys . length === 3 ? new bitgo . RootWalletKeys ( keys as Triple < BIP32Interface > ) : undefined ;
98101}
99102
100103function getPsbtInputSignaturesCount (
101104 psbt : bitgo . UtxoPsbt ,
102105 params : {
103- pubs ?: string [ ] ;
106+ pubs ?: bitgo . RootWalletKeys | string [ ] ;
104107 }
105108) {
106109 const rootWalletKeys = getRootWalletKeys ( params ) ;
@@ -113,7 +116,7 @@ function getTxInputSignaturesCount<TNumber extends number | bigint>(
113116 tx : bitgo . UtxoTransaction < TNumber > ,
114117 params : {
115118 txInfo ?: { unspents ?: bitgo . Unspent < TNumber > [ ] } ;
116- pubs ?: string [ ] ;
119+ pubs ?: bitgo . RootWalletKeys | string [ ] ;
117120 } ,
118121 network : utxolib . Network
119122) {
@@ -142,162 +145,165 @@ function getTxInputSignaturesCount<TNumber extends number | bigint>(
142145 } ) ;
143146}
144147
145- /**
146- * Decompose a raw psbt into useful information, such as the total amounts,
147- * change amounts, and transaction outputs.
148- */
149- export function explainPsbt < TNumber extends number | bigint , Tx extends bitgo . UtxoTransaction < bigint > > (
150- psbt : bitgo . UtxoPsbt < Tx > ,
151- params : {
152- pubs ?: string [ ] ;
153- txInfo ?: { unspents ?: bitgo . Unspent < TNumber > [ ] } ;
154- } ,
155- network : utxolib . Network ,
156- { strict = false } : { strict ?: boolean } = { }
157- ) : TransactionExplanation {
158- const txOutputs = psbt . txOutputs ;
159- const txInputs = psbt . txInputs ;
148+ function getChainAndIndexFromBip32Derivations ( output : bitgo . PsbtOutput ) {
149+ const derivations = output . bip32Derivation ?? output . tapBip32Derivation ?? undefined ;
150+ if ( ! derivations ) {
151+ return undefined ;
152+ }
153+ const paths = derivations . map ( ( d ) => d . path ) ;
154+ if ( ! paths || paths . length !== 3 ) {
155+ throw new Error ( 'expected 3 paths in bip32Derivation or tapBip32Derivation' ) ;
156+ }
157+ if ( ! paths . every ( ( p ) => paths [ 0 ] === p ) ) {
158+ throw new Error ( 'expected all paths to be the same' ) ;
159+ }
160160
161- function getChainAndIndexFromBip32Derivations ( output : bitgo . PsbtOutput ) {
162- const derivations = output . bip32Derivation ?? output . tapBip32Derivation ?? undefined ;
163- if ( ! derivations ) {
164- return undefined ;
165- }
166- const paths = derivations . map ( ( d ) => d . path ) ;
167- if ( ! paths || paths . length !== 3 ) {
168- throw new Error ( 'expected 3 paths in bip32Derivation or tapBip32Derivation' ) ;
169- }
170- if ( ! paths . every ( ( p ) => paths [ 0 ] === p ) ) {
171- throw new Error ( 'expected all paths to be the same' ) ;
161+ paths . forEach ( ( path ) => {
162+ if ( paths [ 0 ] !== path ) {
163+ throw new Error (
164+ 'Unable to get a single chain and index on the output because there are different paths for different keys'
165+ ) ;
172166 }
167+ } ) ;
168+ return utxolib . bitgo . getChainAndIndexFromPath ( paths [ 0 ] ) ;
169+ }
173170
174- paths . forEach ( ( path ) => {
175- if ( paths [ 0 ] !== path ) {
176- throw new Error (
177- 'Unable to get a single chain and index on the output because there are different paths for different keys'
178- ) ;
171+ function getChangeInfo ( psbt : bitgo . UtxoPsbt ) : ChangeAddressInfo [ ] | undefined {
172+ try {
173+ return utxolib . bitgo . findInternalOutputIndices ( psbt ) . map ( ( i ) => {
174+ const derivationInformation = getChainAndIndexFromBip32Derivations ( psbt . data . outputs [ i ] ) ;
175+ if ( ! derivationInformation ) {
176+ throw new Error ( 'could not find derivation information on bip32Derivation or tapBip32Derivation' ) ;
179177 }
178+ return {
179+ address : utxolib . address . fromOutputScript ( psbt . txOutputs [ i ] . script , psbt . network ) ,
180+ external : false ,
181+ ...derivationInformation ,
182+ } ;
180183 } ) ;
181- return utxolib . bitgo . getChainAndIndexFromPath ( paths [ 0 ] ) ;
184+ } catch ( e ) {
185+ if ( e instanceof utxolib . bitgo . ErrorNoMultiSigInputFound ) {
186+ return undefined ;
187+ }
188+ throw e ;
182189 }
190+ }
183191
184- function getChangeInfo ( ) {
185- try {
186- return utxolib . bitgo . findInternalOutputIndices ( psbt ) . map ( ( i ) => {
187- const derivationInformation = getChainAndIndexFromBip32Derivations ( psbt . data . outputs [ i ] ) ;
188- if ( ! derivationInformation ) {
189- throw new Error ( 'could not find derivation information on bip32Derivation or tapBip32Derivation' ) ;
190- }
191- return {
192- address : utxolib . address . fromOutputScript ( txOutputs [ i ] . script , network ) ,
193- external : false ,
194- ...derivationInformation ,
195- } ;
196- } ) ;
197- } catch ( e ) {
198- if ( e instanceof utxolib . bitgo . ErrorNoMultiSigInputFound ) {
199- return undefined ;
200- }
201- throw e ;
202- }
192+ /**
193+ * Extract PayGo address proof information from the PSBT if present
194+ * @returns Information about the PayGo proof, including the output index and address
195+ */
196+ function getPayGoVerificationInfo (
197+ psbt : bitgo . UtxoPsbt ,
198+ network : utxolib . Network
199+ ) : { outputIndex : number ; verificationPubkey : string } | undefined {
200+ let outputIndex : number | undefined = undefined ;
201+ let address : string | undefined = undefined ;
202+ // Check if this PSBT has any PayGo address proofs
203+ if ( ! utxocore . paygo . psbtOutputIncludesPaygoAddressProof ( psbt ) ) {
204+ return undefined ;
203205 }
204206
205- /**
206- * Extract PayGo address proof information from the PSBT if present
207- * @returns Information about the PayGo proof, including the output index and address
208- */
209- function getPayGoVerificationInfo ( ) : { outputIndex : number ; verificationPubkey : string } | undefined {
210- let outputIndex : number | undefined = undefined ;
211- let address : string | undefined = undefined ;
212- // Check if this PSBT has any PayGo address proofs
213- if ( ! utxocore . paygo . psbtOutputIncludesPaygoAddressProof ( psbt ) ) {
214- return undefined ;
215- }
207+ // This pulls the pubkey depending on given network
208+ const verificationPubkey = getPayGoVerificationPubkey ( network ) ;
209+ // find which output index that contains the PayGo proof
210+ outputIndex = utxocore . paygo . getPayGoAddressProofOutputIndex ( psbt ) ;
211+ if ( outputIndex === undefined || ! verificationPubkey ) {
212+ return undefined ;
213+ }
214+ const output = psbt . txOutputs [ outputIndex ] ;
215+ address = utxolib . address . fromOutputScript ( output . script , network ) ;
216+ if ( ! address ) {
217+ throw new Error ( `Can not derive address ${ address } Pay Go Attestation.` ) ;
218+ }
216219
217- // This pulls the pubkey depending on given network
218- const verificationPubkey = getPayGoVerificationPubkey ( network ) ;
219- // find which output index that contains the PayGo proof
220- outputIndex = utxocore . paygo . getPayGoAddressProofOutputIndex ( psbt ) ;
221- if ( outputIndex === undefined || ! verificationPubkey ) {
222- return undefined ;
223- }
224- const output = txOutputs [ outputIndex ] ;
225- address = utxolib . address . fromOutputScript ( output . script , network ) ;
226- if ( ! address ) {
227- throw new Error ( `Can not derive address ${ address } Pay Go Attestation.` ) ;
228- }
220+ return { outputIndex, verificationPubkey } ;
221+ }
229222
230- return { outputIndex, verificationPubkey } ;
231- }
223+ /**
224+ * Extract the BIP322 messages and addresses from the PSBT inputs and perform
225+ * verification on the transaction to ensure that it meets the BIP322 requirements.
226+ * @returns An array of objects containing the message and address for each input,
227+ * or undefined if no BIP322 messages are found.
228+ */
229+ function getBip322MessageInfoAndVerify ( psbt : bitgo . UtxoPsbt , network : utxolib . Network ) : Bip322Message [ ] | undefined {
230+ const bip322Messages : { message : string ; address : string } [ ] = [ ] ;
231+ for ( let i = 0 ; i < psbt . data . inputs . length ; i ++ ) {
232+ const message = bip322 . getBip322ProofMessageAtIndex ( psbt , i ) ;
233+ if ( message ) {
234+ const input = psbt . data . inputs [ i ] ;
235+ if ( ! input . witnessUtxo ) {
236+ throw new Error ( `Missing witnessUtxo for input index ${ i } ` ) ;
237+ }
238+ if ( ! input . nonWitnessUtxo ) {
239+ throw new Error ( `Missing nonWitnessUtxo for input index ${ i } ` ) ;
240+ }
241+ const scriptPubKey = input . witnessUtxo . script ;
232242
233- /**
234- * Extract the BIP322 messages and addresses from the PSBT inputs and perform
235- * verification on the transaction to ensure that it meets the BIP322 requirements.
236- * @returns An array of objects containing the message and address for each input,
237- * or undefined if no BIP322 messages are found.
238- */
239- function getBip322MessageInfoAndVerify ( ) : Bip322Message [ ] | undefined {
240- const bip322Messages : { message : string ; address : string } [ ] = [ ] ;
241- for ( let i = 0 ; i < psbt . data . inputs . length ; i ++ ) {
242- const message = bip322 . getBip322ProofMessageAtIndex ( psbt , i ) ;
243- if ( message ) {
244- const input = psbt . data . inputs [ i ] ;
245- if ( ! input . witnessUtxo ) {
246- throw new Error ( `Missing witnessUtxo for input index ${ i } ` ) ;
247- }
248- if ( ! input . nonWitnessUtxo ) {
249- throw new Error ( `Missing nonWitnessUtxo for input index ${ i } ` ) ;
250- }
251- const scriptPubKey = input . witnessUtxo . script ;
252-
253- // Verify that the toSpend transaction can be recreated in the PSBT and is encoded correctly in the nonWitnessUtxo
254- const toSpend = bip322 . buildToSpendTransaction ( scriptPubKey , message ) ;
255- const toSpendB64 = toSpend . toBuffer ( ) . toString ( 'base64' ) ;
256- if ( input . nonWitnessUtxo . toString ( 'base64' ) !== toSpendB64 ) {
257- throw new Error ( `Non-witness UTXO does not match the expected toSpend transaction at input index ${ i } ` ) ;
258- }
259-
260- // Verify that the toSpend transaction ID matches the input's referenced transaction ID
261- if ( toSpend . getId ( ) !== utxolib . bitgo . getOutputIdForInput ( txInputs [ i ] ) . txid ) {
262- throw new Error ( `ToSpend transaction ID does not match the input at index ${ i } ` ) ;
263- }
264-
265- // Verify the input specifics
266- if ( txInputs [ i ] . sequence !== 0 ) {
267- throw new Error ( `Unexpected sequence number at input index ${ i } : ${ txInputs [ i ] . sequence } . Expected 0.` ) ;
268- }
269- if ( txInputs [ i ] . index !== 0 ) {
270- throw new Error ( `Unexpected input index at position ${ i } : ${ txInputs [ i ] . index } . Expected 0.` ) ;
271- }
272-
273- bip322Messages . push ( {
274- message : message . toString ( 'utf8' ) ,
275- address : utxolib . address . fromOutputScript ( scriptPubKey , network ) ,
276- } ) ;
243+ // Verify that the toSpend transaction can be recreated in the PSBT and is encoded correctly in the nonWitnessUtxo
244+ const toSpend = bip322 . buildToSpendTransaction ( scriptPubKey , message ) ;
245+ const toSpendB64 = toSpend . toBuffer ( ) . toString ( 'base64' ) ;
246+ if ( input . nonWitnessUtxo . toString ( 'base64' ) !== toSpendB64 ) {
247+ throw new Error ( `Non-witness UTXO does not match the expected toSpend transaction at input index ${ i } ` ) ;
277248 }
278- }
279249
280- if ( bip322Messages . length > 0 ) {
281- // If there is a BIP322 message in any input, all inputs must have one.
282- if ( bip322Messages . length !== psbt . data . inputs . length ) {
283- throw new Error ( 'Inconsistent BIP322 messages across inputs.' ) ;
250+ // Verify that the toSpend transaction ID matches the input's referenced transaction ID
251+ if ( toSpend . getId ( ) !== utxolib . bitgo . getOutputIdForInput ( psbt . txInputs [ i ] ) . txid ) {
252+ throw new Error ( `ToSpend transaction ID does not match the input at index ${ i } ` ) ;
284253 }
285254
286- // Verify the transaction specifics for BIP322
287- if ( psbt . version !== 0 && psbt . version !== 2 ) {
288- throw new Error ( `Unsupported PSBT version for BIP322 : ${ psbt . version } . Expected 0 ` ) ;
255+ // Verify the input specifics
256+ if ( psbt . txInputs [ i ] . sequence !== 0 ) {
257+ throw new Error ( `Unexpected sequence number at input index ${ i } : ${ psbt . txInputs [ i ] . sequence } . Expected 0. ` ) ;
289258 }
290- if ( psbt . data . outputs . length !== 1 || txOutputs [ 0 ] . script . toString ( 'hex' ) !== '6a' || txOutputs [ 0 ] . value !== 0n ) {
291- throw new Error ( `Invalid PSBT outputs for BIP322. Expected exactly one OP_RETURN output with zero value .` ) ;
259+ if ( psbt . txInputs [ i ] . index !== 0 ) {
260+ throw new Error ( `Unexpected input index at position ${ i } : ${ psbt . txInputs [ i ] . index } . Expected 0 .` ) ;
292261 }
293262
294- return bip322Messages ;
263+ bip322Messages . push ( {
264+ message : message . toString ( 'utf8' ) ,
265+ address : utxolib . address . fromOutputScript ( scriptPubKey , network ) ,
266+ } ) ;
267+ }
268+ }
269+
270+ if ( bip322Messages . length > 0 ) {
271+ // If there is a BIP322 message in any input, all inputs must have one.
272+ if ( bip322Messages . length !== psbt . data . inputs . length ) {
273+ throw new Error ( 'Inconsistent BIP322 messages across inputs.' ) ;
295274 }
296275
297- return undefined ;
276+ // Verify the transaction specifics for BIP322
277+ if ( psbt . version !== 0 && psbt . version !== 2 ) {
278+ throw new Error ( `Unsupported PSBT version for BIP322: ${ psbt . version } . Expected 0 ` ) ;
279+ }
280+ if (
281+ psbt . data . outputs . length !== 1 ||
282+ psbt . txOutputs [ 0 ] . script . toString ( 'hex' ) !== '6a' ||
283+ psbt . txOutputs [ 0 ] . value !== 0n
284+ ) {
285+ throw new Error ( `Invalid PSBT outputs for BIP322. Expected exactly one OP_RETURN output with zero value.` ) ;
286+ }
287+
288+ return bip322Messages ;
298289 }
299290
300- const payGoVerificationInfo = getPayGoVerificationInfo ( ) ;
291+ return undefined ;
292+ }
293+
294+ /**
295+ * Decompose a raw psbt into useful information, such as the total amounts,
296+ * change amounts, and transaction outputs.
297+ */
298+ export function explainPsbt < TNumber extends number | bigint , Tx extends bitgo . UtxoTransaction < bigint > > (
299+ psbt : bitgo . UtxoPsbt < Tx > ,
300+ params : {
301+ pubs ?: bitgo . RootWalletKeys | string [ ] ;
302+ } ,
303+ network : utxolib . Network ,
304+ { strict = false } : { strict ?: boolean } = { }
305+ ) : TransactionExplanation {
306+ const payGoVerificationInfo = getPayGoVerificationInfo ( psbt , network ) ;
301307 if ( payGoVerificationInfo ) {
302308 try {
303309 utxocore . paygo . verifyPayGoAddressProof (
@@ -313,14 +319,14 @@ export function explainPsbt<TNumber extends number | bigint, Tx extends bitgo.Ut
313319 }
314320 }
315321
316- const messages = getBip322MessageInfoAndVerify ( ) ;
317- const changeInfo = getChangeInfo ( ) ;
322+ const messages = getBip322MessageInfoAndVerify ( psbt , network ) ;
323+ const changeInfo = getChangeInfo ( psbt ) ;
318324 const tx = psbt . getUnsignedTx ( ) as bitgo . UtxoTransaction < TNumber > ;
319325 const common = explainCommon ( tx , { ...params , changeInfo } , network ) ;
320326 const inputSignaturesCount = getPsbtInputSignaturesCount ( psbt , params ) ;
321327
322328 // Set fee from subtracting inputs from outputs
323- const outputAmount = txOutputs . reduce ( ( cumulative , curr ) => cumulative + BigInt ( curr . value ) , BigInt ( 0 ) ) ;
329+ const outputAmount = psbt . txOutputs . reduce ( ( cumulative , curr ) => cumulative + BigInt ( curr . value ) , BigInt ( 0 ) ) ;
324330 const inputAmount = psbt . txInputs . reduce ( ( cumulative , txInput , i ) => {
325331 const data = psbt . data . inputs [ i ] ;
326332 if ( data . witnessUtxo ) {
0 commit comments