@@ -3,6 +3,7 @@ import { ok as assert } from 'assert';
33
44import {
55 createOutputScriptP2shP2pk ,
6+ isSupportedScriptType ,
67 ScriptType ,
78 ScriptType2Of3 ,
89 scriptTypeP2shP2pk ,
@@ -26,10 +27,12 @@ import {
2627 UtxoTransaction ,
2728 verifySignatureWithUnspent ,
2829 addXpubsToPsbt ,
30+ clonePsbtWithoutNonWitnessUtxo ,
2931} from '../bitgo' ;
3032import { Network } from '../networks' ;
3133import { mockReplayProtectionUnspent , mockWalletUnspent } from './mock' ;
3234import { toOutputScript } from '../address' ;
35+ import { getDefaultWalletKeys , getWalletKeysForSeed } from './keys' ;
3336
3437/**
3538 * This is a bit of a misnomer, as it actually specifies the spend type of the input.
@@ -48,6 +51,8 @@ export type Input = {
4851 value : bigint ;
4952} ;
5053
54+ export type SignStage = 'unsigned' | 'halfsigned' | 'fullsigned' ;
55+
5156/**
5257 * Set isInternalAddress=true for internal output address
5358 */
@@ -169,7 +174,7 @@ export function constructPsbt(
169174 outputs : Output [ ] ,
170175 network : Network ,
171176 rootWalletKeys : RootWalletKeys ,
172- sign : 'unsigned' | 'halfsigned' | 'fullsigned' ,
177+ signStage : SignStage ,
173178 params ?: {
174179 signers ?: { signerName : KeyName ; cosignerName ?: KeyName } ;
175180 deterministic ?: boolean ;
@@ -183,6 +188,11 @@ export function constructPsbt(
183188 assert ( totalInputAmount >= outputInputAmount , 'total output can not exceed total input' ) ;
184189
185190 const psbt = createPsbtForNetwork ( { network } ) ;
191+
192+ if ( params ?. addGlobalXPubs ) {
193+ addXpubsToPsbt ( psbt , rootWalletKeys ) ;
194+ }
195+
186196 const unspents = inputs . map ( ( input , i ) => toUnspent ( input , i , network , rootWalletKeys ) ) ;
187197
188198 unspents . forEach ( ( u , i ) => {
@@ -226,7 +236,7 @@ export function constructPsbt(
226236 throw new Error ( 'invalid output' ) ;
227237 } ) ;
228238
229- if ( sign === 'unsigned' ) {
239+ if ( signStage === 'unsigned' ) {
230240 return psbt ;
231241 }
232242
@@ -237,15 +247,90 @@ export function constructPsbt(
237247
238248 signAllPsbtInputs ( psbt , inputs , rootWalletKeys , 'halfsigned' , { signers, skipNonWitnessUtxo } ) ;
239249
240- if ( sign === 'fullsigned' ) {
241- signAllPsbtInputs ( psbt , inputs , rootWalletKeys , sign , { signers, deterministic, skipNonWitnessUtxo } ) ;
250+ if ( signStage === 'fullsigned' ) {
251+ signAllPsbtInputs ( psbt , inputs , rootWalletKeys , signStage , { signers, deterministic, skipNonWitnessUtxo } ) ;
242252 }
243253
244- if ( params ?. addGlobalXPubs ) {
245- addXpubsToPsbt ( psbt , rootWalletKeys ) ;
254+ return psbt ;
255+ }
256+
257+ export type TxFormat = 'psbt' | 'psbt-lite' ;
258+
259+ /**
260+ * Creates a valid PSBT with as many features as possible.
261+ *
262+ * - Inputs:
263+ * - All wallet script types that are supported by the network.
264+ * - A p2shP2pk input (for replay protection)
265+ * - Outputs:
266+ * - All wallet script types that are supported by the network.
267+ * - A p2sh output with derivation info of a different wallet (not in the global psbt xpubs)
268+ * - A p2sh output with no derivation info (external output)
269+ * - An OP_RETURN output
270+ */
271+ export class AcidTest {
272+ public readonly network : Network ;
273+ public readonly signStage : SignStage ;
274+ public readonly txFormat : TxFormat ;
275+ public readonly rootWalletKeys : RootWalletKeys ;
276+ public readonly otherWalletKeys : RootWalletKeys ;
277+ public readonly inputs : Input [ ] ;
278+ public readonly outputs : Output [ ] ;
279+
280+ constructor (
281+ network : Network ,
282+ signStage : SignStage ,
283+ txFormat : TxFormat ,
284+ rootWalletKeys : RootWalletKeys ,
285+ otherWalletKeys : RootWalletKeys ,
286+ inputs : Input [ ] ,
287+ outputs : Output [ ]
288+ ) {
289+ this . network = network ;
290+ this . signStage = signStage ;
291+ this . txFormat = txFormat ;
292+ this . rootWalletKeys = rootWalletKeys ;
293+ this . otherWalletKeys = otherWalletKeys ;
294+ this . inputs = inputs ;
295+ this . outputs = outputs ;
246296 }
247297
248- return psbt ;
298+ static withDefaults ( network : Network , signStage : SignStage , txFormat : TxFormat ) : AcidTest {
299+ const rootWalletKeys = getDefaultWalletKeys ( ) ;
300+
301+ const otherWalletKeys = getWalletKeysForSeed ( 'too many secrets' ) ;
302+ const inputs : Input [ ] = inputScriptTypes
303+ . filter ( ( scriptType ) =>
304+ scriptType === 'taprootKeyPathSpend'
305+ ? isSupportedScriptType ( network , 'p2trMusig2' )
306+ : isSupportedScriptType ( network , scriptType )
307+ )
308+ . map ( ( scriptType ) => ( { scriptType, value : BigInt ( 2000 ) } ) ) ;
309+
310+ const outputs : Output [ ] = outputScriptTypes
311+ . filter ( ( scriptType ) => isSupportedScriptType ( network , scriptType ) )
312+ . map ( ( scriptType ) => ( { scriptType, value : BigInt ( 900 ) } ) ) ;
313+
314+ // Test other wallet output (with derivation info)
315+ outputs . push ( { scriptType : 'p2sh' , value : BigInt ( 900 ) , walletKeys : otherWalletKeys } ) ;
316+ // Tes non-wallet output
317+ outputs . push ( { scriptType : 'p2sh' , value : BigInt ( 900 ) , walletKeys : null } ) ;
318+ // Test OP_RETURN output
319+ outputs . push ( { opReturn : 'setec astronomy' , value : BigInt ( 900 ) } ) ;
320+
321+ return new AcidTest ( network , signStage , txFormat , rootWalletKeys , otherWalletKeys , inputs , outputs ) ;
322+ }
323+
324+ createPsbt ( ) : UtxoPsbt {
325+ const psbt = constructPsbt ( this . inputs , this . outputs , this . network , this . rootWalletKeys , this . signStage , {
326+ deterministic : true ,
327+ addGlobalXPubs : true ,
328+ } ) ;
329+ if ( this . txFormat === 'psbt-lite' ) {
330+ return clonePsbtWithoutNonWitnessUtxo ( psbt ) ;
331+ }
332+ return psbt ;
333+ }
249334}
250335
251336/**
0 commit comments