1+ import assert from 'assert' ;
2+
13import * as utxolib from '@bitgo/utxo-lib' ;
4+ import { BIP32Interface , bip32 } from '@bitgo/secp256k1' ;
5+ import { bitgo } from '@bitgo/utxo-lib' ;
6+ import { isTriple , Triple } from '@bitgo/sdk-core' ;
27import debugLib from 'debug' ;
38
49import { getReplayProtectionAddresses } from './replayProtection' ;
10+ import { InputSigningError , TransactionSigningError } from './SigningError' ;
511
612const debug = debugLib ( 'bitgo:v2:utxo' ) ;
713
@@ -11,132 +17,6 @@ type Unspent<TNumber extends number | bigint = number> = utxolib.bitgo.Unspent<T
1117
1218type RootWalletKeys = utxolib . bitgo . RootWalletKeys ;
1319
14- type PsbtParsedScriptType =
15- | 'p2sh'
16- | 'p2wsh'
17- | 'p2shP2wsh'
18- | 'p2shP2pk'
19- | 'taprootKeyPathSpend'
20- | 'taprootScriptPathSpend' ;
21-
22- export class InputSigningError < TNumber extends number | bigint = number > extends Error {
23- static expectedWalletUnspent < TNumber extends number | bigint > (
24- inputIndex : number ,
25- inputType : PsbtParsedScriptType | null , // null for legacy transaction format
26- unspent : Unspent < TNumber > | { id : string }
27- ) : InputSigningError < TNumber > {
28- return new InputSigningError (
29- inputIndex ,
30- inputType ,
31- unspent ,
32- `not a wallet unspent, not a replay protection unspent`
33- ) ;
34- }
35-
36- constructor (
37- public inputIndex : number ,
38- public inputType : PsbtParsedScriptType | null , // null for legacy transaction format
39- public unspent : Unspent < TNumber > | { id : string } ,
40- public reason : Error | string
41- ) {
42- super ( `signing error at input ${ inputIndex } : type=${ inputType } unspentId=${ unspent . id } : ${ reason } ` ) ;
43- }
44- }
45-
46- export class TransactionSigningError < TNumber extends number | bigint = number > extends Error {
47- constructor ( signErrors : InputSigningError < TNumber > [ ] , verifyError : InputSigningError < TNumber > [ ] ) {
48- super (
49- `sign errors at inputs: [${ signErrors . join ( ',' ) } ], ` +
50- `verify errors at inputs: [${ verifyError . join ( ',' ) } ], see log for details`
51- ) ;
52- }
53- }
54-
55- /**
56- * Sign all inputs of a psbt and verify signatures after signing.
57- * Collects and logs signing errors and verification errors, throws error in the end if any of them
58- * failed.
59- *
60- * If it is the last signature, finalize and extract the transaction from the psbt.
61- *
62- * This function mirrors signAndVerifyWalletTransaction, but is used for signing PSBTs instead of
63- * using TransactionBuilder
64- *
65- * @param psbt
66- * @param signerKeychain
67- * @param isLastSignature
68- */
69- export function signAndVerifyPsbt (
70- psbt : utxolib . bitgo . UtxoPsbt ,
71- signerKeychain : utxolib . BIP32Interface ,
72- {
73- isLastSignature,
74- /** deprecated */
75- allowNonSegwitSigningWithoutPrevTx,
76- } : { isLastSignature : boolean ; allowNonSegwitSigningWithoutPrevTx ?: boolean }
77- ) : utxolib . bitgo . UtxoPsbt | utxolib . bitgo . UtxoTransaction < bigint > {
78- const txInputs = psbt . txInputs ;
79- const outputIds : string [ ] = [ ] ;
80- const scriptTypes : PsbtParsedScriptType [ ] = [ ] ;
81-
82- const signErrors : InputSigningError < bigint > [ ] = psbt . data . inputs
83- . map ( ( input , inputIndex : number ) => {
84- const outputId = utxolib . bitgo . formatOutputId ( utxolib . bitgo . getOutputIdForInput ( txInputs [ inputIndex ] ) ) ;
85- outputIds . push ( outputId ) ;
86-
87- const { scriptType } = utxolib . bitgo . parsePsbtInput ( input ) ;
88- scriptTypes . push ( scriptType ) ;
89-
90- if ( scriptType === 'p2shP2pk' ) {
91- debug ( 'Skipping signature for input %d of %d (RP input?)' , inputIndex + 1 , psbt . data . inputs . length ) ;
92- return ;
93- }
94-
95- try {
96- psbt . signInputHD ( inputIndex , signerKeychain ) ;
97- debug ( 'Successfully signed input %d of %d' , inputIndex + 1 , psbt . data . inputs . length ) ;
98- } catch ( e ) {
99- return new InputSigningError < bigint > ( inputIndex , scriptType , { id : outputId } , e ) ;
100- }
101- } )
102- . filter ( ( e ) : e is InputSigningError < bigint > => e !== undefined ) ;
103-
104- const verifyErrors : InputSigningError < bigint > [ ] = psbt . data . inputs
105- . map ( ( input , inputIndex ) => {
106- const scriptType = scriptTypes [ inputIndex ] ;
107- if ( scriptType === 'p2shP2pk' ) {
108- debug (
109- 'Skipping input signature %d of %d (unspent from replay protection address which is platform signed only)' ,
110- inputIndex + 1 ,
111- psbt . data . inputs . length
112- ) ;
113- return ;
114- }
115-
116- const outputId = outputIds [ inputIndex ] ;
117- try {
118- if ( ! psbt . validateSignaturesOfInputHD ( inputIndex , signerKeychain ) ) {
119- return new InputSigningError ( inputIndex , scriptType , { id : outputId } , new Error ( `invalid signature` ) ) ;
120- }
121- } catch ( e ) {
122- debug ( 'Invalid signature' ) ;
123- return new InputSigningError < bigint > ( inputIndex , scriptType , { id : outputId } , e ) ;
124- }
125- } )
126- . filter ( ( e ) : e is InputSigningError < bigint > => e !== undefined ) ;
127-
128- if ( signErrors . length || verifyErrors . length ) {
129- throw new TransactionSigningError ( signErrors , verifyErrors ) ;
130- }
131-
132- if ( isLastSignature ) {
133- psbt . finalizeAllInputs ( ) ;
134- return psbt . extractTransaction ( ) ;
135- }
136-
137- return psbt ;
138- }
139-
14020/**
14121 * Sign all inputs of a wallet transaction and verify signatures after signing.
14222 * Collects and logs signing errors and verification errors, throws error in the end if any of them
@@ -232,3 +112,43 @@ export function signAndVerifyWalletTransaction<TNumber extends number | bigint>(
232112
233113 return signedTransaction ;
234114}
115+
116+ export function signLegacyTransaction < TNumber extends number | bigint > (
117+ tx : utxolib . bitgo . UtxoTransaction < TNumber > ,
118+ signerKeychain : BIP32Interface | undefined ,
119+ params : {
120+ isLastSignature : boolean ;
121+ signingStep : 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined ;
122+ txInfo : { unspents ?: utxolib . bitgo . Unspent < TNumber > [ ] } | undefined ;
123+ pubs : string [ ] | undefined ;
124+ cosignerPub : string | undefined ;
125+ }
126+ ) : utxolib . bitgo . UtxoTransaction < TNumber > {
127+ switch ( params . signingStep ) {
128+ case 'signerNonce' :
129+ case 'cosignerNonce' :
130+ /**
131+ * In certain cases, the caller of this method may not know whether the txHex contains a psbt with taproot key path spend input(s).
132+ * Instead of throwing error, no-op and return the txHex. So that the caller can call this method in the same sequence.
133+ */
134+ return tx ;
135+ }
136+
137+ if ( tx . ins . length !== params . txInfo ?. unspents ?. length ) {
138+ throw new Error ( 'length of unspents array should equal to the number of transaction inputs' ) ;
139+ }
140+
141+ if ( ! params . pubs || ! isTriple ( params . pubs ) ) {
142+ throw new Error ( `must provide xpub array` ) ;
143+ }
144+
145+ const keychains = params . pubs . map ( ( pub ) => bip32 . fromBase58 ( pub ) ) as Triple < BIP32Interface > ;
146+ const cosignerPub = params . cosignerPub ?? params . pubs [ 2 ] ;
147+ const cosignerKeychain = bip32 . fromBase58 ( cosignerPub ) ;
148+
149+ assert ( signerKeychain ) ;
150+ const walletSigner = new bitgo . WalletUnspentSigner < RootWalletKeys > ( keychains , signerKeychain , cosignerKeychain ) ;
151+ return signAndVerifyWalletTransaction ( tx , params . txInfo . unspents , walletSigner , {
152+ isLastSignature : params . isLastSignature ,
153+ } ) as utxolib . bitgo . UtxoTransaction < TNumber > ;
154+ }
0 commit comments