@@ -9,13 +9,17 @@ import type { Bip322Message } from '../../abstractUtxoCoin';
99import type { Output , FixedScriptWalletOutput } from '../types' ;
1010import { toExtendedAddressFormat } from '../recipient' ;
1111import { getPayGoVerificationPubkey } from '../getPayGoVerificationPubkey' ;
12+ import { toBip32Triple } from '../../keychains' ;
1213
1314// ===== Transaction Explanation Type Definitions =====
1415
15- export interface AbstractUtxoTransactionExplanation < TFee = string > extends BaseTransactionExplanation < TFee , string > {
16+ export interface AbstractUtxoTransactionExplanation < TFee = string , TChangeOutput extends Output = Output >
17+ extends BaseTransactionExplanation < TFee , string > {
1618 /** NOTE: this actually only captures external outputs */
1719 outputs : Output [ ] ;
18- changeOutputs : Output [ ] ;
20+ changeOutputs : TChangeOutput [ ] ;
21+ customChangeOutputs ?: TChangeOutput [ ] ;
22+ customChangeAmount ?: string ;
1923
2024 /**
2125 * BIP322 messages extracted from the transaction inputs.
@@ -25,7 +29,8 @@ export interface AbstractUtxoTransactionExplanation<TFee = string> extends BaseT
2529}
2630
2731/** @deprecated - the signature fields are not very useful */
28- interface TransactionExplanationWithSignatures < TFee = string > extends AbstractUtxoTransactionExplanation < TFee > {
32+ interface TransactionExplanationWithSignatures < TFee = string , TChangeOutput extends Output = Output >
33+ extends AbstractUtxoTransactionExplanation < TFee , TChangeOutput > {
2934 /** @deprecated - unused outside of tests */
3035 locktime ?: number ;
3136
@@ -43,14 +48,16 @@ interface TransactionExplanationWithSignatures<TFee = string> extends AbstractUt
4348}
4449
4550/** For our wasm backend, we do not return the deprecated fields. We set TFee to string for backwards compatibility. */
46- export type TransactionExplanationWasm = AbstractUtxoTransactionExplanation < string > ;
51+ export type TransactionExplanationWasm = AbstractUtxoTransactionExplanation < string , FixedScriptWalletOutput > ;
4752
4853/** When parsing the legacy transaction format, we cannot always infer the fee so we set it to string | undefined */
4954export type TransactionExplanationUtxolibLegacy = TransactionExplanationWithSignatures < string | undefined > ;
5055
5156/** When parsing a PSBT, we can infer the fee so we set TFee to string. */
5257export type TransactionExplanationUtxolibPsbt = TransactionExplanationWithSignatures < string > ;
5358
59+ export type TransactionExplanationDescriptor = TransactionExplanationWithSignatures < string , Output > ;
60+
5461export type TransactionExplanation =
5562 | TransactionExplanationUtxolibLegacy
5663 | TransactionExplanationUtxolibPsbt
@@ -62,49 +69,67 @@ export type ChangeAddressInfo = {
6269 index : number ;
6370} ;
6471
72+ function toChangeOutput (
73+ txOutput : utxolib . TxOutput < number | bigint > ,
74+ network : utxolib . Network ,
75+ changeInfo : ChangeAddressInfo [ ] | undefined
76+ ) : FixedScriptWalletOutput | undefined {
77+ if ( ! changeInfo ) {
78+ return undefined ;
79+ }
80+ const address = toExtendedAddressFormat ( txOutput . script , network ) ;
81+ const change = changeInfo . find ( ( change ) => change . address === address ) ;
82+ if ( ! change ) {
83+ return undefined ;
84+ }
85+ return {
86+ address,
87+ amount : txOutput . value . toString ( ) ,
88+ chain : change . chain ,
89+ index : change . index ,
90+ external : false ,
91+ } ;
92+ }
93+
94+ function outputSum ( outputs : { amount : string | number } [ ] ) : bigint {
95+ return outputs . reduce ( ( sum , output ) => sum + BigInt ( output . amount ) , BigInt ( 0 ) ) ;
96+ }
97+
6598function explainCommon < TNumber extends number | bigint > (
6699 tx : bitgo . UtxoTransaction < TNumber > ,
67100 params : {
68101 changeInfo ?: ChangeAddressInfo [ ] ;
102+ customChangeInfo ?: ChangeAddressInfo [ ] ;
69103 feeInfo ?: string ;
70104 } ,
71105 network : utxolib . Network
72106) {
73107 const displayOrder = [ 'id' , 'outputAmount' , 'changeAmount' , 'outputs' , 'changeOutputs' ] ;
74- let spendAmount = BigInt ( 0 ) ;
75- let changeAmount = BigInt ( 0 ) ;
76108 const changeOutputs : FixedScriptWalletOutput [ ] = [ ] ;
77- const outputs : Output [ ] = [ ] ;
109+ const customChangeOutputs : FixedScriptWalletOutput [ ] = [ ] ;
110+ const externalOutputs : Output [ ] = [ ] ;
78111
79- const { changeInfo } = params ;
80- const changeAddresses = changeInfo ?. map ( ( info ) => info . address ) ?? [ ] ;
112+ const { changeInfo, customChangeInfo } = params ;
81113
82114 tx . outs . forEach ( ( currentOutput ) => {
83115 // Try to encode the script pubkey with an address. If it fails, try to parse it as an OP_RETURN output with the prefix.
84116 // If that fails, then it is an unrecognized scriptPubkey and should fail
85117 const currentAddress = toExtendedAddressFormat ( currentOutput . script , network ) ;
86118 const currentAmount = BigInt ( currentOutput . value ) ;
87119
88- if ( changeAddresses . includes ( currentAddress ) ) {
89- // this is change
90- changeAmount += currentAmount ;
91- const change = changeInfo ?. find ( ( change ) => change . address === currentAddress ) ;
120+ const changeOutput = toChangeOutput ( currentOutput , network , changeInfo ) ;
121+ if ( changeOutput ) {
122+ changeOutputs . push ( changeOutput ) ;
123+ return ;
124+ }
92125
93- if ( ! change ) {
94- throw new Error ( 'changeInfo must have change information for all change outputs' ) ;
95- }
96- changeOutputs . push ( {
97- address : currentAddress ,
98- amount : currentAmount . toString ( ) ,
99- chain : change . chain ,
100- index : change . index ,
101- external : false ,
102- } ) ;
126+ const customChangeOutput = toChangeOutput ( currentOutput , network , customChangeInfo ) ;
127+ if ( customChangeOutput ) {
128+ customChangeOutputs . push ( customChangeOutput ) ;
103129 return ;
104130 }
105131
106- spendAmount += currentAmount ;
107- outputs . push ( {
132+ externalOutputs . push ( {
108133 address : currentAddress ,
109134 amount : currentAmount . toString ( ) ,
110135 // If changeInfo has a length greater than or equal to zero, it means that the change information
@@ -117,10 +142,14 @@ function explainCommon<TNumber extends number | bigint>(
117142 } ) ;
118143
119144 const outputDetails = {
120- outputAmount : spendAmount . toString ( ) ,
121- changeAmount : changeAmount . toString ( ) ,
122- outputs ,
145+ outputs : externalOutputs ,
146+ outputAmount : outputSum ( externalOutputs ) . toString ( ) ,
147+
123148 changeOutputs,
149+ changeAmount : outputSum ( changeOutputs ) . toString ( ) ,
150+
151+ customChangeAmount : outputSum ( customChangeOutputs ) . toString ( ) ,
152+ customChangeOutputs,
124153 } ;
125154
126155 let fee : string | undefined ;
@@ -215,25 +244,27 @@ function getChainAndIndexFromBip32Derivations(output: bitgo.PsbtOutput) {
215244 return utxolib . bitgo . getChainAndIndexFromPath ( paths [ 0 ] ) ;
216245}
217246
218- function getChangeInfo ( psbt : bitgo . UtxoPsbt ) : ChangeAddressInfo [ ] | undefined {
247+ function getChangeInfo ( psbt : bitgo . UtxoPsbt , walletKeys ?: Triple < BIP32Interface > ) : ChangeAddressInfo [ ] | undefined {
219248 try {
220- return utxolib . bitgo . findInternalOutputIndices ( psbt ) . map ( ( i ) => {
221- const derivationInformation = getChainAndIndexFromBip32Derivations ( psbt . data . outputs [ i ] ) ;
222- if ( ! derivationInformation ) {
223- throw new Error ( 'could not find derivation information on bip32Derivation or tapBip32Derivation' ) ;
224- }
225- return {
226- address : utxolib . address . fromOutputScript ( psbt . txOutputs [ i ] . script , psbt . network ) ,
227- external : false ,
228- ...derivationInformation ,
229- } ;
230- } ) ;
249+ walletKeys = walletKeys ?? utxolib . bitgo . getSortedRootNodes ( psbt ) ;
231250 } catch ( e ) {
232251 if ( e instanceof utxolib . bitgo . ErrorNoMultiSigInputFound ) {
233252 return undefined ;
234253 }
235254 throw e ;
236255 }
256+
257+ return utxolib . bitgo . findWalletOutputIndices ( psbt , walletKeys ) . map ( ( i ) => {
258+ const derivationInformation = getChainAndIndexFromBip32Derivations ( psbt . data . outputs [ i ] ) ;
259+ if ( ! derivationInformation ) {
260+ throw new Error ( 'could not find derivation information on bip32Derivation or tapBip32Derivation' ) ;
261+ }
262+ return {
263+ address : utxolib . address . fromOutputScript ( psbt . txOutputs [ i ] . script , psbt . network ) ,
264+ external : false ,
265+ ...derivationInformation ,
266+ } ;
267+ } ) ;
237268}
238269
239270/**
@@ -351,6 +382,7 @@ export function explainPsbt(
351382 psbt : bitgo . UtxoPsbt ,
352383 params : {
353384 pubs ?: bitgo . RootWalletKeys | string [ ] ;
385+ customChangePubs ?: bitgo . RootWalletKeys | string [ ] ;
354386 } ,
355387 network : utxolib . Network ,
356388 { strict = false } : { strict ?: boolean } = { }
@@ -373,8 +405,11 @@ export function explainPsbt(
373405
374406 const messages = getBip322MessageInfoAndVerify ( psbt , network ) ;
375407 const changeInfo = getChangeInfo ( psbt ) ;
408+ const customChangeInfo = params . customChangePubs
409+ ? getChangeInfo ( psbt , toBip32Triple ( params . customChangePubs ) )
410+ : undefined ;
376411 const tx = psbt . getUnsignedTx ( ) ;
377- const common = explainCommon ( tx , { ...params , changeInfo } , network ) ;
412+ const common = explainCommon ( tx , { ...params , changeInfo, customChangeInfo } , network ) ;
378413 const inputSignaturesCount = getPsbtInputSignaturesCount ( psbt , params ) ;
379414
380415 // Set fee from subtracting inputs from outputs
0 commit comments