Skip to content

Commit 195c1eb

Browse files
OttoAllmendingerllm-git
andcommitted
fix(abstract-utxo): refactor parseTransaction to improve organization
Extracted RBF parsing and output conversion logic into separate functions for better code organization and readability. This improves maintainability and makes the transaction parsing flow clearer. Issue: BTC-2732 Co-authored-by: llm-git <[email protected]>
1 parent 975b73e commit 195c1eb

File tree

1 file changed

+75
-42
lines changed

1 file changed

+75
-42
lines changed

modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts

Lines changed: 75 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import assert from 'assert';
22

33
import _ from 'lodash';
4-
import { Triple, VerificationOptions, Wallet } from '@bitgo/sdk-core';
4+
import { ITransactionRecipient, Triple, VerificationOptions, Wallet } from '@bitgo/sdk-core';
55
import * as utxolib from '@bitgo/utxo-lib';
66

77
import 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+
2088
export 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

Comments
 (0)