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,85 @@ 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+ allOutputs : Output [ ]
59+ ) : Output [ ] {
60+ // verify that each recipient from txParams has their own output
61+ const expectedOutputs = ( txParams . recipients ?? [ ] ) . flatMap ( ( output ) => {
62+ if ( output . address === undefined ) {
63+ if ( output . amount . toString ( ) !== '0' ) {
64+ throw new Error ( `Only zero amounts allowed for non-encodeable scriptPubkeys: ${ output } ` ) ;
65+ }
66+ return [ output ] ;
67+ }
68+ return [ { ...output , address : coin . canonicalAddress ( output . address ) } ] ;
69+ } ) ;
70+ if ( txParams . allowExternalChangeAddress && txParams . changeAddress ) {
71+ // when an external change address is explicitly specified, count all outputs going towards that
72+ // address in the expected outputs (regardless of the output amount)
73+ expectedOutputs . push (
74+ ...allOutputs . flatMap ( ( output ) => {
75+ if (
76+ output . address === undefined ||
77+ output . address !== coin . canonicalAddress ( txParams . changeAddress as string )
78+ ) {
79+ return [ ] ;
80+ }
81+ return [ { ...output , address : coin . canonicalAddress ( output . address ) } ] ;
82+ } )
83+ ) ;
84+ }
85+ return expectedOutputs ;
86+ }
87+
2088export async function parseTransaction < TNumber extends bigint | number > (
2189 coin : AbstractUtxoCoin ,
2290 params : ParseTransactionOptions < TNumber >
2391) : Promise < ParsedTransaction < TNumber > > {
2492 const { txParams, txPrebuild, wallet, verification = { } , reqId } = params ;
2593
94+ // Branch off early for RBF transactions
95+ if ( txParams . rbfTxIds ) {
96+ return parseRbfTransaction ( coin , params ) ;
97+ }
98+
2699 if ( ! _ . isUndefined ( verification . disableNetworking ) && ! _ . isBoolean ( verification . disableNetworking ) ) {
27100 throw new Error ( 'verification.disableNetworking must be a boolean' ) ;
28101 }
@@ -56,47 +129,7 @@ export async function parseTransaction<TNumber extends bigint | number>(
56129
57130 const allOutputs = [ ...explanation . outputs , ...explanation . changeOutputs ] ;
58131
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- }
132+ const expectedOutputs = toExpectedOutputs ( coin , txParams , allOutputs ) ;
100133
101134 // get the keychains from the custom change wallet if needed
102135 let customChange : CustomChangeOptions | undefined ;
0 commit comments