11import assert from 'assert' ;
22
33import _ from 'lodash' ;
4- import { Triple , VerificationOptions , Wallet } from '@bitgo/sdk-core' ;
4+ import { ITransactionRecipient , Triple , VerificationOptions , Wallet } from '@bitgo/sdk-core' ;
55import * as utxolib from '@bitgo/utxo-lib' ;
66
77import type { AbstractUtxoCoin , ParseTransactionOptions } from '../../abstractUtxoCoin' ;
@@ -17,12 +17,74 @@ export type ComparableOutputWithExternal<TValue> = ComparableOutput<TValue> & {
1717 external : boolean | undefined ;
1818} ;
1919
20+ async function parseRbfTransaction < TNumber extends bigint | number > (
21+ coin : AbstractUtxoCoin ,
22+ params : ParseTransactionOptions < TNumber >
23+ ) : Promise < ParsedTransaction < TNumber > > {
24+ const { txParams, wallet } = params ;
25+
26+ assert ( txParams . rbfTxIds ) ;
27+ assert ( txParams . rbfTxIds . length === 1 ) ;
28+
29+ const txToBeReplaced = await wallet . getTransaction ( { txHash : txParams . rbfTxIds [ 0 ] , includeRbf : true } ) ;
30+ const recipients = txToBeReplaced . outputs . flatMap (
31+ ( output : { valueString : string ; address ?: string ; wallet ?: string } ) => {
32+ // For self-sends, the walletId will be the same as the wallet's id
33+ if ( output . wallet === wallet . id ( ) ) {
34+ return [ ] ;
35+ }
36+ return [ coin . toCanonicalTransactionRecipient ( output ) ] ;
37+ }
38+ ) ;
39+
40+ // Recurse into parseTransaction with the derived recipients and without rbfTxIds
41+ return parseTransaction ( coin , {
42+ ...params ,
43+ txParams : {
44+ ...txParams ,
45+ recipients,
46+ rbfTxIds : undefined ,
47+ } ,
48+ } ) ;
49+ }
50+
51+ function toExpectedOutputs (
52+ coin : AbstractUtxoCoin ,
53+ txParams : {
54+ recipients ?: ITransactionRecipient [ ] ;
55+ allowExternalChangeAddress ?: boolean ;
56+ changeAddress ?: string ;
57+ }
58+ ) : Output [ ] {
59+ // verify that each recipient from txParams has their own output
60+ const expectedOutputs = ( txParams . recipients ?? [ ] ) . flatMap ( ( output ) => {
61+ if ( output . address === undefined ) {
62+ if ( output . amount . toString ( ) !== '0' ) {
63+ throw new Error ( `Only zero amounts allowed for non-encodeable scriptPubkeys: ${ output } ` ) ;
64+ }
65+ return [ output ] ;
66+ }
67+ return [ { ...output , address : coin . canonicalAddress ( output . address ) } ] ;
68+ } ) ;
69+ if ( txParams . allowExternalChangeAddress && txParams . changeAddress ) {
70+ // when an external change address is explicitly specified, count all outputs going towards that
71+ // address in the expected outputs (regardless of the output amount)
72+ expectedOutputs . push ( { address : coin . canonicalAddress ( txParams . changeAddress ) , amount : 'max' } ) ;
73+ }
74+ return expectedOutputs ;
75+ }
76+
2077export async function parseTransaction < TNumber extends bigint | number > (
2178 coin : AbstractUtxoCoin ,
2279 params : ParseTransactionOptions < TNumber >
2380) : Promise < ParsedTransaction < TNumber > > {
2481 const { txParams, txPrebuild, wallet, verification = { } , reqId } = params ;
2582
83+ // Branch off early for RBF transactions
84+ if ( txParams . rbfTxIds ) {
85+ return parseRbfTransaction ( coin , params ) ;
86+ }
87+
2688 if ( ! _ . isUndefined ( verification . disableNetworking ) && ! _ . isBoolean ( verification . disableNetworking ) ) {
2789 throw new Error ( 'verification.disableNetworking must be a boolean' ) ;
2890 }
@@ -47,56 +109,7 @@ export async function parseTransaction<TNumber extends bigint | number>(
47109 throw new Error ( 'missing required txPrebuild property txHex' ) ;
48110 }
49111
50- // obtain all outputs
51- const explanation : TransactionExplanation = await coin . explainTransaction < TNumber > ( {
52- txHex : txPrebuild . txHex ,
53- txInfo : txPrebuild . txInfo ,
54- pubs : keychainArray . map ( ( k ) => k . pub ) as Triple < string > ,
55- } ) ;
56-
57- const allOutputs = [ ...explanation . outputs , ...explanation . changeOutputs ] ;
58-
59- let expectedOutputs ;
60- if ( txParams . rbfTxIds ) {
61- assert ( txParams . rbfTxIds . length === 1 ) ;
62-
63- const txToBeReplaced = await wallet . getTransaction ( { txHash : txParams . rbfTxIds [ 0 ] , includeRbf : true } ) ;
64- expectedOutputs = txToBeReplaced . outputs . flatMap (
65- ( output : { valueString : string ; address ?: string ; wallet ?: string } ) => {
66- // For self-sends, the walletId will be the same as the wallet's id
67- if ( output . wallet === wallet . id ( ) ) {
68- return [ ] ;
69- }
70- return [ coin . toCanonicalTransactionRecipient ( output ) ] ;
71- }
72- ) ;
73- } else {
74- // verify that each recipient from txParams has their own output
75- expectedOutputs = ( txParams . recipients ?? [ ] ) . flatMap ( ( output ) => {
76- if ( output . address === undefined ) {
77- if ( output . amount . toString ( ) !== '0' ) {
78- throw new Error ( `Only zero amounts allowed for non-encodeable scriptPubkeys: ${ output } ` ) ;
79- }
80- return [ output ] ;
81- }
82- return [ { ...output , address : coin . canonicalAddress ( output . address ) } ] ;
83- } ) ;
84- if ( txParams . allowExternalChangeAddress && txParams . changeAddress ) {
85- // when an external change address is explicitly specified, count all outputs going towards that
86- // address in the expected outputs (regardless of the output amount)
87- expectedOutputs . push (
88- ...allOutputs . flatMap ( ( output ) => {
89- if (
90- output . address === undefined ||
91- output . address !== coin . canonicalAddress ( txParams . changeAddress as string )
92- ) {
93- return [ ] ;
94- }
95- return [ { ...output , address : coin . canonicalAddress ( output . address ) } ] ;
96- } )
97- ) ;
98- }
99- }
112+ const expectedOutputs = toExpectedOutputs ( coin , txParams ) ;
100113
101114 // get the keychains from the custom change wallet if needed
102115 let customChange : CustomChangeOptions | undefined ;
@@ -126,6 +139,15 @@ export async function parseTransaction<TNumber extends bigint | number>(
126139 }
127140 }
128141
142+ // obtain all outputs
143+ const explanation : TransactionExplanation = await coin . explainTransaction < TNumber > ( {
144+ txHex : txPrebuild . txHex ,
145+ txInfo : txPrebuild . txInfo ,
146+ pubs : keychainArray . map ( ( k ) => k . pub ) as Triple < string > ,
147+ } ) ;
148+
149+ const allOutputs = [ ...explanation . outputs , ...explanation . changeOutputs ] ;
150+
129151 /**
130152 * Loop through all the outputs and classify each of them as either internal spends
131153 * or external spends by setting the "external" property to true or false on the output object.
0 commit comments