Skip to content

Commit e664c26

Browse files
feat(abstract-utxo): add support for 'max' value parse.ts
Issue: BTC-1697
1 parent 470767a commit e664c26

File tree

4 files changed

+55
-20
lines changed

4 files changed

+55
-20
lines changed

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

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,28 @@ import { fromExtendedAddressFormatToScript, toExtendedAddressFormat } from '../r
1717

1818
import { outputDifferencesWithExpected, OutputDifferenceWithExpected } from './outputDifference';
1919

20-
function toParsedOutput(recipient: ITransactionRecipient, network: utxolib.Network): ParsedOutput {
20+
export type RecipientOutput = Omit<ParsedOutput, 'value'> & {
21+
value: bigint | 'max';
22+
};
23+
24+
function toRecipientOutput(recipient: ITransactionRecipient, network: utxolib.Network): RecipientOutput {
2125
return {
2226
address: recipient.address,
23-
value: BigInt(recipient.amount),
27+
value: recipient.amount === 'max' ? 'max' : BigInt(recipient.amount),
2428
script: fromExtendedAddressFormatToScript(recipient.address, network),
2529
};
2630
}
2731

2832
// TODO(BTC-1697): allow outputs with `value: 'max'` here
29-
type ParsedOutputs = OutputDifferenceWithExpected<ParsedOutput, ParsedOutput> & {
33+
type ParsedOutputs = OutputDifferenceWithExpected<ParsedOutput, RecipientOutput> & {
3034
outputs: ParsedOutput[];
3135
changeOutputs: ParsedOutput[];
3236
};
3337

3438
function parseOutputsWithPsbt(
3539
psbt: utxolib.bitgo.UtxoPsbt,
3640
descriptorMap: coreDescriptors.DescriptorMap,
37-
recipientOutputs: ParsedOutput[]
41+
recipientOutputs: RecipientOutput[]
3842
): ParsedOutputs {
3943
const parsed = coreDescriptors.parse(psbt, descriptorMap, psbt.network);
4044
const externalOutputs = parsed.outputs.filter((o) => o.scriptId === undefined);
@@ -51,17 +55,22 @@ function sumValues(arr: { value: bigint }[]): bigint {
5155
return arr.reduce((sum, e) => sum + e.value, BigInt(0));
5256
}
5357

54-
function toBaseOutputs(outputs: ParsedOutput[], network: utxolib.Network): BaseOutput<bigint>[] {
58+
function toBaseOutputs(outputs: ParsedOutput[], network: utxolib.Network): BaseOutput<bigint>[];
59+
function toBaseOutputs(outputs: RecipientOutput[], network: utxolib.Network): BaseOutput<bigint | 'max'>[];
60+
function toBaseOutputs(
61+
outputs: (ParsedOutput | RecipientOutput)[],
62+
network: utxolib.Network
63+
): BaseOutput<bigint | 'max'>[] {
5564
return outputs.map(
56-
(o): BaseOutput<bigint> => ({
65+
(o): BaseOutput<bigint | 'max'> => ({
5766
address: toExtendedAddressFormat(o.script, network),
58-
amount: BigInt(o.value),
67+
amount: o.value === 'max' ? 'max' : BigInt(o.value),
5968
external: o.scriptId === undefined,
6069
})
6170
);
6271
}
6372

64-
export type ParsedOutputsBigInt = BaseParsedTransactionOutputs<bigint, BaseOutput<bigint>>;
73+
export type ParsedOutputsBigInt = BaseParsedTransactionOutputs<bigint, BaseOutput<bigint | 'max'>>;
6574

6675
function toBaseParsedTransactionOutputs(
6776
{ outputs, changeOutputs, explicitExternalOutputs, implicitExternalOutputs, missingOutputs }: ParsedOutputs,
@@ -88,15 +97,15 @@ export function toBaseParsedTransactionOutputsFromPsbt(
8897
parseOutputsWithPsbt(
8998
psbt,
9099
descriptorMap,
91-
recipients.map((r) => toParsedOutput(r, psbt.network))
100+
recipients.map((r) => toRecipientOutput(r, psbt.network))
92101
),
93102
network
94103
);
95104
}
96105

97106
export type ParsedDescriptorTransaction<TAmount extends number | bigint> = BaseParsedTransaction<
98107
TAmount,
99-
BaseOutput<TAmount>
108+
BaseOutput<TAmount | 'max'>
100109
>;
101110

102111
export function parse(

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { parse, ParsedDescriptorTransaction } from './parse';
66
type AmountType = 'number' | 'bigint' | 'string';
77

88
export function toAmountType(v: number | bigint | string, t: AmountType): number | bigint | string {
9+
if (v === 'max') {
10+
return v;
11+
}
912
switch (t) {
1013
case 'number':
1114
return Number(v);
@@ -22,7 +25,7 @@ type AmountTypeOptions = {
2225
};
2326

2427
function baseOutputToTNumber<TAmount extends number | bigint>(
25-
output: BaseOutput<bigint>,
28+
output: BaseOutput<bigint | 'max'>,
2629
amountType: AmountType
2730
): BaseOutput<TAmount> {
2831
return {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ export class ValidationError extends Error {
1313
}
1414

1515
export class ErrorMissingOutputs extends ValidationError {
16-
constructor(public missingOutputs: BaseOutput<bigint>[]) {
16+
constructor(public missingOutputs: BaseOutput<bigint | 'max'>[]) {
1717
super(`missing outputs (count=${missingOutputs.length})`);
1818
}
1919
}
2020

2121
export class ErrorImplicitExternalOutputs extends ValidationError {
22-
constructor(public implicitExternalOutputs: BaseOutput<bigint>[]) {
22+
constructor(public implicitExternalOutputs: BaseOutput<bigint | 'max'>[]) {
2323
super(`unexpected implicit external outputs (count=${implicitExternalOutputs.length})`);
2424
}
2525
}
@@ -31,7 +31,7 @@ export class AggregateValidationError extends ValidationError {
3131
}
3232

3333
export function assertExpectedOutputDifference(
34-
parsedOutputs: BaseParsedTransactionOutputs<bigint, BaseOutput<bigint>>
34+
parsedOutputs: BaseParsedTransactionOutputs<bigint, BaseOutput<bigint | 'max'>>
3535
): void {
3636
const errors: ValidationError[] = [];
3737
if (parsedOutputs.missingOutputs.length > 0) {

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

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

3-
import * as utxolib from '@bitgo/utxo-lib';
4-
53
import { mockPsbtDefaultWithDescriptorTemplate } from '../../core/descriptor/psbt/mock.utils';
64
import { ParsedOutputsBigInt, toBaseParsedTransactionOutputsFromPsbt } from '../../../src/transaction/descriptor/parse';
75
import { getDefaultXPubs, getDescriptorMap } from '../../core/descriptor/descriptor.utils';
@@ -17,26 +15,38 @@ import { BaseOutput } from '../../../src';
1715

1816
import { assertEqualFixture } from './fixtures.utils';
1917

20-
function toBaseOutput<TNumber>(output: utxolib.PsbtTxOutput, amountType: 'bigint' | 'string'): BaseOutput<TNumber> {
18+
type OutputWithValue<T = number | bigint | string> = {
19+
address?: string;
20+
value: T;
21+
};
22+
23+
function toBaseOutput<TNumber>(output: OutputWithValue, amountType: 'bigint' | 'string'): BaseOutput<TNumber> {
2124
assert(output.address);
2225
return {
2326
address: output.address,
2427
amount: toAmountType(output.value, amountType) as TNumber,
2528
};
2629
}
2730

28-
function toBaseOutputBigInt(output: utxolib.PsbtTxOutput): BaseOutput<bigint> {
31+
function toBaseOutputBigInt(output: OutputWithValue): BaseOutput<bigint> {
2932
return toBaseOutput(output, 'bigint');
3033
}
3134

32-
function toBaseOutputString(output: utxolib.PsbtTxOutput): BaseOutput<string> {
35+
function toBaseOutputString(output: OutputWithValue): BaseOutput<string> {
3336
return toBaseOutput(output, 'string');
3437
}
3538

39+
function toMaxOutput(output: OutputWithValue): OutputWithValue<'max'> {
40+
return {
41+
...output,
42+
value: 'max',
43+
};
44+
}
45+
3646
describe('parse', function () {
3747
const psbt = mockPsbtDefaultWithDescriptorTemplate('Wsh2Of3');
3848

39-
function getBaseParsedTransaction(recipients: utxolib.PsbtTxOutput[]): ParsedOutputsBigInt {
49+
function getBaseParsedTransaction(recipients: OutputWithValue[]): ParsedOutputsBigInt {
4050
return toBaseParsedTransactionOutputsFromPsbt(
4151
psbt,
4252
getDescriptorMap('Wsh2Of3', getDefaultXPubs('a')),
@@ -49,6 +59,11 @@ describe('parse', function () {
4959
it('should return the correct BaseParsedTransactionOutputs', async function () {
5060
await assertEqualFixture('parseWithoutRecipients.json', toPlainObject(getBaseParsedTransaction([])));
5161
await assertEqualFixture('parseWithRecipient.json', toPlainObject(getBaseParsedTransaction([psbt.txOutputs[0]])));
62+
await assertEqualFixture(
63+
'parseWithRecipient.json',
64+
// max recipient: ignore actual value
65+
toPlainObject(getBaseParsedTransaction([toMaxOutput(psbt.txOutputs[0])]))
66+
);
5267
});
5368

5469
function assertEqualValidationError(actual: unknown, expected: AggregateValidationError) {
@@ -88,6 +103,14 @@ describe('parse', function () {
88103
new ErrorImplicitExternalOutputs([{ ...toBaseOutputBigInt(psbt.txOutputs[0]), external: true }]),
89104
])
90105
);
106+
107+
assertValidationError(
108+
() => assertExpectedOutputDifference(getBaseParsedTransaction([toMaxOutput(psbt.txOutputs[1])])),
109+
new AggregateValidationError([
110+
new ErrorMissingOutputs([{ ...toBaseOutputBigInt(toMaxOutput(psbt.txOutputs[1])), external: true }]),
111+
new ErrorImplicitExternalOutputs([{ ...toBaseOutputBigInt(psbt.txOutputs[0]), external: true }]),
112+
])
113+
);
91114
});
92115
});
93116
});

0 commit comments

Comments
 (0)