Skip to content

Commit 55363c5

Browse files
Merge pull request #5417 from BitGo/BTC-1791.explicit-internal-recipient
feat(abstract-utxo): handle explicit internal recipient
2 parents 3e51361 + 74a940f commit 55363c5

File tree

6 files changed

+119
-45
lines changed

6 files changed

+119
-45
lines changed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,8 @@ function parseOutputsWithPsbt(
4040
recipientOutputs: RecipientOutput[]
4141
): ParsedOutputs {
4242
const parsed = coreDescriptors.parse(psbt, descriptorMap, psbt.network);
43-
const externalOutputs = parsed.outputs.filter((o) => o.scriptId === undefined);
4443
const changeOutputs = parsed.outputs.filter((o) => o.scriptId !== undefined);
45-
const outputDiffs = outputDifferencesWithExpected(externalOutputs, recipientOutputs);
44+
const outputDiffs = outputDifferencesWithExpected(parsed.outputs, recipientOutputs);
4645
return {
4746
outputs: parsed.outputs,
4847
changeOutputs,
@@ -72,9 +71,11 @@ function toBaseOutputs(
7271
export type ParsedOutputsBigInt = BaseParsedTransactionOutputs<bigint, BaseOutput<bigint | 'max'>>;
7372

7473
function toBaseParsedTransactionOutputs(
75-
{ outputs, changeOutputs, explicitExternalOutputs, implicitExternalOutputs, missingOutputs }: ParsedOutputs,
74+
{ outputs, changeOutputs, explicitOutputs, implicitOutputs, missingOutputs }: ParsedOutputs,
7675
network: utxolib.Network
7776
): ParsedOutputsBigInt {
77+
const explicitExternalOutputs = explicitOutputs.filter((o) => o.scriptId === undefined);
78+
const implicitExternalOutputs = implicitOutputs.filter((o) => o.scriptId === undefined);
7879
return {
7980
outputs: toBaseOutputs(outputs, network),
8081
changeOutputs: toBaseOutputs(changeOutputs, network),

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,33 +42,33 @@ export function outputDifference<A extends ActualOutput | ExpectedOutput, B exte
4242

4343
export type OutputDifferenceWithExpected<TActual extends ActualOutput, TExpected extends ExpectedOutput> = {
4444
/** These are the external outputs that were expected and found in the transaction. */
45-
explicitExternalOutputs: TActual[];
45+
explicitOutputs: TActual[];
4646
/**
4747
* These are the surprise external outputs that were not explicitly specified in the transaction.
4848
* They can be PayGo fees.
4949
*/
50-
implicitExternalOutputs: TActual[];
50+
implicitOutputs: TActual[];
5151
/**
5252
* These are the outputs that were expected to be in the transaction but were not found.
5353
*/
5454
missingOutputs: TExpected[];
5555
};
5656

5757
/**
58-
* @param actualExternalOutputs - external outputs in the transaction
59-
* @param expectedExternalOutputs - external outputs that were expected to be in the transaction
58+
* @param actualOutputs - external outputs in the transaction
59+
* @param expectedOutputs - external outputs that were expected to be in the transaction
6060
* @returns the difference between the actual and expected external outputs
6161
*/
6262
export function outputDifferencesWithExpected<TActual extends ActualOutput, TExpected extends ExpectedOutput>(
63-
actualExternalOutputs: TActual[],
64-
expectedExternalOutputs: TExpected[]
63+
actualOutputs: TActual[],
64+
expectedOutputs: TExpected[]
6565
): OutputDifferenceWithExpected<TActual, TExpected> {
66-
const implicitExternalOutputs = outputDifference(actualExternalOutputs, expectedExternalOutputs);
67-
const explicitExternalOutputs = outputDifference(actualExternalOutputs, implicitExternalOutputs);
68-
const missingOutputs = outputDifference(expectedExternalOutputs, actualExternalOutputs);
66+
const implicitOutputs = outputDifference(actualOutputs, expectedOutputs);
67+
const explicitOutputs = outputDifference(actualOutputs, implicitOutputs);
68+
const missingOutputs = outputDifference(expectedOutputs, actualOutputs);
6969
return {
70-
explicitExternalOutputs,
71-
implicitExternalOutputs,
70+
explicitOutputs,
71+
implicitOutputs,
7272
missingOutputs,
7373
};
7474
}

modules/abstract-utxo/test/transaction/descriptor/fixtures/parseWithRecipient.json renamed to modules/abstract-utxo/test/transaction/descriptor/fixtures/parseWithExternalRecipient.json

File renamed without changes.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"outputs": [
3+
{
4+
"address": "bc1qvn279lx29cg843u77p3c37npay7w4uc4xw5d92xxa92z8gd3lkuq4w8477",
5+
"amount": "400000",
6+
"external": true
7+
},
8+
{
9+
"address": "bc1q2yau645jl7k577lmanqn9a0ulcgcqm0wrrmx09dppd3kcwguvyzqk86umj",
10+
"amount": "400000",
11+
"external": false
12+
}
13+
],
14+
"changeOutputs": [
15+
{
16+
"address": "bc1q2yau645jl7k577lmanqn9a0ulcgcqm0wrrmx09dppd3kcwguvyzqk86umj",
17+
"amount": "400000",
18+
"external": false
19+
}
20+
],
21+
"explicitExternalOutputs": [],
22+
"explicitExternalSpendAmount": "0",
23+
"implicitExternalOutputs": [
24+
{
25+
"address": "bc1qvn279lx29cg843u77p3c37npay7w4uc4xw5d92xxa92z8gd3lkuq4w8477",
26+
"amount": "400000",
27+
"external": true
28+
}
29+
],
30+
"implicitExternalSpendAmount": "400000",
31+
"missingOutputs": []
32+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ describe('outputDifference', function () {
7676
) {
7777
const result = outputDifferencesWithExpected(outputs, recipients);
7878
assert.deepStrictEqual(result, {
79-
explicitExternalOutputs: expected.explicit,
80-
implicitExternalOutputs: expected.implicit,
79+
explicitOutputs: expected.explicit,
80+
implicitOutputs: expected.implicit,
8181
missingOutputs: expected.missing,
8282
});
8383
}

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

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

3-
import { mockPsbtDefaultWithDescriptorTemplate } from '../../core/descriptor/psbt/mock.utils';
3+
import * as utxolib from '@bitgo/utxo-lib';
4+
import { Descriptor } from '@bitgo/wasm-miniscript';
5+
6+
import { mockPsbtDefault } from '../../core/descriptor/psbt/mock.utils';
47
import { ParsedOutputsBigInt, toBaseParsedTransactionOutputsFromPsbt } from '../../../src/transaction/descriptor/parse';
5-
import { getDefaultXPubs, getDescriptorMap } from '../../core/descriptor/descriptor.utils';
8+
import { getDefaultXPubs, getDescriptor, getDescriptorMap } from '../../core/descriptor/descriptor.utils';
69
import { toPlainObject } from '../../core/toPlainObject.utils';
710
import {
811
AggregateValidationError,
@@ -12,6 +15,7 @@ import {
1215
} from '../../../src/transaction/descriptor/verifyTransaction';
1316
import { toAmountType } from '../../../src/transaction/descriptor/parseToAmountType';
1417
import { BaseOutput } from '../../../src';
18+
import { createAddressFromDescriptor } from '../../../src/core/descriptor';
1519

1620
import { getFixtureRoot } from './fixtures.utils';
1721

@@ -46,9 +50,23 @@ function toMaxOutput(output: OutputWithValue): OutputWithValue<'max'> {
4650
}
4751

4852
describe('parse', function () {
49-
const psbt = mockPsbtDefaultWithDescriptorTemplate('Wsh2Of3');
53+
const descriptorSelf = getDescriptor('Wsh2Of3', getDefaultXPubs('a'));
54+
const descriptorOther = getDescriptor('Wsh2Of3', getDefaultXPubs('b'));
55+
const psbt = mockPsbtDefault({ descriptorSelf, descriptorOther });
56+
57+
function recipient(descriptor: Descriptor, index: number, value = 1000) {
58+
return { value, address: createAddressFromDescriptor(descriptor, index, utxolib.networks.bitcoin) };
59+
}
60+
61+
function internalRecipient(index: number, value?: number): OutputWithValue {
62+
return recipient(descriptorSelf, index, value);
63+
}
64+
65+
function externalRecipient(index: number, value?: number): OutputWithValue {
66+
return recipient(descriptorOther, index, value);
67+
}
5068

51-
function getBaseParsedTransaction(recipients: OutputWithValue[]): ParsedOutputsBigInt {
69+
function getBaseParsedTransaction(psbt: utxolib.bitgo.UtxoPsbt, recipients: OutputWithValue[]): ParsedOutputsBigInt {
5270
return toBaseParsedTransactionOutputsFromPsbt(
5371
psbt,
5472
getDescriptorMap('Wsh2Of3', getDefaultXPubs('a')),
@@ -59,20 +77,30 @@ describe('parse', function () {
5977

6078
describe('toBase', function () {
6179
it('should return the correct BaseParsedTransactionOutputs', async function () {
62-
await assertEqualFixture('parseWithoutRecipients.json', toPlainObject(getBaseParsedTransaction([])));
63-
await assertEqualFixture('parseWithRecipient.json', toPlainObject(getBaseParsedTransaction([psbt.txOutputs[0]])));
80+
await assertEqualFixture('parseWithoutRecipients.json', toPlainObject(getBaseParsedTransaction(psbt, [])));
81+
await assertEqualFixture(
82+
'parseWithExternalRecipient.json',
83+
toPlainObject(getBaseParsedTransaction(psbt, [psbt.txOutputs[0]]))
84+
);
6485
await assertEqualFixture(
65-
'parseWithRecipient.json',
86+
'parseWithInternalRecipient.json',
87+
toPlainObject(getBaseParsedTransaction(psbt, [psbt.txOutputs[1]]))
88+
);
89+
await assertEqualFixture(
90+
'parseWithExternalRecipient.json',
6691
// max recipient: ignore actual value
67-
toPlainObject(getBaseParsedTransaction([toMaxOutput(psbt.txOutputs[0])]))
92+
toPlainObject(getBaseParsedTransaction(psbt, [toMaxOutput(psbt.txOutputs[0])]))
6893
);
6994
});
7095

7196
function assertEqualValidationError(actual: unknown, expected: AggregateValidationError) {
97+
function normErrors(e: Error[]): Error[] {
98+
return e.map((e) => ({ ...e, stack: undefined }));
99+
}
72100
if (actual instanceof AggregateValidationError) {
73-
assert.deepStrictEqual(actual.errors, expected.errors);
101+
assert.deepStrictEqual(normErrors(actual.errors), normErrors(expected.errors));
74102
} else {
75-
throw new Error('unexpected error type');
103+
throw new Error('unexpected error type: ' + actual);
76104
}
77105
}
78106

@@ -83,35 +111,48 @@ describe('parse', function () {
83111
});
84112
}
85113

86-
it('should throw expected errors', function () {
114+
function implicitOutputError(output: OutputWithValue, { external = true } = {}): ErrorImplicitExternalOutputs {
115+
return new ErrorImplicitExternalOutputs([{ ...toBaseOutputBigInt(output), external }]);
116+
}
117+
118+
function missingOutputError(output: OutputWithValue, { external = true } = {}): ErrorMissingOutputs {
119+
return new ErrorMissingOutputs([{ ...toBaseOutputBigInt(output), external }]);
120+
}
121+
122+
it('should throw expected error: no recipient requested', function () {
87123
assertValidationError(
88-
() => assertExpectedOutputDifference(getBaseParsedTransaction([])),
89-
new AggregateValidationError([
90-
new ErrorImplicitExternalOutputs([{ ...toBaseOutputBigInt(psbt.txOutputs[0]), external: true }]),
91-
])
124+
() => assertExpectedOutputDifference(getBaseParsedTransaction(psbt, [])),
125+
new AggregateValidationError([implicitOutputError(psbt.txOutputs[0])])
92126
);
127+
});
93128

129+
it('should throw expected error: only internal recipient requested', function () {
94130
assertValidationError(
95-
() => assertExpectedOutputDifference(getBaseParsedTransaction([])),
96-
new AggregateValidationError([
97-
new ErrorImplicitExternalOutputs([{ ...toBaseOutputBigInt(psbt.txOutputs[0]), external: true }]),
98-
])
131+
() => assertExpectedOutputDifference(getBaseParsedTransaction(psbt, [psbt.txOutputs[1]])),
132+
new AggregateValidationError([implicitOutputError(psbt.txOutputs[0])])
99133
);
134+
});
100135

136+
it('should throw expected error: only internal max recipient requested', function () {
101137
assertValidationError(
102-
() => assertExpectedOutputDifference(getBaseParsedTransaction([psbt.txOutputs[1]])),
103-
new AggregateValidationError([
104-
new ErrorMissingOutputs([{ ...toBaseOutputBigInt(psbt.txOutputs[1]), external: true }]),
105-
new ErrorImplicitExternalOutputs([{ ...toBaseOutputBigInt(psbt.txOutputs[0]), external: true }]),
106-
])
138+
() => assertExpectedOutputDifference(getBaseParsedTransaction(psbt, [toMaxOutput(psbt.txOutputs[1])])),
139+
new AggregateValidationError([implicitOutputError(psbt.txOutputs[0])])
107140
);
141+
});
142+
143+
it('should throw expected error: swapped recipient', function () {
144+
const recipient = externalRecipient(99);
145+
assertValidationError(
146+
() => assertExpectedOutputDifference(getBaseParsedTransaction(psbt, [recipient])),
147+
new AggregateValidationError([missingOutputError(recipient), implicitOutputError(psbt.txOutputs[0])])
148+
);
149+
});
108150

151+
it('should throw expected error: missing internal recipient', function () {
152+
const recipient = internalRecipient(99);
109153
assertValidationError(
110-
() => assertExpectedOutputDifference(getBaseParsedTransaction([toMaxOutput(psbt.txOutputs[1])])),
111-
new AggregateValidationError([
112-
new ErrorMissingOutputs([{ ...toBaseOutputBigInt(toMaxOutput(psbt.txOutputs[1])), external: true }]),
113-
new ErrorImplicitExternalOutputs([{ ...toBaseOutputBigInt(psbt.txOutputs[0]), external: true }]),
114-
])
154+
() => assertExpectedOutputDifference(getBaseParsedTransaction(psbt, [recipient])),
155+
new AggregateValidationError([missingOutputError(recipient), implicitOutputError(psbt.txOutputs[0])])
115156
);
116157
});
117158
});

0 commit comments

Comments
 (0)