Skip to content

Commit 470767a

Browse files
feat(abstract-utxo): allow 'max' value in ExpectedOutput
Issue: BTC-1697
1 parent 5a592c1 commit 470767a

File tree

3 files changed

+86
-47
lines changed

3 files changed

+86
-47
lines changed

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

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,68 @@
1-
export type ComparableOutput = {
1+
export type ComparableOutput<TValue> = {
22
script: Buffer;
3-
value: bigint;
3+
value: TValue;
44
};
55

6-
export function equalOutput<T extends ComparableOutput>(a: T, b: T): boolean {
7-
return a.value === b.value && a.script.equals(b.script);
6+
/** Actual outputs have fixed values. */
7+
export type ActualOutput = ComparableOutput<bigint>;
8+
9+
/** Expected outputs can have a fixed value or 'max'. */
10+
export type ExpectedOutput = ComparableOutput<bigint | 'max'>;
11+
12+
/**
13+
* @param a
14+
* @param b
15+
* @returns whether the two outputs are equal. Outputs with value `max` are considered equal to any other output with the same script.
16+
*/
17+
export function matchingOutput<TValue>(a: ComparableOutput<TValue>, b: ComparableOutput<TValue>): boolean {
18+
if (a.value === 'max' || b.value === 'max') {
19+
return a.script.equals(b.script);
20+
}
21+
return a.script.equals(b.script) && a.value === b.value;
822
}
923

1024
/**
11-
* @returns all outputs in the first array that are not in the second array
25+
* @returns all outputs in the first array that are not in the second array.
1226
* Outputs can occur more than once in each array.
27+
* An output with value `max` is considered equal to any other output with the same script.
1328
*/
14-
export function outputDifference<T extends ComparableOutput>(first: T[], second: T[]): T[] {
29+
export function outputDifference<A extends ActualOutput | ExpectedOutput, B extends ActualOutput | ExpectedOutput>(
30+
first: A[],
31+
second: B[]
32+
): A[] {
1533
first = first.slice();
1634
for (const output of second) {
17-
const index = first.findIndex((o) => equalOutput(o, output));
35+
const index = first.findIndex((o) => matchingOutput(o, output));
1836
if (index !== -1) {
1937
first.splice(index, 1);
2038
}
2139
}
2240
return first;
2341
}
2442

25-
export type OutputDifferenceWithExpected<T extends ComparableOutput> = {
43+
export type OutputDifferenceWithExpected<TActual extends ActualOutput, TExpected extends ExpectedOutput> = {
2644
/** These are the external outputs that were expected and found in the transaction. */
27-
explicitExternalOutputs: T[];
45+
explicitExternalOutputs: TActual[];
2846
/**
2947
* These are the surprise external outputs that were not explicitly specified in the transaction.
3048
* They can be PayGo fees.
3149
*/
32-
implicitExternalOutputs: T[];
50+
implicitExternalOutputs: TActual[];
3351
/**
3452
* These are the outputs that were expected to be in the transaction but were not found.
3553
*/
36-
missingOutputs: T[];
54+
missingOutputs: TExpected[];
3755
};
3856

3957
/**
4058
* @param actualExternalOutputs - external outputs in the transaction
4159
* @param expectedExternalOutputs - external outputs that were expected to be in the transaction
4260
* @returns the difference between the actual and expected external outputs
4361
*/
44-
export function outputDifferencesWithExpected<T extends ComparableOutput>(
45-
actualExternalOutputs: T[],
46-
expectedExternalOutputs: T[]
47-
): OutputDifferenceWithExpected<T> {
62+
export function outputDifferencesWithExpected<TActual extends ActualOutput, TExpected extends ExpectedOutput>(
63+
actualExternalOutputs: TActual[],
64+
expectedExternalOutputs: TExpected[]
65+
): OutputDifferenceWithExpected<TActual, TExpected> {
4866
const implicitExternalOutputs = outputDifference(actualExternalOutputs, expectedExternalOutputs);
4967
const explicitExternalOutputs = outputDifference(actualExternalOutputs, implicitExternalOutputs);
5068
const missingOutputs = outputDifference(expectedExternalOutputs, actualExternalOutputs);

modules/abstract-utxo/src/transaction/descriptor/parse.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ function toParsedOutput(recipient: ITransactionRecipient, network: utxolib.Netwo
2525
};
2626
}
2727

28-
type ParsedOutputs = OutputDifferenceWithExpected<ParsedOutput> & {
28+
// TODO(BTC-1697): allow outputs with `value: 'max'` here
29+
type ParsedOutputs = OutputDifferenceWithExpected<ParsedOutput, ParsedOutput> & {
2930
outputs: ParsedOutput[];
3031
changeOutputs: ParsedOutput[];
3132
};
Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,77 @@
11
import assert from 'assert';
22

33
import {
4-
ComparableOutput,
5-
equalOutput,
4+
ActualOutput,
5+
ExpectedOutput,
6+
matchingOutput,
67
outputDifference,
78
outputDifferencesWithExpected,
89
} from '../../../src/transaction/descriptor/outputDifference';
910

1011
describe('outputDifference', function () {
11-
function output(script: string, value: bigint | number) {
12+
function output(script: string, value: bigint | number): ActualOutput;
13+
function output(script: string, value: 'max'): ExpectedOutput;
14+
function output(script: string, value: bigint | number | 'max'): ActualOutput | ExpectedOutput {
1215
const scriptBuffer = Buffer.from(script, 'hex');
1316
if (scriptBuffer.toString('hex') !== script) {
1417
throw new Error('invalid script');
1518
}
1619
return {
1720
script: Buffer.from(script, 'hex'),
18-
value: BigInt(value),
21+
value: value === 'max' ? 'max' : BigInt(value),
1922
};
2023
}
2124

2225
const a = output('aa', 1);
2326
const a2 = output('aa', 2);
24-
const b = output('bb', 2);
25-
const c = output('cc', 3);
27+
const aMax = output('aa', 'max');
28+
const b = output('bb', 1);
29+
const c = output('cc', 1);
2630

2731
describe('equalOutput', function () {
2832
it('has expected result', function () {
29-
assert.deepStrictEqual(equalOutput(a, a), true);
30-
assert.deepStrictEqual(equalOutput(a, a2), false);
31-
assert.deepStrictEqual(equalOutput(a, b), false);
33+
assert.deepStrictEqual(matchingOutput(a, a), true);
34+
assert.deepStrictEqual(matchingOutput(a, a2), false);
35+
assert.deepStrictEqual(matchingOutput(a, b), false);
36+
assert.deepStrictEqual(matchingOutput(aMax, b), false);
37+
38+
assert.deepStrictEqual(matchingOutput(aMax, a), true);
39+
assert.deepStrictEqual(matchingOutput(a, aMax), true);
40+
// this one does not appear in practice but is a valid comparison
41+
assert.deepStrictEqual(matchingOutput(aMax, aMax), true);
3242
});
3343
});
3444

3545
describe('outputDifference', function () {
36-
assert.deepStrictEqual(outputDifference([], []), []);
37-
assert.deepStrictEqual(outputDifference([a], []), [a]);
38-
assert.deepStrictEqual(outputDifference([], [a]), []);
39-
assert.deepStrictEqual(outputDifference([a], [a]), []);
40-
assert.deepStrictEqual(outputDifference([a, a], [a]), [a]);
41-
assert.deepStrictEqual(outputDifference([a, a, a], [a]), [a, a]);
42-
assert.deepStrictEqual(outputDifference([a, b, c], [a, b]), [c]);
43-
assert.deepStrictEqual(outputDifference([a, b, c, a], [a, b]), [c, a]);
46+
it('has expected result', function () {
47+
assert.deepStrictEqual(outputDifference([], []), []);
48+
assert.deepStrictEqual(outputDifference([a], []), [a]);
49+
assert.deepStrictEqual(outputDifference([aMax], []), [aMax]);
50+
assert.deepStrictEqual(outputDifference([], [a]), []);
51+
assert.deepStrictEqual(outputDifference([], [aMax]), []);
52+
assert.deepStrictEqual(outputDifference([a], [a]), []);
53+
assert.deepStrictEqual(outputDifference([a], [aMax]), []);
54+
assert.deepStrictEqual(outputDifference([aMax], [a]), []);
55+
assert.deepStrictEqual(outputDifference([a, a], [a]), [a]);
56+
assert.deepStrictEqual(outputDifference([a, a], [aMax]), [a]);
57+
assert.deepStrictEqual(outputDifference([a, a, a], [a]), [a, a]);
58+
assert.deepStrictEqual(outputDifference([a, b, c], [a, b]), [c]);
59+
assert.deepStrictEqual(outputDifference([a, b, c], [aMax, b]), [c]);
60+
assert.deepStrictEqual(outputDifference([a, b, c, a], [a, b]), [c, a]);
4461

45-
assert.deepStrictEqual(outputDifference([a], [a2]), [a]);
46-
assert.deepStrictEqual(outputDifference([a2], [a]), [a2]);
62+
assert.deepStrictEqual(outputDifference([a], [a2]), [a]);
63+
assert.deepStrictEqual(outputDifference([a2], [a]), [a2]);
64+
});
4765
});
4866

4967
describe('outputDifferencesWithExpected', function () {
5068
function test(
51-
outputs: ComparableOutput[],
52-
recipients: ComparableOutput[],
69+
outputs: ActualOutput[],
70+
recipients: ExpectedOutput[],
5371
expected: {
54-
missing: ComparableOutput[];
55-
explicit: ComparableOutput[];
56-
implicit: ComparableOutput[];
72+
missing: ExpectedOutput[];
73+
explicit: ActualOutput[];
74+
implicit: ActualOutput[];
5775
}
5876
) {
5977
const result = outputDifferencesWithExpected(outputs, recipients);
@@ -64,12 +82,14 @@ describe('outputDifference', function () {
6482
});
6583
}
6684

67-
test([a], [], { missing: [], explicit: [], implicit: [a] });
68-
test([], [a], { missing: [a], explicit: [], implicit: [] });
69-
test([a], [a], { missing: [], explicit: [a], implicit: [] });
70-
test([a], [a2], { missing: [a2], explicit: [], implicit: [a] });
71-
test([b], [a], { missing: [a], explicit: [], implicit: [b] });
72-
test([a, a], [a], { missing: [], explicit: [a], implicit: [a] });
73-
test([a, b], [a], { missing: [], explicit: [a], implicit: [b] });
85+
it('has expected result', function () {
86+
test([a], [], { missing: [], explicit: [], implicit: [a] });
87+
test([], [a], { missing: [a], explicit: [], implicit: [] });
88+
test([a], [a], { missing: [], explicit: [a], implicit: [] });
89+
test([a], [a2], { missing: [a2], explicit: [], implicit: [a] });
90+
test([b], [a], { missing: [a], explicit: [], implicit: [b] });
91+
test([a, a], [a], { missing: [], explicit: [a], implicit: [a] });
92+
test([a, b], [a], { missing: [], explicit: [a], implicit: [b] });
93+
});
7494
});
7595
});

0 commit comments

Comments
 (0)