Skip to content

Commit 710e60b

Browse files
OttoAllmendingerllm-git
andcommitted
feat(abstract-utxo): allow optional expected outputs
Add optional flag to ExpectedOutput type for outputs that can be excluded. Update output difference logic to filter non-optional outputs when determining missing outputs. Issue: BTC-2962 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 27c4e89 commit 710e60b

2 files changed

Lines changed: 67 additions & 3 deletions

File tree

modules/abstract-utxo/src/transaction/outputDifference.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ export type ComparableOutput<TValue> = {
77
export type ActualOutput = ComparableOutput<bigint>;
88

99
/** Expected outputs can have a fixed value or 'max'. */
10-
export type ExpectedOutput = ComparableOutput<bigint | 'max'>;
10+
export type ExpectedOutput = ComparableOutput<bigint | 'max'> & {
11+
/** When true, the output is not required to be present in the transaction. */
12+
optional?: boolean;
13+
};
1114

1215
/**
1316
* @param a
@@ -40,6 +43,13 @@ export function outputDifference<A extends ActualOutput | ExpectedOutput, B exte
4043
return first;
4144
}
4245

46+
export function getMissingOutputs<A extends ActualOutput, B extends ExpectedOutput>(
47+
actualOutputs: A[],
48+
expectedOutputs: B[]
49+
): B[] {
50+
return outputDifference(expectedOutputs, actualOutputs).filter((o) => !o.optional);
51+
}
52+
4353
export type OutputDifferenceWithExpected<TActual extends ActualOutput, TExpected extends ExpectedOutput> = {
4454
/** These are the external outputs that were expected and found in the transaction. */
4555
explicitOutputs: TActual[];
@@ -65,7 +75,7 @@ export function outputDifferencesWithExpected<TActual extends ActualOutput, TExp
6575
): OutputDifferenceWithExpected<TActual, TExpected> {
6676
const implicitOutputs = outputDifference(actualOutputs, expectedOutputs);
6777
const explicitOutputs = outputDifference(actualOutputs, implicitOutputs);
68-
const missingOutputs = outputDifference(expectedOutputs, actualOutputs);
78+
const missingOutputs = getMissingOutputs(actualOutputs, expectedOutputs);
6979
return {
7080
explicitOutputs,
7181
implicitOutputs,

modules/abstract-utxo/test/unit/transaction/outputDifference.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import assert from 'assert';
33
import {
44
ActualOutput,
55
ExpectedOutput,
6+
getMissingOutputs,
67
matchingOutput,
78
outputDifference,
89
outputDifferencesWithExpected,
@@ -11,17 +12,22 @@ import {
1112
describe('outputDifference', function () {
1213
function output(script: string, value: bigint | number): ActualOutput;
1314
function output(script: string, value: 'max'): ExpectedOutput;
14-
function output(script: string, value: bigint | number | 'max'): ActualOutput | ExpectedOutput {
15+
function output(script: string, value: bigint | number | 'max', optional?: boolean): ActualOutput | ExpectedOutput {
1516
const scriptBuffer = Buffer.from(script, 'hex');
1617
if (scriptBuffer.toString('hex') !== script) {
1718
throw new Error('invalid script');
1819
}
1920
return {
2021
script: Buffer.from(script, 'hex'),
2122
value: value === 'max' ? 'max' : BigInt(value),
23+
...(optional !== undefined ? { optional } : {}),
2224
};
2325
}
2426

27+
function expectedOutput(script: string, value: bigint | number | 'max', optional?: boolean): ExpectedOutput {
28+
return output(script, value as 'max', optional) as ExpectedOutput;
29+
}
30+
2531
const a = output('aa', 1);
2632
const a2 = output('aa', 2);
2733
const aMax = output('aa', 'max');
@@ -64,6 +70,32 @@ describe('outputDifference', function () {
6470
});
6571
});
6672

73+
describe('getMissingOutputs', function () {
74+
it('returns missing non-optional outputs', function () {
75+
const aOptional = expectedOutput('aa', 1, true);
76+
const bOptional = expectedOutput('bb', 1, true);
77+
78+
// No expected outputs means no missing outputs
79+
assert.deepStrictEqual(getMissingOutputs([a], []), []);
80+
81+
// Missing required output is returned
82+
assert.deepStrictEqual(getMissingOutputs([], [a]), [a]);
83+
assert.deepStrictEqual(getMissingOutputs([b], [a]), [a]);
84+
85+
// Missing optional output is filtered out
86+
assert.deepStrictEqual(getMissingOutputs([], [aOptional]), []);
87+
assert.deepStrictEqual(getMissingOutputs([b], [aOptional]), []);
88+
89+
// Mix of optional and required: only required missing outputs returned
90+
assert.deepStrictEqual(getMissingOutputs([], [a, bOptional]), [a]);
91+
assert.deepStrictEqual(getMissingOutputs([], [aOptional, b]), [b]);
92+
93+
// Present outputs are not returned regardless of optional flag
94+
assert.deepStrictEqual(getMissingOutputs([a], [a]), []);
95+
assert.deepStrictEqual(getMissingOutputs([a], [aOptional]), []);
96+
});
97+
});
98+
6799
describe('outputDifferencesWithExpected', function () {
68100
function test(
69101
outputs: ActualOutput[],
@@ -91,5 +123,27 @@ describe('outputDifference', function () {
91123
test([a, a], [a], { missing: [], explicit: [a], implicit: [a] });
92124
test([a, b], [a], { missing: [], explicit: [a], implicit: [b] });
93125
});
126+
127+
it('handles optional expected outputs', function () {
128+
const aOptional = expectedOutput('aa', 1, true);
129+
const bOptional = expectedOutput('bb', 1, true);
130+
131+
// Missing optional output is not reported as missing
132+
test([], [aOptional], { missing: [], explicit: [], implicit: [] });
133+
test([b], [aOptional], { missing: [], explicit: [], implicit: [b] });
134+
135+
// Present optional output is still explicit
136+
test([a], [aOptional], { missing: [], explicit: [a], implicit: [] });
137+
138+
// Mix of required and optional outputs
139+
test([], [a, bOptional], { missing: [a], explicit: [], implicit: [] });
140+
test([a], [a, bOptional], { missing: [], explicit: [a], implicit: [] });
141+
test([a, b], [a, bOptional], { missing: [], explicit: [a, b], implicit: [] });
142+
143+
// Multiple optional outputs
144+
test([], [aOptional, bOptional], { missing: [], explicit: [], implicit: [] });
145+
test([a], [aOptional, bOptional], { missing: [], explicit: [a], implicit: [] });
146+
test([a, b], [aOptional, bOptional], { missing: [], explicit: [a, b], implicit: [] });
147+
});
94148
});
95149
});

0 commit comments

Comments
 (0)