Skip to content

Commit 975b73e

Browse files
OttoAllmendingerllm-git
andcommitted
feat(abstract-utxo): add support for external change address
Added ability to set a recipient address as a change address for testing and verification purposes. This helps with scenarios where we need to test custom change addresses without applying size constraints. Issue: BTC-2732 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 9ace7ad commit 975b73e

1 file changed

Lines changed: 35 additions & 8 deletions

File tree

  • modules/abstract-utxo/test/unit/transaction/fixedScript

modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,39 @@ import type {
1515
} from '../../../../src/transaction/fixedScript/explainTransaction';
1616
import { getChainFromNetwork } from '../../../../src/names';
1717
import { TransactionPrebuild } from '../../../../src/abstractUtxoCoin';
18-
import { isScriptRecipient } from '../../../../src/transaction/recipient';
1918

20-
function getTxParamsFromExplanation(explanation: TransactionExplanation): {
19+
function getTxParamsFromExplanation(
20+
explanation: TransactionExplanation,
21+
{ externalCustomChangeAddress }: { externalCustomChangeAddress: boolean }
22+
): {
2123
recipients: ITransactionRecipient[];
2224
changeAddress?: string;
2325
} {
2426
// The external outputs are the ones that are in outputs but not in changeOutputs
2527
const changeAddresses = new Set(explanation.changeOutputs.map((o) => o.address));
26-
const externalOutputs = explanation.outputs.filter((o) => o.address && !changeAddresses.has(o.address));
28+
let externalOutputs = explanation.outputs.filter((o) => o.address && !changeAddresses.has(o.address));
29+
30+
let changeAddress: string | undefined;
31+
if (externalCustomChangeAddress) {
32+
// convert an external output to a change output
33+
//
34+
// in combination with allowExternalChangeAddress, this allows an external
35+
// output on the transaction without a size constraint
36+
const externalOutput = externalOutputs[0];
37+
if (!externalOutput) {
38+
throw new Error('no external output found');
39+
}
40+
changeAddress = externalOutput.address;
41+
externalOutputs = externalOutputs.slice(1);
42+
}
2743

2844
return {
2945
recipients: externalOutputs.map((output) => ({
3046
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
3147
address: output.address!,
3248
amount: output.amount,
3349
})),
34-
changeAddress: undefined,
50+
changeAddress,
3551
};
3652
}
3753

@@ -64,19 +80,20 @@ function describeParseTransactionWith(
6480
label: string,
6581
{
6682
txParams,
83+
externalCustomChangeAddress = false,
6784
expectedExplicitExternalSpendAmount,
6885
expectedImplicitExternalSpendAmount,
6986
txFormat = 'psbt',
7087
}: {
7188
txParams:
7289
| {
7390
recipients: ITransactionRecipient[];
74-
changeAddress?: string;
7591
}
7692
| {
7793
rbfTxIds: string[];
7894
}
7995
| 'inferFromExplanation';
96+
externalCustomChangeAddress?: boolean;
8097
expectedExplicitExternalSpendAmount: bigint;
8198
expectedImplicitExternalSpendAmount: bigint;
8299
txFormat?: 'psbt' | 'legacy';
@@ -114,7 +131,7 @@ function describeParseTransactionWith(
114131
// Determine txParams
115132
let resolvedTxParams;
116133
if (txParams === 'inferFromExplanation' || txParams === undefined) {
117-
resolvedTxParams = getTxParamsFromExplanation(explanation);
134+
resolvedTxParams = getTxParamsFromExplanation(explanation, { externalCustomChangeAddress });
118135
} else if ('rbfTxIds' in txParams) {
119136
// Replace placeholder txHash with actual computed txHash
120137
resolvedTxParams = {
@@ -124,6 +141,10 @@ function describeParseTransactionWith(
124141
resolvedTxParams = txParams;
125142
}
126143

144+
if (externalCustomChangeAddress) {
145+
resolvedTxParams.allowExternalChangeAddress = true;
146+
}
147+
127148
// Create mock wallet
128149
mockWallet = sinon.createStubInstance(Wallet);
129150
mockWallet.id.returns('test-wallet-id');
@@ -132,7 +153,7 @@ function describeParseTransactionWith(
132153

133154
// Mock getTransaction for RBF case
134155
if ('rbfTxIds' in resolvedTxParams) {
135-
const rbfTxParams = getTxParamsFromExplanation(explanation);
156+
const rbfTxParams = getTxParamsFromExplanation(explanation, { externalCustomChangeAddress: false });
136157
mockWallet.getTransaction.resolves({
137158
outputs: rbfTxParams.recipients.map((r) => ({
138159
valueString: typeof r.amount === 'string' ? r.amount : r.amount.toString(),
@@ -243,7 +264,6 @@ describe('parseTransaction', function () {
243264
describeParseTransactionWith(test, 'empty recipients', {
244265
txParams: {
245266
recipients: [],
246-
changeAddress: undefined,
247267
},
248268
expectedExplicitExternalSpendAmount: 0n,
249269
expectedImplicitExternalSpendAmount: 1800n,
@@ -256,5 +276,12 @@ describe('parseTransaction', function () {
256276
expectedExplicitExternalSpendAmount: 1800n,
257277
expectedImplicitExternalSpendAmount: 0n,
258278
});
279+
280+
describeParseTransactionWith(test, 'allowExternalChangeAddress', {
281+
txParams: 'inferFromExplanation',
282+
externalCustomChangeAddress: true,
283+
expectedExplicitExternalSpendAmount: 1800n,
284+
expectedImplicitExternalSpendAmount: 0n,
285+
});
259286
});
260287
});

0 commit comments

Comments
 (0)