Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import assert from 'assert';

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

import { UtxoCoinSpecific, VerifyAddressOptions } from '../abstractUtxoCoin';
import { getNetworkFromCoinName, UtxoCoinName } from '../names';
import { UtxoCoinName } from '../names';

class DescriptorAddressMismatchError extends Error {
constructor(descriptor: Descriptor, index: number, derivedAddress: string, expectedAddress: string) {
Expand All @@ -18,7 +16,7 @@ class DescriptorAddressMismatchError extends Error {
export function assertDescriptorWalletAddress(
coinName: UtxoCoinName,
params: VerifyAddressOptions<UtxoCoinSpecific>,
descriptors: DescriptorMap
descriptors: descriptorWallet.DescriptorMap
): void {
assert(params.coinSpecific);
assert('descriptorName' in params.coinSpecific);
Expand All @@ -35,8 +33,7 @@ export function assertDescriptorWalletAddress(
);
}
const derivedScript = Buffer.from(descriptor.atDerivationIndex(params.index).scriptPubkey());
const network = getNetworkFromCoinName(coinName);
const derivedAddress = utxolib.address.fromOutputScript(derivedScript, network);
const derivedAddress = address.fromOutputScriptWithCoin(derivedScript, coinName);
if (params.address !== derivedAddress) {
throw new DescriptorAddressMismatchError(descriptor, params.index, derivedAddress, params.address);
}
Expand Down
2 changes: 1 addition & 1 deletion modules/abstract-utxo/src/descriptor/descriptorWallet.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as t from 'io-ts';
import { IWallet, WalletCoinSpecific } from '@bitgo/sdk-core';
import { DescriptorMap } from '@bitgo/utxo-core/descriptor';

import { UtxoWallet, UtxoWalletData } from '../wallet';
import type { DescriptorMap } from '../wasmUtil';

import { NamedDescriptor } from './NamedDescriptor';
import { DescriptorValidationPolicy, KeyTriple, toDescriptorMapValidate } from './validatePolicy';
Expand Down
2 changes: 1 addition & 1 deletion modules/abstract-utxo/src/descriptor/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { Miniscript, Descriptor } from '@bitgo/wasm-utxo';
export { DescriptorMap } from '@bitgo/utxo-core/descriptor';
export type { DescriptorMap } from '../wasmUtil';
export { assertDescriptorWalletAddress } from './assertDescriptorWalletAddress';
export {
NamedDescriptor,
Expand Down
6 changes: 4 additions & 2 deletions modules/abstract-utxo/src/descriptor/validatePolicy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { EnvironmentName, Triple } from '@bitgo/sdk-core';
import * as utxolib from '@bitgo/utxo-lib';
import { DescriptorMap, toDescriptorMap } from '@bitgo/utxo-core/descriptor';
import { descriptorWallet } from '@bitgo/wasm-utxo';

import type { DescriptorMap } from '../wasmUtil';

import { parseDescriptor } from './builder';
import { hasValidSignature, NamedDescriptor, NamedDescriptorNative, toNamedDescriptorNative } from './NamedDescriptor';
Expand Down Expand Up @@ -91,7 +93,7 @@ export function toDescriptorMapValidate(
toNamedDescriptorNative(v, 'derivable')
);
assertDescriptorPolicy(namedDescriptorsNative, policy, walletKeys);
return toDescriptorMap(namedDescriptorsNative);
return descriptorWallet.toDescriptorMap(namedDescriptorsNative);
}

export function getPolicyForEnv(env: EnvironmentName): DescriptorValidationPolicy {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BIP32Interface, bip32 } from '@bitgo/secp256k1';
import * as utxolib from '@bitgo/utxo-lib';
import { Psbt } from '@bitgo/wasm-utxo';
import { BaseCoin } from '@bitgo/sdk-core';

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

function createHalfSignedFromPsbt(psbt: utxolib.Psbt): OfflineVaultHalfSigned {
return { halfSigned: { txHex: psbt.toHex() } };
function createHalfSignedFromPsbt(psbt: Psbt): OfflineVaultHalfSigned {
return { halfSigned: { txHex: Buffer.from(psbt.serialize()).toString('hex') } };
}

export function createHalfSigned(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as utxolib from '@bitgo/utxo-lib';
import * as t from 'io-ts';
import { Psbt } from '@bitgo/wasm-utxo';
import type { BIP32Interface } from '@bitgo/utxo-lib';

import { DescriptorMap, NamedDescriptor } from '../../descriptor';
import { OfflineVaultSignable, toKeyTriple } from '../OfflineVaultSignable';
Expand All @@ -11,7 +12,8 @@ import {
} from '../../descriptor/validatePolicy';
import { explainPsbt, signPsbt } from '../../transaction/descriptor';
import { TransactionExplanation } from '../TransactionExplanation';
import { getNetworkFromCoinName, UtxoCoinName } from '../../names';
import { UtxoCoinName } from '../../names';
import { toWasmPsbt } from '../../wasmUtil';

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

export function getHalfSignedPsbt(
tx: DescriptorTransaction,
prv: utxolib.BIP32Interface,
coinName: UtxoCoinName
): utxolib.Psbt {
const network = getNetworkFromCoinName(coinName);
const psbt = utxolib.bitgo.createPsbtDecode(tx.coinSpecific.txHex, network);
export function getHalfSignedPsbt(tx: DescriptorTransaction, prv: BIP32Interface, coinName: UtxoCoinName): Psbt {
const psbt = toWasmPsbt(Buffer.from(tx.coinSpecific.txHex, 'hex'));
const descriptorMap = getDescriptorsFromDescriptorTransaction(tx);
signPsbt(psbt, descriptorMap, prv, { onUnknownInput: 'throw' });
return psbt;
Expand All @@ -48,10 +45,9 @@ export function getTransactionExplanationFromPsbt(
tx: DescriptorTransaction,
coinName: UtxoCoinName
): TransactionExplanation<string> {
const network = getNetworkFromCoinName(coinName);
const psbt = utxolib.bitgo.createPsbtDecode(tx.coinSpecific.txHex, network);
const psbt = toWasmPsbt(Buffer.from(tx.coinSpecific.txHex, 'hex'));
const descriptorMap = getDescriptorsFromDescriptorTransaction(tx);
const { outputs, changeOutputs, fee } = explainPsbt(psbt, descriptorMap);
const { outputs, changeOutputs, fee } = explainPsbt(psbt, descriptorMap, coinName);
return {
outputs,
changeOutputs,
Expand Down
42 changes: 19 additions & 23 deletions modules/abstract-utxo/src/transaction/descriptor/explainPsbt.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,49 @@
import * as utxolib from '@bitgo/utxo-lib';
import { ITransactionRecipient } from '@bitgo/sdk-core';
import * as coreDescriptors from '@bitgo/utxo-core/descriptor';
import { Psbt, descriptorWallet } from '@bitgo/wasm-utxo';

import { toExtendedAddressFormat } from '../recipient';
import type { TransactionExplanationDescriptor } from '../fixedScript/explainTransaction';
import { getCoinName, UtxoCoinName } from '../../names';
import { UtxoCoinName } from '../../names';
import { sumValues } from '../../wasmUtil';

function toRecipient(output: coreDescriptors.ParsedOutput, coinName: UtxoCoinName): ITransactionRecipient {
function toRecipient(output: descriptorWallet.ParsedOutput, coinName: UtxoCoinName): ITransactionRecipient {
const address = output.address ?? `scriptPubKey:${Buffer.from(output.script).toString('hex')}`;
return {
address: toExtendedAddressFormat(output.script, coinName),
address,
amount: output.value.toString(),
};
}

function sumValues(arr: { value: bigint }[]): bigint {
return arr.reduce((sum, e) => sum + e.value, BigInt(0));
}

function getInputSignaturesForInputIndex(psbt: utxolib.bitgo.UtxoPsbt, inputIndex: number): number {
const { partialSig } = psbt.data.inputs[inputIndex];
if (!partialSig) {
function getInputSignaturesForInputIndex(psbt: Psbt, inputIndex: number): number {
if (!psbt.hasPartialSignatures(inputIndex)) {
return 0;
}
return partialSig.reduce((agg, p) => {
const valid = psbt.validateSignaturesOfInputCommon(inputIndex, p.pubkey);
const partialSigs = psbt.getPartialSignatures(inputIndex);
return partialSigs.reduce((agg, p) => {
const valid = psbt.validateSignatureAtInput(inputIndex, p.pubkey);
return agg + (valid ? 1 : 0);
}, 0);
}

function getInputSignatures(psbt: utxolib.bitgo.UtxoPsbt): number[] {
return psbt.data.inputs.map((_, i) => getInputSignaturesForInputIndex(psbt, i));
function getInputSignatures(psbt: Psbt): number[] {
return Array.from({ length: psbt.inputCount() }, (_, i) => getInputSignaturesForInputIndex(psbt, i));
}

export function explainPsbt(
psbt: utxolib.bitgo.UtxoPsbt,
descriptors: coreDescriptors.DescriptorMap
psbt: Psbt,
descriptors: descriptorWallet.DescriptorMap,
coinName: UtxoCoinName
): TransactionExplanationDescriptor {
const parsedTransaction = coreDescriptors.parse(psbt, descriptors, psbt.network);
const parsedTransaction = descriptorWallet.parse(psbt, descriptors, coinName);
const { inputs, outputs } = parsedTransaction;
const externalOutputs = outputs.filter((o) => o.scriptId === undefined);
const changeOutputs = outputs.filter((o) => o.scriptId !== undefined);
const fee = sumValues(inputs) - sumValues(outputs);
const inputSignatures = getInputSignatures(psbt);
const coinName = getCoinName(psbt.network);
return {
inputSignatures,
signatures: inputSignatures.reduce((a, b) => Math.min(a, b), Infinity),
locktime: psbt.locktime,
id: psbt.getUnsignedTx().getId(),
locktime: psbt.lockTime(),
id: psbt.unsignedTxId(),
outputs: externalOutputs.map((o) => toRecipient(o, coinName)),
outputAmount: sumValues(externalOutputs).toString(),
changeOutputs: changeOutputs.map((o) => toRecipient(o, coinName)),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { DescriptorMap } from '@bitgo/utxo-core/descriptor';
export type { DescriptorMap } from '../../wasmUtil';
export { explainPsbt } from './explainPsbt';
export { parse } from './parse';
export { parseToAmountType } from './parseToAmountType';
Expand Down
49 changes: 28 additions & 21 deletions modules/abstract-utxo/src/transaction/descriptor/parse.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as utxolib from '@bitgo/utxo-lib';
import { ITransactionRecipient } from '@bitgo/sdk-core';
import * as coreDescriptors from '@bitgo/utxo-core/descriptor';
import { Psbt, descriptorWallet } from '@bitgo/wasm-utxo';

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

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

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

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

function parseOutputsWithPsbt(
psbt: utxolib.bitgo.UtxoPsbt,
descriptorMap: coreDescriptors.DescriptorMap,
recipientOutputs: RecipientOutput[]
psbt: Psbt,
descriptorMap: descriptorWallet.DescriptorMap,
recipientOutputs: RecipientOutput[],
coinName: UtxoCoinName
): ParsedOutputs {
const parsed = coreDescriptors.parse(psbt, descriptorMap, psbt.network);
const changeOutputs = parsed.outputs.filter((o) => o.scriptId !== undefined);
const outputDiffs = outputDifferencesWithExpected(parsed.outputs, recipientOutputs);
const parsed = descriptorWallet.parse(psbt, descriptorMap, coinName);
const outputs: ParsedOutput[] = parsed.outputs.map((output) => ({
...output,
script: Buffer.from(output.script),
}));
const changeOutputs = outputs.filter((o) => o.scriptId !== undefined);
const outputDiffs = outputDifferencesWithExpected(outputs, recipientOutputs);
return {
outputs: parsed.outputs,
outputs,
changeOutputs,
...outputDiffs,
};
}

function sumValues(arr: { value: bigint }[]): bigint {
return arr.reduce((sum, e) => sum + e.value, BigInt(0));
}

function toBaseOutputs(outputs: ParsedOutput[], coinName: UtxoCoinName): BaseOutput<bigint>[];
function toBaseOutputs(outputs: RecipientOutput[], coinName: UtxoCoinName): BaseOutput<bigint | 'max'>[];
function toBaseOutputs(
Expand Down Expand Up @@ -85,16 +87,18 @@ function toBaseParsedTransactionOutputs(
}

export function toBaseParsedTransactionOutputsFromPsbt(
psbt: utxolib.bitgo.UtxoPsbt,
descriptorMap: coreDescriptors.DescriptorMap,
psbt: Psbt | UtxoLibPsbt | Uint8Array,
descriptorMap: descriptorWallet.DescriptorMap,
recipients: ITransactionRecipient[],
coinName: UtxoCoinName
): ParsedOutputsBigInt {
const wasmPsbt = toWasmPsbt(psbt);
return toBaseParsedTransactionOutputs(
parseOutputsWithPsbt(
psbt,
wasmPsbt,
descriptorMap,
recipients.map((r) => toRecipientOutput(r, coinName))
recipients.map((r) => toRecipientOutput(r, coinName)),
coinName
),
coinName
);
Expand Down Expand Up @@ -125,13 +129,16 @@ export function parse(
throw new Error('recipients is required');
}
const psbt = coin.decodeTransactionFromPrebuild(params.txPrebuild);
if (!(psbt instanceof utxolib.bitgo.UtxoPsbt)) {
throw new Error('expected psbt to be an instance of UtxoPsbt');
let wasmPsbt: Psbt;
try {
wasmPsbt = toWasmPsbt(psbt as Psbt | UtxoLibPsbt | Uint8Array);
} catch (e) {
throw new Error(`expected psbt to be a wasm-utxo or utxo-lib PSBT: ${e instanceof Error ? e.message : e}`);
}
const walletKeys = toBip32Triple(keychains);
const descriptorMap = getDescriptorMapFromWallet(wallet, walletKeys, getPolicyForEnv(params.wallet.bitgo.env));
return {
...toBaseParsedTransactionOutputsFromPsbt(psbt, descriptorMap, recipients, coin.name),
...toBaseParsedTransactionOutputsFromPsbt(wasmPsbt, descriptorMap, recipients, coin.name),
keychains,
keySignatures: getKeySignatures(wallet) ?? {},
customChange: undefined,
Expand Down
43 changes: 23 additions & 20 deletions modules/abstract-utxo/src/transaction/descriptor/signPsbt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as utxolib from '@bitgo/utxo-lib';
import { DescriptorMap, findDescriptorForInput } from '@bitgo/utxo-core/descriptor';
import { Psbt, descriptorWallet } from '@bitgo/wasm-utxo';

import type { SignerKey } from '../../wasmUtil';

export class ErrorUnknownInput extends Error {
constructor(public vin: number) {
Expand All @@ -14,33 +15,35 @@ export class ErrorUnknownInput extends Error {
* found in the descriptor map, the behavior is determined by the `onUnknownInput`
* parameter.
*
*
* @param tx - psbt to sign
* @param descriptorMap - map of input index to descriptor
* @param signerKeychain - key to sign with
* @param psbt - psbt to sign
* @param descriptorMap - map of descriptor name to descriptor
* @param signerKey - key to sign with (BIP32 or ECPair)
* @param params - onUnknownInput: 'throw' | 'skip' | 'sign'.
* Determines what to do when an input is not found in the
* descriptor map.
*/
export function signPsbt(
tx: utxolib.Psbt,
descriptorMap: DescriptorMap,
signerKeychain: utxolib.BIP32Interface,
psbt: Psbt,
descriptorMap: descriptorWallet.DescriptorMap,
signerKey: SignerKey,
params: {
onUnknownInput: 'throw' | 'skip' | 'sign';
}
): void {
for (const [vin, input] of tx.data.inputs.entries()) {
if (!findDescriptorForInput(input, descriptorMap)) {
switch (params.onUnknownInput) {
case 'skip':
continue;
case 'throw':
throw new ErrorUnknownInput(vin);
case 'sign':
break;
}
const inputs = psbt.getInputs();
const unknownInputs = inputs
.map((input, vin) => ({ input, vin }))
.filter(({ input }) => !descriptorWallet.findDescriptorForInput(input, descriptorMap));

if (unknownInputs.length > 0) {
switch (params.onUnknownInput) {
case 'skip':
return;
case 'throw':
throw new ErrorUnknownInput(unknownInputs[0].vin);
case 'sign':
break;
}
tx.signInputHD(vin, signerKeychain);
}
descriptorWallet.signWithKey(psbt, signerKey);
}
Loading