Skip to content

Commit 76711a2

Browse files
Merge pull request #8038 from BitGo/BTC-2866.bump-wasm-utxo.descriptor-utxo-wasm
feat(abstract-utxo): switch descriptor wallet stack to wasm-utxo primitives
2 parents f439084 + 473b076 commit 76711a2

18 files changed

Lines changed: 215 additions & 131 deletions

File tree

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

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

3-
import * as utxolib from '@bitgo/utxo-lib';
4-
import { Descriptor } from '@bitgo/wasm-utxo';
5-
import { DescriptorMap } from '@bitgo/utxo-core/descriptor';
3+
import { Descriptor, address, descriptorWallet } from '@bitgo/wasm-utxo';
64

75
import { UtxoCoinSpecific, VerifyAddressOptions } from '../abstractUtxoCoin';
8-
import { getNetworkFromCoinName, UtxoCoinName } from '../names';
6+
import { UtxoCoinName } from '../names';
97

108
class DescriptorAddressMismatchError extends Error {
119
constructor(descriptor: Descriptor, index: number, derivedAddress: string, expectedAddress: string) {
@@ -18,7 +16,7 @@ class DescriptorAddressMismatchError extends Error {
1816
export function assertDescriptorWalletAddress(
1917
coinName: UtxoCoinName,
2018
params: VerifyAddressOptions<UtxoCoinSpecific>,
21-
descriptors: DescriptorMap
19+
descriptors: descriptorWallet.DescriptorMap
2220
): void {
2321
assert(params.coinSpecific);
2422
assert('descriptorName' in params.coinSpecific);
@@ -35,8 +33,7 @@ export function assertDescriptorWalletAddress(
3533
);
3634
}
3735
const derivedScript = Buffer.from(descriptor.atDerivationIndex(params.index).scriptPubkey());
38-
const network = getNetworkFromCoinName(coinName);
39-
const derivedAddress = utxolib.address.fromOutputScript(derivedScript, network);
36+
const derivedAddress = address.fromOutputScriptWithCoin(derivedScript, coinName);
4037
if (params.address !== derivedAddress) {
4138
throw new DescriptorAddressMismatchError(descriptor, params.index, derivedAddress, params.address);
4239
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as t from 'io-ts';
22
import { IWallet, WalletCoinSpecific } from '@bitgo/sdk-core';
3-
import { DescriptorMap } from '@bitgo/utxo-core/descriptor';
43

54
import { UtxoWallet, UtxoWalletData } from '../wallet';
5+
import type { DescriptorMap } from '../wasmUtil';
66

77
import { NamedDescriptor } from './NamedDescriptor';
88
import { DescriptorValidationPolicy, KeyTriple, toDescriptorMapValidate } from './validatePolicy';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export { Miniscript, Descriptor } from '@bitgo/wasm-utxo';
2-
export { DescriptorMap } from '@bitgo/utxo-core/descriptor';
2+
export type { DescriptorMap } from '../wasmUtil';
33
export { assertDescriptorWalletAddress } from './assertDescriptorWalletAddress';
44
export {
55
NamedDescriptor,

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { EnvironmentName, Triple } from '@bitgo/sdk-core';
22
import * as utxolib from '@bitgo/utxo-lib';
3-
import { DescriptorMap, toDescriptorMap } from '@bitgo/utxo-core/descriptor';
3+
import { descriptorWallet } from '@bitgo/wasm-utxo';
4+
5+
import type { DescriptorMap } from '../wasmUtil';
46

57
import { parseDescriptor } from './builder';
68
import { hasValidSignature, NamedDescriptor, NamedDescriptorNative, toNamedDescriptorNative } from './NamedDescriptor';
@@ -91,7 +93,7 @@ export function toDescriptorMapValidate(
9193
toNamedDescriptorNative(v, 'derivable')
9294
);
9395
assertDescriptorPolicy(namedDescriptorsNative, policy, walletKeys);
94-
return toDescriptorMap(namedDescriptorsNative);
96+
return descriptorWallet.toDescriptorMap(namedDescriptorsNative);
9597
}
9698

9799
export function getPolicyForEnv(env: EnvironmentName): DescriptorValidationPolicy {

modules/abstract-utxo/src/offlineVault/OfflineVaultHalfSigned.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BIP32Interface, bip32 } from '@bitgo/secp256k1';
2-
import * as utxolib from '@bitgo/utxo-lib';
2+
import { Psbt } from '@bitgo/wasm-utxo';
33
import { BaseCoin } from '@bitgo/sdk-core';
44

55
import { UtxoCoinName } from '../names';
@@ -11,8 +11,8 @@ export type OfflineVaultHalfSigned = {
1111
halfSigned: { txHex: string };
1212
};
1313

14-
function createHalfSignedFromPsbt(psbt: utxolib.Psbt): OfflineVaultHalfSigned {
15-
return { halfSigned: { txHex: psbt.toHex() } };
14+
function createHalfSignedFromPsbt(psbt: Psbt): OfflineVaultHalfSigned {
15+
return { halfSigned: { txHex: Buffer.from(psbt.serialize()).toString('hex') } };
1616
}
1717

1818
export function createHalfSigned(

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

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import * as utxolib from '@bitgo/utxo-lib';
21
import * as t from 'io-ts';
2+
import { Psbt } from '@bitgo/wasm-utxo';
3+
import type { BIP32Interface } from '@bitgo/utxo-lib';
34

45
import { DescriptorMap, NamedDescriptor } from '../../descriptor';
56
import { OfflineVaultSignable, toKeyTriple } from '../OfflineVaultSignable';
@@ -11,7 +12,8 @@ import {
1112
} from '../../descriptor/validatePolicy';
1213
import { explainPsbt, signPsbt } from '../../transaction/descriptor';
1314
import { TransactionExplanation } from '../TransactionExplanation';
14-
import { getNetworkFromCoinName, UtxoCoinName } from '../../names';
15+
import { UtxoCoinName } from '../../names';
16+
import { toWasmPsbt } from '../../wasmUtil';
1517

1618
export const DescriptorTransaction = t.intersection(
1719
[OfflineVaultSignable, t.type({ descriptors: t.array(NamedDescriptor) })],
@@ -32,13 +34,8 @@ export function getDescriptorsFromDescriptorTransaction(tx: DescriptorTransactio
3234
return toDescriptorMapValidate(descriptors, pubkeys, policy);
3335
}
3436

35-
export function getHalfSignedPsbt(
36-
tx: DescriptorTransaction,
37-
prv: utxolib.BIP32Interface,
38-
coinName: UtxoCoinName
39-
): utxolib.Psbt {
40-
const network = getNetworkFromCoinName(coinName);
41-
const psbt = utxolib.bitgo.createPsbtDecode(tx.coinSpecific.txHex, network);
37+
export function getHalfSignedPsbt(tx: DescriptorTransaction, prv: BIP32Interface, coinName: UtxoCoinName): Psbt {
38+
const psbt = toWasmPsbt(Buffer.from(tx.coinSpecific.txHex, 'hex'));
4239
const descriptorMap = getDescriptorsFromDescriptorTransaction(tx);
4340
signPsbt(psbt, descriptorMap, prv, { onUnknownInput: 'throw' });
4441
return psbt;
@@ -48,10 +45,9 @@ export function getTransactionExplanationFromPsbt(
4845
tx: DescriptorTransaction,
4946
coinName: UtxoCoinName
5047
): TransactionExplanation<string> {
51-
const network = getNetworkFromCoinName(coinName);
52-
const psbt = utxolib.bitgo.createPsbtDecode(tx.coinSpecific.txHex, network);
48+
const psbt = toWasmPsbt(Buffer.from(tx.coinSpecific.txHex, 'hex'));
5349
const descriptorMap = getDescriptorsFromDescriptorTransaction(tx);
54-
const { outputs, changeOutputs, fee } = explainPsbt(psbt, descriptorMap);
50+
const { outputs, changeOutputs, fee } = explainPsbt(psbt, descriptorMap, coinName);
5551
return {
5652
outputs,
5753
changeOutputs,

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

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,49 @@
1-
import * as utxolib from '@bitgo/utxo-lib';
21
import { ITransactionRecipient } from '@bitgo/sdk-core';
3-
import * as coreDescriptors from '@bitgo/utxo-core/descriptor';
2+
import { Psbt, descriptorWallet } from '@bitgo/wasm-utxo';
43

5-
import { toExtendedAddressFormat } from '../recipient';
64
import type { TransactionExplanationDescriptor } from '../fixedScript/explainTransaction';
7-
import { getCoinName, UtxoCoinName } from '../../names';
5+
import { UtxoCoinName } from '../../names';
6+
import { sumValues } from '../../wasmUtil';
87

9-
function toRecipient(output: coreDescriptors.ParsedOutput, coinName: UtxoCoinName): ITransactionRecipient {
8+
function toRecipient(output: descriptorWallet.ParsedOutput, coinName: UtxoCoinName): ITransactionRecipient {
9+
const address = output.address ?? `scriptPubKey:${Buffer.from(output.script).toString('hex')}`;
1010
return {
11-
address: toExtendedAddressFormat(output.script, coinName),
11+
address,
1212
amount: output.value.toString(),
1313
};
1414
}
1515

16-
function sumValues(arr: { value: bigint }[]): bigint {
17-
return arr.reduce((sum, e) => sum + e.value, BigInt(0));
18-
}
19-
20-
function getInputSignaturesForInputIndex(psbt: utxolib.bitgo.UtxoPsbt, inputIndex: number): number {
21-
const { partialSig } = psbt.data.inputs[inputIndex];
22-
if (!partialSig) {
16+
function getInputSignaturesForInputIndex(psbt: Psbt, inputIndex: number): number {
17+
if (!psbt.hasPartialSignatures(inputIndex)) {
2318
return 0;
2419
}
25-
return partialSig.reduce((agg, p) => {
26-
const valid = psbt.validateSignaturesOfInputCommon(inputIndex, p.pubkey);
20+
const partialSigs = psbt.getPartialSignatures(inputIndex);
21+
return partialSigs.reduce((agg, p) => {
22+
const valid = psbt.validateSignatureAtInput(inputIndex, p.pubkey);
2723
return agg + (valid ? 1 : 0);
2824
}, 0);
2925
}
3026

31-
function getInputSignatures(psbt: utxolib.bitgo.UtxoPsbt): number[] {
32-
return psbt.data.inputs.map((_, i) => getInputSignaturesForInputIndex(psbt, i));
27+
function getInputSignatures(psbt: Psbt): number[] {
28+
return Array.from({ length: psbt.inputCount() }, (_, i) => getInputSignaturesForInputIndex(psbt, i));
3329
}
3430

3531
export function explainPsbt(
36-
psbt: utxolib.bitgo.UtxoPsbt,
37-
descriptors: coreDescriptors.DescriptorMap
32+
psbt: Psbt,
33+
descriptors: descriptorWallet.DescriptorMap,
34+
coinName: UtxoCoinName
3835
): TransactionExplanationDescriptor {
39-
const parsedTransaction = coreDescriptors.parse(psbt, descriptors, psbt.network);
36+
const parsedTransaction = descriptorWallet.parse(psbt, descriptors, coinName);
4037
const { inputs, outputs } = parsedTransaction;
4138
const externalOutputs = outputs.filter((o) => o.scriptId === undefined);
4239
const changeOutputs = outputs.filter((o) => o.scriptId !== undefined);
4340
const fee = sumValues(inputs) - sumValues(outputs);
4441
const inputSignatures = getInputSignatures(psbt);
45-
const coinName = getCoinName(psbt.network);
4642
return {
4743
inputSignatures,
4844
signatures: inputSignatures.reduce((a, b) => Math.min(a, b), Infinity),
49-
locktime: psbt.locktime,
50-
id: psbt.getUnsignedTx().getId(),
45+
locktime: psbt.lockTime(),
46+
id: psbt.unsignedTxId(),
5147
outputs: externalOutputs.map((o) => toRecipient(o, coinName)),
5248
outputAmount: sumValues(externalOutputs).toString(),
5349
changeOutputs: changeOutputs.map((o) => toRecipient(o, coinName)),

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { DescriptorMap } from '@bitgo/utxo-core/descriptor';
1+
export type { DescriptorMap } from '../../wasmUtil';
22
export { explainPsbt } from './explainPsbt';
33
export { parse } from './parse';
44
export { parseToAmountType } from './parseToAmountType';

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

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import * as utxolib from '@bitgo/utxo-lib';
21
import { ITransactionRecipient } from '@bitgo/sdk-core';
3-
import * as coreDescriptors from '@bitgo/utxo-core/descriptor';
2+
import { Psbt, descriptorWallet } from '@bitgo/wasm-utxo';
43

54
import { AbstractUtxoCoin, ParseTransactionOptions } from '../../abstractUtxoCoin';
65
import { BaseOutput, BaseParsedTransaction, BaseParsedTransactionOutputs } from '../types';
@@ -10,8 +9,9 @@ import { IDescriptorWallet } from '../../descriptor/descriptorWallet';
109
import { fromExtendedAddressFormatToScript, toExtendedAddressFormat } from '../recipient';
1110
import { outputDifferencesWithExpected, OutputDifferenceWithExpected } from '../outputDifference';
1211
import { UtxoCoinName } from '../../names';
12+
import { sumValues, toWasmPsbt, UtxoLibPsbt } from '../../wasmUtil';
1313

14-
type ParsedOutput = coreDescriptors.ParsedOutput;
14+
type ParsedOutput = Omit<descriptorWallet.ParsedOutput, 'script'> & { script: Buffer };
1515

1616
export type RecipientOutput = Omit<ParsedOutput, 'value'> & {
1717
value: bigint | 'max';
@@ -22,6 +22,7 @@ function toRecipientOutput(recipient: ITransactionRecipient, coinName: UtxoCoinN
2222
address: recipient.address,
2323
value: recipient.amount === 'max' ? 'max' : BigInt(recipient.amount),
2424
script: fromExtendedAddressFormatToScript(recipient.address, coinName),
25+
scriptId: undefined, // Recipients are external outputs
2526
};
2627
}
2728

@@ -32,24 +33,25 @@ type ParsedOutputs = OutputDifferenceWithExpected<ParsedOutput, RecipientOutput>
3233
};
3334

3435
function parseOutputsWithPsbt(
35-
psbt: utxolib.bitgo.UtxoPsbt,
36-
descriptorMap: coreDescriptors.DescriptorMap,
37-
recipientOutputs: RecipientOutput[]
36+
psbt: Psbt,
37+
descriptorMap: descriptorWallet.DescriptorMap,
38+
recipientOutputs: RecipientOutput[],
39+
coinName: UtxoCoinName
3840
): ParsedOutputs {
39-
const parsed = coreDescriptors.parse(psbt, descriptorMap, psbt.network);
40-
const changeOutputs = parsed.outputs.filter((o) => o.scriptId !== undefined);
41-
const outputDiffs = outputDifferencesWithExpected(parsed.outputs, recipientOutputs);
41+
const parsed = descriptorWallet.parse(psbt, descriptorMap, coinName);
42+
const outputs: ParsedOutput[] = parsed.outputs.map((output) => ({
43+
...output,
44+
script: Buffer.from(output.script),
45+
}));
46+
const changeOutputs = outputs.filter((o) => o.scriptId !== undefined);
47+
const outputDiffs = outputDifferencesWithExpected(outputs, recipientOutputs);
4248
return {
43-
outputs: parsed.outputs,
49+
outputs,
4450
changeOutputs,
4551
...outputDiffs,
4652
};
4753
}
4854

49-
function sumValues(arr: { value: bigint }[]): bigint {
50-
return arr.reduce((sum, e) => sum + e.value, BigInt(0));
51-
}
52-
5355
function toBaseOutputs(outputs: ParsedOutput[], coinName: UtxoCoinName): BaseOutput<bigint>[];
5456
function toBaseOutputs(outputs: RecipientOutput[], coinName: UtxoCoinName): BaseOutput<bigint | 'max'>[];
5557
function toBaseOutputs(
@@ -85,16 +87,18 @@ function toBaseParsedTransactionOutputs(
8587
}
8688

8789
export function toBaseParsedTransactionOutputsFromPsbt(
88-
psbt: utxolib.bitgo.UtxoPsbt,
89-
descriptorMap: coreDescriptors.DescriptorMap,
90+
psbt: Psbt | UtxoLibPsbt | Uint8Array,
91+
descriptorMap: descriptorWallet.DescriptorMap,
9092
recipients: ITransactionRecipient[],
9193
coinName: UtxoCoinName
9294
): ParsedOutputsBigInt {
95+
const wasmPsbt = toWasmPsbt(psbt);
9396
return toBaseParsedTransactionOutputs(
9497
parseOutputsWithPsbt(
95-
psbt,
98+
wasmPsbt,
9699
descriptorMap,
97-
recipients.map((r) => toRecipientOutput(r, coinName))
100+
recipients.map((r) => toRecipientOutput(r, coinName)),
101+
coinName
98102
),
99103
coinName
100104
);
@@ -125,13 +129,16 @@ export function parse(
125129
throw new Error('recipients is required');
126130
}
127131
const psbt = coin.decodeTransactionFromPrebuild(params.txPrebuild);
128-
if (!(psbt instanceof utxolib.bitgo.UtxoPsbt)) {
129-
throw new Error('expected psbt to be an instance of UtxoPsbt');
132+
let wasmPsbt: Psbt;
133+
try {
134+
wasmPsbt = toWasmPsbt(psbt as Psbt | UtxoLibPsbt | Uint8Array);
135+
} catch (e) {
136+
throw new Error(`expected psbt to be a wasm-utxo or utxo-lib PSBT: ${e instanceof Error ? e.message : e}`);
130137
}
131138
const walletKeys = toBip32Triple(keychains);
132139
const descriptorMap = getDescriptorMapFromWallet(wallet, walletKeys, getPolicyForEnv(params.wallet.bitgo.env));
133140
return {
134-
...toBaseParsedTransactionOutputsFromPsbt(psbt, descriptorMap, recipients, coin.name),
141+
...toBaseParsedTransactionOutputsFromPsbt(wasmPsbt, descriptorMap, recipients, coin.name),
135142
keychains,
136143
keySignatures: getKeySignatures(wallet) ?? {},
137144
customChange: undefined,
Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import * as utxolib from '@bitgo/utxo-lib';
2-
import { DescriptorMap, findDescriptorForInput } from '@bitgo/utxo-core/descriptor';
1+
import { Psbt, descriptorWallet } from '@bitgo/wasm-utxo';
2+
3+
import type { SignerKey } from '../../wasmUtil';
34

45
export class ErrorUnknownInput extends Error {
56
constructor(public vin: number) {
@@ -14,33 +15,35 @@ export class ErrorUnknownInput extends Error {
1415
* found in the descriptor map, the behavior is determined by the `onUnknownInput`
1516
* parameter.
1617
*
17-
*
18-
* @param tx - psbt to sign
19-
* @param descriptorMap - map of input index to descriptor
20-
* @param signerKeychain - key to sign with
18+
* @param psbt - psbt to sign
19+
* @param descriptorMap - map of descriptor name to descriptor
20+
* @param signerKey - key to sign with (BIP32 or ECPair)
2121
* @param params - onUnknownInput: 'throw' | 'skip' | 'sign'.
2222
* Determines what to do when an input is not found in the
2323
* descriptor map.
2424
*/
2525
export function signPsbt(
26-
tx: utxolib.Psbt,
27-
descriptorMap: DescriptorMap,
28-
signerKeychain: utxolib.BIP32Interface,
26+
psbt: Psbt,
27+
descriptorMap: descriptorWallet.DescriptorMap,
28+
signerKey: SignerKey,
2929
params: {
3030
onUnknownInput: 'throw' | 'skip' | 'sign';
3131
}
3232
): void {
33-
for (const [vin, input] of tx.data.inputs.entries()) {
34-
if (!findDescriptorForInput(input, descriptorMap)) {
35-
switch (params.onUnknownInput) {
36-
case 'skip':
37-
continue;
38-
case 'throw':
39-
throw new ErrorUnknownInput(vin);
40-
case 'sign':
41-
break;
42-
}
33+
const inputs = psbt.getInputs();
34+
const unknownInputs = inputs
35+
.map((input, vin) => ({ input, vin }))
36+
.filter(({ input }) => !descriptorWallet.findDescriptorForInput(input, descriptorMap));
37+
38+
if (unknownInputs.length > 0) {
39+
switch (params.onUnknownInput) {
40+
case 'skip':
41+
return;
42+
case 'throw':
43+
throw new ErrorUnknownInput(unknownInputs[0].vin);
44+
case 'sign':
45+
break;
4346
}
44-
tx.signInputHD(vin, signerKeychain);
4547
}
48+
descriptorWallet.signWithKey(psbt, signerKey);
4649
}

0 commit comments

Comments
 (0)