@@ -21,6 +21,46 @@ import { common, getAddressP2PKH, getNetwork, sanitizeLegacyPath } from '@bitgo/
2121import { verifyAddress } from './verifyAddress' ;
2222import { tryPromise } from '../util' ;
2323
24+ type Triple < T > = [ T , T , T ] ;
25+
26+ interface V1Keychain {
27+ xpub : string ;
28+ path ?: string ;
29+ walletSubPath ?: string ;
30+ }
31+
32+ /**
33+ * Parse chainPath like "/0/13" into { chain: 0, index: 13 }
34+ */
35+ function parseChainPath ( chainPath : string ) : { chain : number ; index : number } {
36+ const parts = chainPath . split ( '/' ) . filter ( ( p ) => p . length > 0 ) ;
37+ if ( parts . length !== 2 ) {
38+ throw new Error ( `Invalid chainPath: ${ chainPath } ` ) ;
39+ }
40+ return { chain : parseInt ( parts [ 0 ] , 10 ) , index : parseInt ( parts [ 1 ] , 10 ) } ;
41+ }
42+
43+ /**
44+ * Create RootWalletKeys from v1 wallet keychains.
45+ * v1 keychains have a structure like { xpub, path: 'm', walletSubPath: '/0/0' }
46+ */
47+ function createRootWalletKeysFromV1Keychains ( keychains : V1Keychain [ ] ) : utxolib . bitgo . RootWalletKeys {
48+ if ( keychains . length !== 3 ) {
49+ throw new Error ( 'Expected 3 keychains for v1 wallet' ) ;
50+ }
51+
52+ const bip32Keys = keychains . map ( ( k ) => bip32 . fromBase58 ( k . xpub ) ) as Triple < utxolib . BIP32Interface > ;
53+
54+ // v1 wallets typically have walletSubPath like '/0/0' which we convert to derivation prefixes like '0/0'
55+ const derivationPrefixes = keychains . map ( ( k ) => {
56+ const walletSubPath = k . walletSubPath || '/0/0' ;
57+ // Remove leading slash if present
58+ return walletSubPath . startsWith ( '/' ) ? walletSubPath . slice ( 1 ) : walletSubPath ;
59+ } ) as Triple < string > ;
60+
61+ return new utxolib . bitgo . RootWalletKeys ( bip32Keys , derivationPrefixes ) ;
62+ }
63+
2464interface BaseOutput {
2565 amount : number ;
2666 travelInfo ?: any ;
@@ -235,6 +275,9 @@ exports.createTransaction = function (params) {
235275
236276 let changeOutputs : Output [ ] = [ ] ;
237277
278+ // All outputs for the transaction (recipients, OP_RETURNs, change, fees)
279+ let outputs : Output [ ] = [ ] ;
280+
238281 let containsUncompressedPublicKeys = false ;
239282
240283 // The transaction.
@@ -603,7 +646,8 @@ exports.createTransaction = function (params) {
603646 throw new Error ( 'transaction too large: estimated size ' + minerFeeInfo . size + ' bytes' ) ;
604647 }
605648
606- const outputs : Output [ ] = [ ] ;
649+ // Reset outputs array (use outer scope variable)
650+ outputs = [ ] ;
607651
608652 recipients . forEach ( function ( recipient ) {
609653 let script ;
@@ -739,8 +783,75 @@ exports.createTransaction = function (params) {
739783 } ) ;
740784 } ;
741785
786+ // Build PSBT with all signing metadata embedded
787+ const buildPsbt = function ( ) : utxolib . bitgo . UtxoPsbt {
788+ const psbt = utxolib . bitgo . createPsbtForNetwork ( { network } ) ;
789+
790+ // Need wallet keychains for PSBT metadata
791+ const walletKeychains = params . wallet . keychains ;
792+ if ( ! walletKeychains || walletKeychains . length !== 3 ) {
793+ throw new Error ( 'Wallet keychains required for PSBT format' ) ;
794+ }
795+
796+ const rootWalletKeys = createRootWalletKeysFromV1Keychains ( walletKeychains ) ;
797+ utxolib . bitgo . addXpubsToPsbt ( psbt , rootWalletKeys ) ;
798+
799+ // Add multisig inputs with PSBT metadata
800+ for ( const unspent of unspents ) {
801+ const { chain, index } = parseChainPath ( unspent . chainPath ) as { chain : utxolib . bitgo . ChainCode ; index : number } ;
802+
803+ const walletUnspent : utxolib . bitgo . WalletUnspent < bigint > = {
804+ id : `${ unspent . tx_hash } :${ unspent . tx_output_n } ` ,
805+ address : unspent . address ,
806+ chain,
807+ index,
808+ value : BigInt ( unspent . value ) ,
809+ } ;
810+
811+ utxolib . bitgo . addWalletUnspentToPsbt ( psbt , walletUnspent , rootWalletKeys , 'user' , 'backup' , {
812+ skipNonWitnessUtxo : true ,
813+ } ) ;
814+ }
815+
816+ // Fee single key inputs are not supported with PSBT yet - throw to trigger fallback to legacy
817+ if ( feeSingleKeyUnspentsUsed . length > 0 ) {
818+ throw new Error ( 'PSBT does not support feeSingleKey inputs - use legacy transaction format' ) ;
819+ }
820+
821+ // Add outputs (recipients, change, fees, OP_RETURNs) - already calculated in outputs array
822+ for ( const output of outputs ) {
823+ psbt . addOutput ( {
824+ script : ( output as ScriptOutput ) . script ,
825+ value : BigInt ( output . amount ) ,
826+ } ) ;
827+ }
828+
829+ return psbt ;
830+ } ;
831+
742832 // Serialize the transaction, returning what is needed to sign it
743833 const serialize = function ( ) {
834+ // Build and return PSBT format when usePsbt is explicitly true
835+ // PSBT hex is returned in transactionHex field for backward compatibility
836+ // Use utxolib.bitgo.isPsbt() to detect if transactionHex contains PSBT or legacy tx
837+ if ( params . usePsbt === true ) {
838+ const psbt = buildPsbt ( ) ;
839+ return {
840+ transactionHex : psbt . toHex ( ) ,
841+ fee : fee ,
842+ changeAddresses : changeOutputs . map ( function ( co ) {
843+ return _ . pick ( co , [ 'address' , 'path' , 'amount' ] ) ;
844+ } ) ,
845+ walletId : params . wallet . id ( ) ,
846+ feeRate : feeRate ,
847+ instant : params . instant ,
848+ bitgoFee : bitgoFeeInfo ,
849+ estimatedSize : minerFeeInfo . size ,
850+ travelInfos : travelInfos ,
851+ } ;
852+ }
853+
854+ // Legacy format: return transactionHex with separate unspents array
744855 // only need to return the unspents that were used and just the chainPath, redeemScript, and instant flag
745856 const pickedUnspents : any = _ . map ( unspents , function ( unspent ) {
746857 return _ . pick ( unspent , [ 'chainPath' , 'redeemScript' , 'instant' , 'witnessScript' , 'script' , 'value' ] ) ;
0 commit comments