Skip to content

Commit 9573556

Browse files
feat(abstract-utxo): implement parseTransaction for descriptor
TICKET: BTC-1450
1 parent b658ab5 commit 9573556

File tree

14 files changed

+361
-14
lines changed

14 files changed

+361
-14
lines changed

modules/abstract-utxo/src/descriptor/descriptorWallet.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { IWallet, WalletCoinSpecific } from '@bitgo/sdk-core';
44
import { NamedDescriptor } from './NamedDescriptor';
55
import { DescriptorMap } from '../core/descriptor';
66
import { DescriptorValidationPolicy, KeyTriple, toDescriptorMapValidate } from './validatePolicy';
7-
import { UtxoWalletData } from '../wallet';
7+
import { UtxoWallet, UtxoWalletData } from '../wallet';
88

99
type DescriptorWalletCoinSpecific = {
1010
descriptors: NamedDescriptor[];
@@ -20,7 +20,7 @@ type DescriptorWalletData = UtxoWalletData & {
2020
coinSpecific: DescriptorWalletCoinSpecific;
2121
};
2222

23-
interface IDescriptorWallet extends IWallet {
23+
export interface IDescriptorWallet extends UtxoWallet {
2424
coinSpecific(): WalletCoinSpecific & DescriptorWalletCoinSpecific;
2525
}
2626

modules/abstract-utxo/src/descriptor/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { Miniscript, Descriptor } from '@bitgo/wasm-miniscript';
22
export { assertDescriptorWalletAddress } from './assertDescriptorWalletAddress';
33
export { NamedDescriptor } from './NamedDescriptor';
44
export { isDescriptorWallet, getDescriptorMapFromWallet } from './descriptorWallet';
5+
export { getPolicyForEnv } from './validatePolicy';

modules/abstract-utxo/src/keychains.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,17 @@ export function toKeychainTriple(keychains: UtxoNamedKeychains): Triple<UtxoKeyc
4545
return [user, backup, bitgo];
4646
}
4747

48-
export function toBip32Triple(keychains: Triple<{ pub: string }> | Triple<string>): Triple<utxolib.BIP32Interface> {
49-
return keychains.map((keychain: { pub: string } | string) => {
50-
const v = typeof keychain === 'string' ? keychain : keychain.pub;
51-
return utxolib.bip32.fromBase58(v);
52-
}) as Triple<utxolib.BIP32Interface>;
48+
export function toBip32Triple(
49+
keychains: UtxoNamedKeychains | Triple<{ pub: string }> | Triple<string>
50+
): Triple<utxolib.BIP32Interface> {
51+
if (Array.isArray(keychains)) {
52+
return keychains.map((keychain: { pub: string } | string) => {
53+
const v = typeof keychain === 'string' ? keychain : keychain.pub;
54+
return utxolib.bip32.fromBase58(v);
55+
}) as Triple<utxolib.BIP32Interface>;
56+
}
57+
58+
return toBip32Triple(toKeychainTriple(keychains));
5359
}
5460

5561
export async function fetchKeychains(
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export { DescriptorMap } from '../../core/descriptor';
22
export { explainPsbt } from './explainPsbt';
3+
export { parse } from './parse';
4+
export { parseToAmountType } from './parseToAmountType';
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
import { ITransactionRecipient } from '@bitgo/sdk-core';
3+
4+
import {
5+
AbstractUtxoCoin,
6+
BaseOutput,
7+
BaseParsedTransaction,
8+
BaseParsedTransactionOutputs,
9+
ParseTransactionOptions,
10+
} from '../../abstractUtxoCoin';
11+
import { getKeySignatures, toBip32Triple, UtxoNamedKeychains } from '../../keychains';
12+
import { getDescriptorMapFromWallet, getPolicyForEnv } from '../../descriptor';
13+
import { IDescriptorWallet } from '../../descriptor/descriptorWallet';
14+
import * as coreDescriptors from '../../core/descriptor';
15+
import { ParsedOutput } from '../../core/descriptor/psbt/parse';
16+
import { outputDifferencesWithExpected, OutputDifferenceWithExpected } from './outputDifference';
17+
import { fromExtendedAddressFormatToScript, toExtendedAddressFormat } from '../recipient';
18+
19+
function toParsedOutput(recipient: ITransactionRecipient, network: utxolib.Network): ParsedOutput {
20+
return {
21+
address: recipient.address,
22+
value: BigInt(recipient.amount),
23+
script: fromExtendedAddressFormatToScript(recipient.address, network),
24+
};
25+
}
26+
27+
type ParsedOutputs = OutputDifferenceWithExpected<ParsedOutput> & {
28+
outputs: ParsedOutput[];
29+
changeOutputs: ParsedOutput[];
30+
};
31+
32+
function parseOutputsWithPsbt(
33+
psbt: utxolib.bitgo.UtxoPsbt,
34+
descriptorMap: coreDescriptors.DescriptorMap,
35+
recipientOutputs: ParsedOutput[]
36+
): ParsedOutputs {
37+
const parsed = coreDescriptors.parse(psbt, descriptorMap, psbt.network);
38+
const externalOutputs = parsed.outputs.filter((o) => o.scriptId === undefined);
39+
const changeOutputs = parsed.outputs.filter((o) => o.scriptId !== undefined);
40+
const outputDiffs = outputDifferencesWithExpected(externalOutputs, recipientOutputs);
41+
return {
42+
outputs: parsed.outputs,
43+
changeOutputs,
44+
...outputDiffs,
45+
};
46+
}
47+
48+
function sumValues(arr: { value: bigint }[]): bigint {
49+
return arr.reduce((sum, e) => sum + e.value, BigInt(0));
50+
}
51+
52+
function toBaseOutputs(outputs: ParsedOutput[], network: utxolib.Network): BaseOutput<bigint>[] {
53+
return outputs.map(
54+
(o): BaseOutput<bigint> => ({
55+
address: toExtendedAddressFormat(o.script, network),
56+
amount: BigInt(o.value),
57+
external: o.scriptId === undefined,
58+
})
59+
);
60+
}
61+
62+
function toBaseParsedTransactionOutputs(
63+
{ outputs, changeOutputs, explicitExternalOutputs, implicitExternalOutputs, missingOutputs }: ParsedOutputs,
64+
network: utxolib.Network
65+
): BaseParsedTransactionOutputs<bigint, BaseOutput<bigint>> {
66+
return {
67+
outputs: toBaseOutputs(outputs, network),
68+
changeOutputs: toBaseOutputs(changeOutputs, network),
69+
explicitExternalOutputs: toBaseOutputs(explicitExternalOutputs, network),
70+
explicitExternalSpendAmount: sumValues(explicitExternalOutputs),
71+
implicitExternalOutputs: toBaseOutputs(implicitExternalOutputs, network),
72+
implicitExternalSpendAmount: sumValues(implicitExternalOutputs),
73+
missingOutputs: toBaseOutputs(missingOutputs, network),
74+
};
75+
}
76+
77+
export function toBaseParsedTransactionOutputsFromPsbt(
78+
psbt: utxolib.bitgo.UtxoPsbt,
79+
descriptorMap: coreDescriptors.DescriptorMap,
80+
recipients: ITransactionRecipient[],
81+
network: utxolib.Network
82+
): BaseParsedTransactionOutputs<bigint, BaseOutput<bigint>> {
83+
return toBaseParsedTransactionOutputs(
84+
parseOutputsWithPsbt(
85+
psbt,
86+
descriptorMap,
87+
recipients.map((r) => toParsedOutput(r, psbt.network))
88+
),
89+
network
90+
);
91+
}
92+
93+
export type ParsedDescriptorTransaction<TAmount extends number | bigint> = BaseParsedTransaction<
94+
TAmount,
95+
BaseOutput<TAmount>
96+
>;
97+
98+
export function parse(
99+
coin: AbstractUtxoCoin,
100+
wallet: IDescriptorWallet,
101+
params: ParseTransactionOptions<number | bigint>
102+
): ParsedDescriptorTransaction<bigint> {
103+
if (params.txParams.allowExternalChangeAddress) {
104+
throw new Error('allowExternalChangeAddress is not supported for descriptor wallets');
105+
}
106+
if (params.txParams.changeAddress) {
107+
throw new Error('changeAddress is not supported for descriptor wallets');
108+
}
109+
const keychains = params.verification?.keychains;
110+
if (!keychains || !UtxoNamedKeychains.is(keychains)) {
111+
throw new Error('keychain is required for descriptor wallets');
112+
}
113+
const { recipients } = params.txParams;
114+
if (!recipients) {
115+
throw new Error('recipients is required');
116+
}
117+
const psbt = coin.decodeTransactionFromPrebuild(params.txPrebuild);
118+
if (!(psbt instanceof utxolib.bitgo.UtxoPsbt)) {
119+
throw new Error('expected psbt to be an instance of UtxoPsbt');
120+
}
121+
const walletKeys = toBip32Triple(keychains);
122+
const descriptorMap = getDescriptorMapFromWallet(wallet, walletKeys, getPolicyForEnv(params.wallet.bitgo.env));
123+
return {
124+
...toBaseParsedTransactionOutputsFromPsbt(psbt, descriptorMap, recipients, psbt.network),
125+
keychains,
126+
keySignatures: getKeySignatures(wallet) ?? {},
127+
customChange: undefined,
128+
needsCustomChangeKeySignatureVerification: false,
129+
};
130+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { AbstractUtxoCoin, BaseOutput, BaseParsedTransaction, ParseTransactionOptions } from '../../abstractUtxoCoin';
2+
import { parse, ParsedDescriptorTransaction } from './parse';
3+
import { IDescriptorWallet } from '../../descriptor/descriptorWallet';
4+
5+
type AmountType = 'number' | 'bigint' | 'string';
6+
7+
function toAmountType(v: number | bigint | string, t: AmountType): number | bigint | string {
8+
switch (t) {
9+
case 'number':
10+
return Number(v);
11+
case 'bigint':
12+
return BigInt(v);
13+
case 'string':
14+
return String(v);
15+
}
16+
}
17+
18+
type AmountTypeOptions = {
19+
amountTypeBaseOutput: AmountType;
20+
amountTypeAggregate: AmountType;
21+
};
22+
23+
function baseOutputToTNumber<TAmount extends number | bigint>(
24+
output: BaseOutput<bigint>,
25+
amountType: AmountType
26+
): BaseOutput<TAmount> {
27+
return {
28+
address: output.address,
29+
amount: toAmountType(output.amount, amountType) as TAmount,
30+
external: output.external,
31+
};
32+
}
33+
34+
function entryToTNumber<
35+
K extends keyof ParsedDescriptorTransaction<bigint>,
36+
V extends ParsedDescriptorTransaction<bigint>[K]
37+
>(k: K, v: V, params: AmountTypeOptions): [K, V] {
38+
switch (k) {
39+
case 'outputs':
40+
case 'changeOutputs':
41+
case 'explicitExternalOutputs':
42+
case 'implicitExternalOutputs':
43+
case 'missingOutputs':
44+
if (v === undefined) {
45+
return [k, v];
46+
}
47+
if (Array.isArray(v)) {
48+
return [k, v.map((o) => baseOutputToTNumber(o, params.amountTypeBaseOutput)) as V];
49+
}
50+
throw new Error('expected array');
51+
case 'explicitExternalSpendAmount':
52+
case 'implicitExternalSpendAmount':
53+
if (typeof v !== 'bigint') {
54+
throw new Error('expected bigint');
55+
}
56+
return [k, toAmountType(v, params.amountTypeAggregate) as V];
57+
default:
58+
return [k, v];
59+
}
60+
}
61+
62+
export function parsedDescriptorTransactionToTNumber<TAmount extends number | bigint, TOutput>(
63+
obj: ParsedDescriptorTransaction<bigint>,
64+
params: AmountTypeOptions
65+
): BaseParsedTransaction<TAmount, TOutput> {
66+
return Object.fromEntries(
67+
Object.entries(obj).map(([k, v]) => entryToTNumber(k as keyof ParsedDescriptorTransaction<bigint>, v, params))
68+
) as BaseParsedTransaction<TAmount, TOutput>;
69+
}
70+
71+
export function parseToAmountType<TAmount extends number | bigint>(
72+
coin: AbstractUtxoCoin,
73+
wallet: IDescriptorWallet,
74+
params: ParseTransactionOptions<TAmount>
75+
): BaseParsedTransaction<TAmount, BaseOutput<string>> {
76+
return parsedDescriptorTransactionToTNumber<TAmount, BaseOutput<string>>(parse(coin, wallet, params), {
77+
amountTypeAggregate: coin.amountType,
78+
amountTypeBaseOutput: 'string',
79+
});
80+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type Recipient = {
2+
address: string;
3+
amount: bigint;
4+
};

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { AbstractUtxoCoin, ParsedTransaction, ParseTransactionOptions } from '..
22

33
import { isDescriptorWallet } from '../descriptor';
44

5+
import * as descriptor from './descriptor';
56
import * as fixedScript from './fixedScript';
67

78
export async function parseTransaction<TNumber extends bigint | number>(
89
coin: AbstractUtxoCoin,
910
params: ParseTransactionOptions<TNumber>
1011
): Promise<ParsedTransaction<TNumber>> {
1112
if (isDescriptorWallet(params.wallet)) {
12-
throw new Error('Descriptor wallets are not supported');
13+
return descriptor.parseToAmountType(coin, params.wallet, params);
1314
} else {
1415
return fixedScript.parseTransaction(coin, params);
1516
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ export function fromExtendedAddressFormat(extendedAddress: string): { address: s
2222
return { address: extendedAddress };
2323
}
2424

25+
export function fromExtendedAddressFormatToScript(extendedAddress: string, network: utxolib.Network): Buffer {
26+
const result = fromExtendedAddressFormat(extendedAddress);
27+
if ('script' in result) {
28+
return Buffer.from(result.script, 'hex');
29+
}
30+
return utxolib.addressFormat.toOutputScriptTryFormats(result.address, network);
31+
}
32+
2533
/**
2634
* Convert a script or address to the extended address format.
2735
* @param script

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

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

3+
import { TransactionExplanation } from '../../../src';
34
import { explainPsbt } from '../../../src/transaction/descriptor';
45
import { mockPsbtDefaultWithDescriptorTemplate } from '../../core/descriptor/psbt/mock.utils';
56
import { getDescriptorMap } from '../../core/descriptor/descriptor.utils';
6-
import { getFixture } from '../../core/fixtures.utils';
77
import { getKeyTriple } from '../../core/key.utils';
8-
import { TransactionExplanation } from '../../../src';
9-
10-
async function assertEqualFixture(name: string, v: unknown) {
11-
assert.deepStrictEqual(v, await getFixture(__dirname + '/fixtures/' + name, v));
12-
}
8+
import { assertEqualFixture } from './fixtures.utils';
139

1410
function assertSignatureCount(expl: TransactionExplanation, signatures: number, inputSignatures: number[]) {
1511
assert.deepStrictEqual(expl.signatures, signatures);

0 commit comments

Comments
 (0)