From 6518aa0a8be5fdda8178ec1b782cdcf2d0bbd78f Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 13:09:28 +0200 Subject: [PATCH 1/4] refactor(abstract-utxo): remove supportedTxFormats and supportedSdkBackends Both properties were hardcoded constants (legacy: false, utxolib: false). Inline the values at the call site: drop the dead psbt-format guard, make the legacy-format path an unconditional throw, and replace the backend availability check with a direct utxolib comparison. Refs: T1-3245, T1-3280 Co-authored-by: llm-git --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 3496d442e0..b66fa7b9cd 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -437,16 +437,6 @@ export abstract class AbstractUtxoCoin public readonly amountType: 'number' | 'bigint'; - protected supportedTxFormats: { psbt: boolean; legacy: boolean } = { - psbt: true, - legacy: false, - }; - - protected supportedSdkBackends: { utxolib: boolean; 'wasm-utxo': boolean } = { - utxolib: false, - 'wasm-utxo': true, - }; - protected constructor(bitgo: BitGoBase, amountType: 'number' | 'bigint' = 'number') { super(bitgo); this.amountType = amountType; @@ -637,25 +627,12 @@ export abstract class AbstractUtxoCoin } if (utxolib.bitgo.isPsbt(input)) { - if (this.supportedSdkBackends[decodeWith] !== true) { + if (decodeWith === 'utxolib') { throw new Error(`SDK support for decodeWith=${decodeWith} is not available on this environment.`); } - - if (!this.supportedTxFormats.psbt) { - throw new ErrorDeprecatedTxFormat('psbt'); - } return decodePsbtWith(input, this.name, decodeWith); } else { - // Legacy format transactions are deprecated. This will be an unconditional error in the future. - if (!this.supportedTxFormats.legacy) { - throw new ErrorDeprecatedTxFormat('legacy'); - } - if (decodeWith !== 'utxolib') { - console.error('received decodeWith hint %s, ignoring for legacy transaction', decodeWith); - } - return utxolib.bitgo.createTransactionFromBuffer(input, this.network, { - amountType: this.amountType, - }); + throw new ErrorDeprecatedTxFormat('legacy'); } } From 3863eec1d3bd87dea125245c26f200fb25b1ca8c Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 13:18:14 +0200 Subject: [PATCH 2/4] refactor(abstract-utxo): remove unreachable utxolib and legacy tx code paths Delete signLegacyTransaction.ts and signPsbtUtxolib.ts entirely. Collapse decodePsbtWith (utxolib/wasm-utxo overloads) into a single decodePsbt. Remove UtxoTransaction/UtxoPsbt from DecodedTransaction, SdkBackend type and isSdkBackend guard, explainLegacyTx and its helpers, UtxoPsbt signing branches from signTransaction, and all decodeWith interface fields. Also fixes decodeTransaction to convert string input to buffer before the isPsbt check, so non-PSBT hex correctly throws ErrorDeprecatedTxFormat rather than a generic wasm deserialization error. Refs: T1-3245, T1-3280 Co-authored-by: llm-git --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 77 ++----- modules/abstract-utxo/src/index.ts | 1 - .../abstract-utxo/src/transaction/decode.ts | 46 +---- .../src/transaction/explainTransaction.ts | 24 +-- .../fixedScript/explainTransaction.ts | 61 +----- .../src/transaction/fixedScript/index.ts | 3 +- .../fixedScript/parseTransaction.ts | 1 - .../fixedScript/signLegacyTransaction.ts | 190 ------------------ .../fixedScript/signPsbtUtxolib.ts | 159 --------------- .../fixedScript/signTransaction.ts | 114 ++--------- .../abstract-utxo/src/transaction/index.ts | 2 +- .../src/transaction/signTransaction.ts | 11 +- .../abstract-utxo/src/transaction/types.ts | 13 +- .../unit/transaction/fixedScript/parsePsbt.ts | 104 +++------- 14 files changed, 78 insertions(+), 728 deletions(-) delete mode 100644 modules/abstract-utxo/src/transaction/fixedScript/signLegacyTransaction.ts delete mode 100644 modules/abstract-utxo/src/transaction/fixedScript/signPsbtUtxolib.ts diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index b66fa7b9cd..9180f93c9d 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -86,13 +86,8 @@ import { UtxoCoinNameMainnet, } from './names'; import { assertFixedScriptWalletAddress } from './address/fixedScript'; -import { isSdkBackend, ParsedTransaction, SdkBackend } from './transaction/types'; -import { - decodeDescriptorPsbt, - decodePsbtWith, - encodeTransaction, - stringToBufferTryFormats, -} from './transaction/decode'; +import { ParsedTransaction } from './transaction/types'; +import { decodeDescriptorPsbt, decodePsbt, encodeTransaction, stringToBufferTryFormats } from './transaction/decode'; import { fetchKeychains, toBip32Triple, UtxoKeychain } from './keychains'; import { verifyKeySignature, verifyUserPublicKey } from './verifyKey'; import { getPolicyForEnv } from './descriptor/validatePolicy'; @@ -250,7 +245,6 @@ export interface ExplainTransactionOptions; customChangeXpubs?: Triple; - decodeWith?: SdkBackend; } export interface DecoratedExplainTransactionOptions @@ -263,7 +257,6 @@ export type UtxoNetwork = utxolib.Network; export interface TransactionPrebuild extends BaseTransactionPrebuild { txInfo?: TransactionInfo; blockHeight?: number; - decodeWith?: SdkBackend; /** * PSBT-lite hex present only in pending approval flows, where another user's send fixed the format. * Not set in regular /tx/build responses (where the caller controls the build parameters). @@ -331,7 +324,6 @@ type UtxoBaseSignTransactionOptions = walletId?: string; txHex: string; txInfo?: TransactionInfo; - decodeWith?: SdkBackend; }; /** xpubs triple for wallet (user, backup, bitgo). Required only when txPrebuild.txHex is not a PSBT */ pubs?: Triple; @@ -420,10 +412,7 @@ export interface SignPsbtResponse { psbt: string; } -export abstract class AbstractUtxoCoin - extends BaseCoin - implements Musig2Participant, Musig2Participant -{ +export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Participant { abstract name: UtxoCoinName; /** @@ -442,8 +431,6 @@ export abstract class AbstractUtxoCoin this.amountType = amountType; } - defaultSdkBackend: SdkBackend = 'wasm-utxo'; - /** * @deprecated - will be removed when we drop support for utxolib * Use `name` property instead. @@ -617,52 +604,29 @@ export abstract class AbstractUtxoCoin return utxolib.bitgo.createTransactionFromHex(hex, this.network, this.amountType); } - decodeTransaction( - input: Buffer | string, - decodeWith: SdkBackend = this.defaultSdkBackend - ): DecodedTransaction { - if (typeof input === 'string') { - const buffer = stringToBufferTryFormats(input, ['hex', 'base64']); - return this.decodeTransaction(buffer, decodeWith); - } - - if (utxolib.bitgo.isPsbt(input)) { - if (decodeWith === 'utxolib') { - throw new Error(`SDK support for decodeWith=${decodeWith} is not available on this environment.`); - } - return decodePsbtWith(input, this.name, decodeWith); - } else { + decodeTransaction(input: Buffer | string): fixedScriptWallet.BitGoPsbt { + const buffer = typeof input === 'string' ? stringToBufferTryFormats(input, ['hex', 'base64']) : input; + if (!utxolib.bitgo.isPsbt(buffer)) { throw new ErrorDeprecatedTxFormat('legacy'); } + return decodePsbt(buffer, this.name); } - decodeTransactionAsPsbt(input: Buffer | string): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt { - const decoded = this.decodeTransaction(input); - if (decoded instanceof fixedScriptWallet.BitGoPsbt || decoded instanceof utxolib.bitgo.UtxoPsbt) { - return decoded; - } - throw new Error('expected psbt but got transaction'); + decodeTransactionAsPsbt(input: Buffer | string): fixedScriptWallet.BitGoPsbt { + return this.decodeTransaction(input); } - decodeTransactionFromPrebuild(prebuild: { + decodeTransactionFromPrebuild(prebuild: { txHex?: string; txBase64?: string; /** See TransactionPrebuild.txHexPsbt — only present in pending approval flows. */ txHexPsbt?: string; - decodeWith?: string; - }): DecodedTransaction { + }): fixedScriptWallet.BitGoPsbt { const string = prebuild.txHexPsbt ?? prebuild.txHex ?? prebuild.txBase64; if (!string) { throw new Error('missing required txHex or txBase64 property'); } - let { decodeWith } = prebuild; - if (decodeWith !== undefined) { - if (typeof decodeWith !== 'string' || !isSdkBackend(decodeWith)) { - console.error('decodeWith %s is not a valid value, using default', decodeWith); - decodeWith = undefined; - } - } - return this.decodeTransaction(string, decodeWith); + return this.decodeTransaction(string); } /** @@ -814,26 +778,13 @@ export abstract class AbstractUtxoCoin * @param psbt all MuSig2 inputs should contain user MuSig2 nonce * @param walletId */ - async getMusig2Nonces(psbt: utxolib.bitgo.UtxoPsbt, walletId: string): Promise; - async getMusig2Nonces(psbt: fixedScriptWallet.BitGoPsbt, walletId: string): Promise; - async getMusig2Nonces( - psbt: T, - walletId: string - ): Promise; - async getMusig2Nonces( - psbt: T, - walletId: string - ): Promise { + async getMusig2Nonces(psbt: fixedScriptWallet.BitGoPsbt, walletId: string): Promise { const buffer = encodeTransaction(psbt); const response = await this.bitgo .post(this.url('/wallet/' + walletId + '/tx/signpsbt')) .send({ psbt: buffer.toString('hex') }) .result(); - if (psbt instanceof utxolib.bitgo.UtxoPsbt) { - return decodePsbtWith(response.psbt, this.name, 'utxolib') as T; - } else { - return decodePsbtWith(response.psbt, this.name, 'wasm-utxo') as T; - } + return decodePsbt(response.psbt, this.name); } /** diff --git a/modules/abstract-utxo/src/index.ts b/modules/abstract-utxo/src/index.ts index 7957ac614d..1118441b2a 100644 --- a/modules/abstract-utxo/src/index.ts +++ b/modules/abstract-utxo/src/index.ts @@ -4,7 +4,6 @@ export * from './config'; export * from './names'; export * from './recovery'; export * from './transaction/fixedScript/replayProtection'; -export * from './transaction/fixedScript/signLegacyTransaction'; export * from './unspent'; export { UtxoWallet } from './wallet'; diff --git a/modules/abstract-utxo/src/transaction/decode.ts b/modules/abstract-utxo/src/transaction/decode.ts index 77d461f6ac..556e5e73d4 100644 --- a/modules/abstract-utxo/src/transaction/decode.ts +++ b/modules/abstract-utxo/src/transaction/decode.ts @@ -3,7 +3,7 @@ import { fixedScriptWallet, hasPsbtMagic, Psbt as WasmPsbt, utxolibCompat } from import { getNetworkFromCoinName, UtxoCoinName } from '../names'; -import { SdkBackend, BitGoPsbt } from './types'; +import { BitGoPsbt } from './types'; type BufferEncoding = 'hex' | 'base64'; @@ -31,39 +31,11 @@ function toNetworkName(coinName: UtxoCoinName): utxolibCompat.UtxolibName { return networkName; } -export function decodePsbtWith( - psbt: string | Buffer, - coinName: UtxoCoinName, - backend: 'utxolib' -): utxolib.bitgo.UtxoPsbt; -export function decodePsbtWith( - psbt: string | Buffer, - coinName: UtxoCoinName, - backend: 'wasm-utxo' -): fixedScriptWallet.BitGoPsbt; -export function decodePsbtWith( - psbt: string | Buffer, - coinName: UtxoCoinName, - backend: SdkBackend -): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt; -export function decodePsbtWith( - psbt: string | Buffer, - coinName: UtxoCoinName, - backend: SdkBackend -): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt { +export function decodePsbt(psbt: string | Buffer, coinName: UtxoCoinName): BitGoPsbt { if (typeof psbt === 'string') { psbt = Buffer.from(psbt, 'hex'); } - if (backend === 'utxolib') { - const network = getNetworkFromCoinName(coinName); - return utxolib.bitgo.createPsbtFromBuffer(psbt, network); - } else { - return fixedScriptWallet.BitGoPsbt.fromBytes(psbt, toNetworkName(coinName)); - } -} - -export function decodePsbt(psbt: string | Buffer, coinName: UtxoCoinName): BitGoPsbt { - return decodePsbtWith(psbt, coinName, 'wasm-utxo'); + return fixedScriptWallet.BitGoPsbt.fromBytes(psbt, toNetworkName(coinName)); } export type PrebuildLike = { @@ -90,14 +62,6 @@ export function decodeDescriptorPsbt(prebuild: PrebuildLike): WasmPsbt { return WasmPsbt.deserialize(bytes); } -export function encodeTransaction( - transaction: utxolib.bitgo.UtxoTransaction | utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt -): Buffer { - if (transaction instanceof utxolib.bitgo.UtxoTransaction) { - return transaction.toBuffer(); - } else if (transaction instanceof utxolib.bitgo.UtxoPsbt) { - return transaction.toBuffer(); - } else { - return Buffer.from(transaction.serialize()); - } +export function encodeTransaction(transaction: fixedScriptWallet.BitGoPsbt): Buffer { + return Buffer.from(transaction.serialize()); } diff --git a/modules/abstract-utxo/src/transaction/explainTransaction.ts b/modules/abstract-utxo/src/transaction/explainTransaction.ts index 45c90da5e9..7cd21f0453 100644 --- a/modules/abstract-utxo/src/transaction/explainTransaction.ts +++ b/modules/abstract-utxo/src/transaction/explainTransaction.ts @@ -1,4 +1,3 @@ -import * as utxolib from '@bitgo/utxo-lib'; import { fixedScriptWallet, Psbt as WasmPsbt } from '@bitgo/wasm-utxo'; import { isTriple, IWallet, Triple } from '@bitgo/sdk-core'; @@ -9,11 +8,7 @@ import { UtxoCoinName } from '../names'; import type { Unspent } from '../unspent'; import { getReplayProtectionPubkeys } from './fixedScript/replayProtection'; -import type { - TransactionExplanationUtxolibLegacy, - TransactionExplanationUtxolibPsbt, - TransactionExplanationWasm, -} from './fixedScript/explainTransaction'; +import type { TransactionExplanationUtxolibPsbt, TransactionExplanationWasm } from './fixedScript/explainTransaction'; import * as fixedScript from './fixedScript'; import * as descriptor from './descriptor'; @@ -22,7 +17,7 @@ import * as descriptor from './descriptor'; * change amounts, and transaction outputs. */ export function explainTx( - tx: utxolib.bitgo.UtxoTransaction | utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt | WasmPsbt, + tx: fixedScriptWallet.BitGoPsbt | WasmPsbt, params: { wallet?: IWallet; pubs?: string[]; @@ -31,7 +26,7 @@ export function explainTx( changeInfo?: fixedScript.ChangeAddressInfo[]; }, coinName: UtxoCoinName -): TransactionExplanationUtxolibLegacy | TransactionExplanationUtxolibPsbt | TransactionExplanationWasm { +): TransactionExplanationUtxolibPsbt | TransactionExplanationWasm { if (params.wallet && isDescriptorWallet(params.wallet)) { if (!(tx instanceof WasmPsbt)) { throw new Error('descriptor wallets require PSBT format transactions'); @@ -43,19 +38,12 @@ export function explainTx( const descriptors = getDescriptorMapFromWallet(params.wallet, walletKeys, getPolicyForEnv(params.wallet.bitgo.env)); return descriptor.explainPsbt(tx, descriptors, coinName); } - if (tx instanceof utxolib.bitgo.UtxoPsbt) { - return fixedScript.explainPsbt(tx, { ...params, customChangePubs: params.customChangeXpubs }, coinName); - } else if (tx instanceof fixedScriptWallet.BitGoPsbt) { + if (tx instanceof fixedScriptWallet.BitGoPsbt) { const pubs = params.pubs; if (!pubs) { throw new Error('pub triple is required'); } - const walletXpubs: Triple | undefined = - pubs instanceof utxolib.bitgo.RootWalletKeys - ? (pubs.triple.map((k) => k.neutered().toBase58()) as Triple) - : isTriple(pubs) - ? (pubs as Triple) - : undefined; + const walletXpubs: Triple | undefined = isTriple(pubs) ? (pubs as Triple) : undefined; if (!walletXpubs) { throw new Error('pub triple must be valid triple or RootWalletKeys'); } @@ -68,6 +56,6 @@ export function explainTx( } else if (tx instanceof WasmPsbt) { throw new Error('descriptor Psbt is only supported for descriptor wallets'); } else { - return fixedScript.explainLegacyTx(tx, params, coinName); + throw new Error('unsupported transaction type'); } } diff --git a/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts index 0165816e7d..d38e18fc73 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts @@ -7,7 +7,6 @@ import * as utxocore from '@bitgo/utxo-core'; import type { Bip322Message } from '../../abstractUtxoCoin'; import type { Output, FixedScriptWalletOutput } from '../types'; -import type { Unspent } from '../../unspent'; import { toExtendedAddressFormat } from '../recipient'; import { getPayGoVerificationPubkey } from '../getPayGoVerificationPubkey'; import { toBip32Triple } from '../../keychains'; @@ -56,18 +55,12 @@ export type TransactionExplanationWasm = AbstractUtxoTransactionExplanation; - /** When parsing a PSBT, we can infer the fee so we set TFee to string. */ export type TransactionExplanationUtxolibPsbt = TransactionExplanationWithSignatures; export type TransactionExplanationDescriptor = TransactionExplanationWithSignatures; -export type TransactionExplanation = - | TransactionExplanationUtxolibLegacy - | TransactionExplanationUtxolibPsbt - | TransactionExplanationWasm; +export type TransactionExplanation = TransactionExplanationUtxolibPsbt | TransactionExplanationWasm; export type ChangeAddressInfo = { address: string; @@ -194,40 +187,6 @@ function getPsbtInputSignaturesCount( : (Array(psbt.data.inputs.length) as number[]).fill(0); } -function getTxInputSignaturesCount( - tx: bitgo.UtxoTransaction, - params: { - txInfo?: { unspents?: Unspent[] }; - pubs?: bitgo.RootWalletKeys | string[]; - }, - coinName: UtxoCoinName -) { - const network = getNetworkFromCoinName(coinName); - const prevOutputs = params.txInfo?.unspents?.map((u) => bitgo.toOutput(u, network)); - const rootWalletKeys = getRootWalletKeys(params); - const { unspents = [] } = params.txInfo ?? {}; - - // get the number of signatures per input - return tx.ins.map((input, idx): number => { - if (unspents.length !== tx.ins.length) { - return 0; - } - if (!prevOutputs) { - throw new Error(`invalid state`); - } - if (!rootWalletKeys) { - // no pub keys or incorrect number of pub keys - return 0; - } - try { - return bitgo.verifySignatureWithUnspent(tx, idx, unspents, rootWalletKeys).filter((v) => v).length; - } catch (e) { - // some other error occurred and we can't validate the signatures - return 0; - } - }); -} - function getChainAndIndexFromBip32Derivations(output: bitgo.PsbtOutput) { const derivations = output.bip32Derivation ?? output.tapBip32Derivation ?? undefined; if (!derivations) { @@ -443,21 +402,3 @@ export function explainPsbt( messages, }; } - -export function explainLegacyTx( - tx: bitgo.UtxoTransaction, - params: { - pubs?: string[]; - txInfo?: { unspents?: Unspent[] }; - changeInfo?: { address: string; chain: number; index: number }[]; - }, - coinName: UtxoCoinName -): TransactionExplanationUtxolibLegacy { - const common = explainCommon(tx, params, coinName); - const inputSignaturesCount = getTxInputSignaturesCount(tx, params, coinName); - return { - ...common, - inputSignatures: inputSignaturesCount, - signatures: inputSignaturesCount.reduce((prev, curr) => (curr > prev ? curr : prev), 0), - }; -} diff --git a/modules/abstract-utxo/src/transaction/fixedScript/index.ts b/modules/abstract-utxo/src/transaction/fixedScript/index.ts index ee341fdcfc..e9fd1cbdc7 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/index.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/index.ts @@ -1,4 +1,4 @@ -export { explainPsbt, explainLegacyTx, ChangeAddressInfo } from './explainTransaction'; +export { explainPsbt, ChangeAddressInfo } from './explainTransaction'; export { explainPsbtWasm, explainPsbtWasmBigInt, @@ -11,6 +11,5 @@ export { parseTransaction } from './parseTransaction'; export { CustomChangeOptions } from './parseOutput'; export { verifyTransaction } from './verifyTransaction'; export { signTransaction } from './signTransaction'; -export * from './signLegacyTransaction'; export * from './SigningError'; export * from './replayProtection'; diff --git a/modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts index c776cf5883..e28e5525a5 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts @@ -210,7 +210,6 @@ export async function parseTransaction( const explanation: TransactionExplanation = await coin.explainTransaction({ txHex: effectiveTxHex, txInfo: txPrebuild.txInfo, - decodeWith: txPrebuild.decodeWith, pubs: keychainArray.map((k) => k.pub) as Triple, customChangeXpubs, }); diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signLegacyTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/signLegacyTransaction.ts deleted file mode 100644 index 304e012740..0000000000 --- a/modules/abstract-utxo/src/transaction/fixedScript/signLegacyTransaction.ts +++ /dev/null @@ -1,190 +0,0 @@ -import assert from 'assert'; - -import * as utxolib from '@bitgo/utxo-lib'; -import { bitgo } from '@bitgo/utxo-lib'; -import { isTriple, Triple } from '@bitgo/sdk-core'; -import { BIP32, bip32 } from '@bitgo/wasm-utxo'; -import debugLib from 'debug'; - -import { UtxoCoinName } from '../../names'; -import { isWalletUnspent, type Unspent } from '../../unspent'; -import { toUtxolibBIP32 } from '../../wasmUtil'; - -import { getReplayProtectionAddresses } from './replayProtection'; -import { InputSigningError, TransactionSigningError } from './SigningError'; - -const debug = debugLib('bitgo:v2:utxo'); - -const { signInputWithUnspent, toOutput } = utxolib.bitgo; - -type RootWalletKeys = utxolib.bitgo.RootWalletKeys; - -const UTXOLIB_VALID_CHAIN_CODES = new Set([0, 1, 10, 11, 20, 21, 30, 31, 40, 41] as const); - -/** - * Sign all inputs of a wallet transaction and verify signatures after signing. - * Collects and logs signing errors and verification errors, throws error in the end if any of them - * failed. - * - * @param transaction - wallet transaction (builder) to be signed - * @param unspents - transaction unspents - * @param walletSigner - signing parameters - * @param coinName - coin name for network-specific logic - * @param isLastSignature - Returns full-signed transaction when true. Builds half-signed when false. - * @param replayProtectionAddresses - List of replay protection addresses to skip signing - */ -export function signAndVerifyWalletTransaction( - transaction: utxolib.bitgo.UtxoTransaction | utxolib.bitgo.UtxoTransactionBuilder, - unspents: Unspent[], - walletSigner: utxolib.bitgo.WalletUnspentSigner, - coinName: UtxoCoinName, - { - isLastSignature, - replayProtectionAddresses, - }: { - isLastSignature: boolean; - replayProtectionAddresses?: string[]; - } -): utxolib.bitgo.UtxoTransaction { - const network = transaction.network as utxolib.Network; - if (replayProtectionAddresses === undefined) { - replayProtectionAddresses = getReplayProtectionAddresses(coinName); - } - const prevOutputs = unspents.map((u) => toOutput(u, network)); - - let txBuilder: utxolib.bitgo.UtxoTransactionBuilder; - if (transaction instanceof utxolib.bitgo.UtxoTransaction) { - txBuilder = utxolib.bitgo.createTransactionBuilderFromTransaction(transaction, prevOutputs); - if (transaction.ins.length !== unspents.length) { - throw new Error(`transaction inputs must match unspents`); - } - } else if (transaction instanceof utxolib.bitgo.UtxoTransactionBuilder) { - txBuilder = transaction; - } else { - throw new Error(`must pass UtxoTransaction or UtxoTransactionBuilder`); - } - - const signErrors: InputSigningError[] = unspents - .map((unspent: Unspent, inputIndex: number) => { - if (replayProtectionAddresses.includes(unspent.address)) { - debug('Skipping signature for input %d of %d (RP input?)', inputIndex + 1, unspents.length); - return; - } - if (!isWalletUnspent(unspent)) { - return InputSigningError.expectedWalletUnspent(inputIndex, null, unspent); - } - if (!UTXOLIB_VALID_CHAIN_CODES.has(unspent.chain as utxolib.bitgo.ChainCode)) { - return new InputSigningError( - inputIndex, - null, - unspent, - new Error(`Chain code ${unspent.chain} is not supported for legacy signing`) - ); - } - try { - signInputWithUnspent( - txBuilder, - inputIndex, - unspent as unknown as utxolib.bitgo.WalletUnspent, - walletSigner - ); - debug('Successfully signed input %d of %d', inputIndex + 1, unspents.length); - } catch (e) { - return new InputSigningError(inputIndex, null, unspent, e); - } - }) - .filter((e): e is InputSigningError => e !== undefined); - - const signedTransaction = isLastSignature ? txBuilder.build() : txBuilder.buildIncomplete(); - - const verifyErrors: InputSigningError[] = signedTransaction.ins - .map((input, inputIndex) => { - const unspent = unspents[inputIndex] as Unspent; - if (replayProtectionAddresses.includes(unspent.address)) { - debug( - 'Skipping input signature %d of %d (unspent from replay protection address which is platform signed only)', - inputIndex + 1, - unspents.length - ); - return; - } - if (!isWalletUnspent(unspent)) { - return InputSigningError.expectedWalletUnspent(inputIndex, null, unspent); - } - if (!UTXOLIB_VALID_CHAIN_CODES.has(unspent.chain as utxolib.bitgo.ChainCode)) { - return new InputSigningError( - inputIndex, - null, - unspent, - new Error(`Chain code ${unspent.chain} is not supported for legacy verification`) - ); - } - const walletUnspent = unspent; - try { - const publicKey = walletSigner.deriveForChainAndIndex( - walletUnspent.chain as utxolib.bitgo.ChainCode, - walletUnspent.index - ).signer.publicKey; - if ( - !utxolib.bitgo.verifySignatureWithPublicKey(signedTransaction, inputIndex, prevOutputs, publicKey) - ) { - return new InputSigningError(inputIndex, null, unspent, new Error(`invalid signature`)); - } - } catch (e) { - debug('Invalid signature'); - return new InputSigningError(inputIndex, null, unspent, e); - } - }) - .filter((e): e is InputSigningError => e !== undefined); - - if (signErrors.length || verifyErrors.length) { - throw new TransactionSigningError(signErrors, verifyErrors); - } - - return signedTransaction; -} - -export function signLegacyTransaction( - tx: utxolib.bitgo.UtxoTransaction, - signerKeychain: bip32.BIP32Interface | undefined, - coinName: UtxoCoinName, - params: { - isLastSignature: boolean; - signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined; - txInfo: { unspents?: Unspent[] } | undefined; - pubs: string[] | undefined; - cosignerPub: string | undefined; - } -): utxolib.bitgo.UtxoTransaction { - switch (params.signingStep) { - case 'signerNonce': - case 'cosignerNonce': - /** - * In certain cases, the caller of this method may not know whether the txHex contains a psbt with taproot key path spend input(s). - * Instead of throwing error, no-op and return the txHex. So that the caller can call this method in the same sequence. - */ - return tx; - } - - if (tx.ins.length !== params.txInfo?.unspents?.length) { - throw new Error('length of unspents array should equal to the number of transaction inputs'); - } - - if (!params.pubs || !isTriple(params.pubs)) { - throw new Error(`must provide xpub array`); - } - - const keychains = params.pubs.map((pub) => toUtxolibBIP32(BIP32.fromBase58(pub))) as Triple; - const cosignerPub = params.cosignerPub ?? params.pubs[2]; - const cosignerKeychain = toUtxolibBIP32(BIP32.fromBase58(cosignerPub)); - - assert(signerKeychain); - const walletSigner = new bitgo.WalletUnspentSigner( - keychains, - toUtxolibBIP32(signerKeychain), - cosignerKeychain - ); - return signAndVerifyWalletTransaction(tx, params.txInfo.unspents, walletSigner, coinName, { - isLastSignature: params.isLastSignature, - }) as utxolib.bitgo.UtxoTransaction; -} diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signPsbtUtxolib.ts b/modules/abstract-utxo/src/transaction/fixedScript/signPsbtUtxolib.ts deleted file mode 100644 index 12bf626dd1..0000000000 --- a/modules/abstract-utxo/src/transaction/fixedScript/signPsbtUtxolib.ts +++ /dev/null @@ -1,159 +0,0 @@ -import assert from 'assert'; - -import * as utxolib from '@bitgo/utxo-lib'; -import { bitgo } from '@bitgo/utxo-lib'; -import debugLib from 'debug'; - -import { InputSigningError, TransactionSigningError } from './SigningError'; -import { Musig2Participant } from './musig2'; - -const debug = debugLib('bitgo:v2:utxo'); - -export type PsbtParsedScriptType = - | 'p2sh' - | 'p2wsh' - | 'p2shP2wsh' - | 'p2shP2pk' - | 'taprootKeyPathSpend' - | 'taprootScriptPathSpend' - // wasm-utxo types - | 'p2trLegacy' - | 'p2trMusig2ScriptPath' - | 'p2trMusig2KeyPath'; - -/** - * Sign all inputs of a psbt and verify signatures after signing. - * Collects and logs signing errors and verification errors, throws error in the end if any of them - * failed. - * - * This function mirrors signAndVerifyWalletTransaction, but is used for signing PSBTs instead of - * using TransactionBuilder - * - * @param psbt - * @param signerKeychain - */ -export function signAndVerifyPsbt( - psbt: utxolib.bitgo.UtxoPsbt, - signerKeychain: utxolib.BIP32Interface -): utxolib.bitgo.UtxoPsbt { - const txInputs = psbt.txInputs; - const outputIds: string[] = []; - const scriptTypes: PsbtParsedScriptType[] = []; - - const signErrors: InputSigningError[] = psbt.data.inputs - .map((input, inputIndex: number) => { - const outputId = utxolib.bitgo.formatOutputId(utxolib.bitgo.getOutputIdForInput(txInputs[inputIndex])); - outputIds.push(outputId); - - const { scriptType } = utxolib.bitgo.parsePsbtInput(input); - scriptTypes.push(scriptType); - - if (scriptType === 'p2shP2pk') { - debug('Skipping signature for input %d of %d (RP input?)', inputIndex + 1, psbt.data.inputs.length); - return; - } - - try { - psbt.signInputHD(inputIndex, signerKeychain); - debug('Successfully signed input %d of %d', inputIndex + 1, psbt.data.inputs.length); - } catch (e) { - return new InputSigningError(inputIndex, scriptType, { id: outputId }, e); - } - }) - .filter((e): e is InputSigningError => e !== undefined); - - const verifyErrors: InputSigningError[] = psbt.data.inputs - .map((input, inputIndex) => { - const scriptType = scriptTypes[inputIndex]; - if (scriptType === 'p2shP2pk') { - debug( - 'Skipping input signature %d of %d (unspent from replay protection address which is platform signed only)', - inputIndex + 1, - psbt.data.inputs.length - ); - return; - } - - const outputId = outputIds[inputIndex]; - try { - if (!psbt.validateSignaturesOfInputHD(inputIndex, signerKeychain)) { - return new InputSigningError(inputIndex, scriptType, { id: outputId }, new Error(`invalid signature`)); - } - } catch (e) { - debug('Invalid signature'); - return new InputSigningError(inputIndex, scriptType, { id: outputId }, e); - } - }) - .filter((e): e is InputSigningError => e !== undefined); - - if (signErrors.length || verifyErrors.length) { - throw new TransactionSigningError(signErrors, verifyErrors); - } - - return psbt; -} - -/** - * Key Value: Unsigned tx id => PSBT - * It is used to cache PSBTs with taproot key path (MuSig2) inputs during external express signer is activated. - * Reason: MuSig2 signer secure nonce is cached in the UtxoPsbt object. It will be required during the signing step. - * For more info, check SignTransactionOptions.signingStep - * - * TODO BTC-276: This cache may need to be done with LRU like memory safe caching if memory issues comes up. - */ -const PSBT_CACHE = new Map(); - -export async function signPsbtWithMusig2ParticipantUtxolib( - coin: Musig2Participant, - tx: utxolib.bitgo.UtxoPsbt, - signerKeychain: utxolib.BIP32Interface | undefined, - params: { - signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined; - walletId: string | undefined; - } -): Promise { - if (bitgo.isTransactionWithKeyPathSpendInput(tx)) { - switch (params.signingStep) { - case 'signerNonce': - assert(signerKeychain); - tx.setAllInputsMusig2NonceHD(signerKeychain); - PSBT_CACHE.set(tx.getUnsignedTx().getId(), tx); - return tx; - case 'cosignerNonce': - assert(params.walletId, 'walletId is required for MuSig2 bitgo nonce'); - return await coin.getMusig2Nonces(tx, params.walletId); - case 'signerSignature': - const txId = tx.getUnsignedTx().getId(); - const psbt = PSBT_CACHE.get(txId); - assert( - psbt, - `Psbt is missing from txCache (cache size ${PSBT_CACHE.size}). - This may be due to the request being routed to a different BitGo-Express instance that for signing step 'signerNonce'.` - ); - PSBT_CACHE.delete(txId); - tx = psbt.combine(tx); - break; - default: - // this instance is not an external signer - assert(params.walletId, 'walletId is required for MuSig2 bitgo nonce'); - assert(signerKeychain); - tx.setAllInputsMusig2NonceHD(signerKeychain); - const response = await coin.getMusig2Nonces(tx, params.walletId); - tx = tx.combine(response); - break; - } - } else { - switch (params.signingStep) { - case 'signerNonce': - case 'cosignerNonce': - /** - * In certain cases, the caller of this method may not know whether the txHex contains a psbt with taproot key path spend input(s). - * Instead of throwing error, no-op and return the txHex. So that the caller can call this method in the same sequence. - */ - return tx; - } - } - - assert(signerKeychain); - return signAndVerifyPsbt(tx, signerKeychain); -} diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts index 23961037da..49c333c4f0 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts @@ -2,64 +2,30 @@ import assert from 'assert'; import { isTriple } from '@bitgo/sdk-core'; import _ from 'lodash'; -import { bitgo } from '@bitgo/utxo-lib'; -import * as utxolib from '@bitgo/utxo-lib'; import { BIP32, bip32, fixedScriptWallet } from '@bitgo/wasm-utxo'; import { UtxoCoinName } from '../../names'; import type { Unspent } from '../../unspent'; -import { toUtxolibBIP32 } from '../../wasmUtil'; import { Musig2Participant } from './musig2'; -import { signLegacyTransaction } from './signLegacyTransaction'; -import { signPsbtWithMusig2ParticipantUtxolib, signAndVerifyPsbt as signAndVerifyPsbtUtxolib } from './signPsbtUtxolib'; import { signPsbtWithMusig2ParticipantWasm, signAndVerifyPsbtWasm, ReplayProtectionKeys } from './signPsbtWasm'; import { getReplayProtectionPubkeys } from './replayProtection'; -/** - * Sign and verify a PSBT using either utxolib or wasm-utxo depending on the PSBT type. - */ -export function signAndVerifyPsbt( - psbt: utxolib.bitgo.UtxoPsbt, - signerKeychain: bip32.BIP32Interface | BIP32, - rootWalletKeys: fixedScriptWallet.RootWalletKeys | undefined, - replayProtection: ReplayProtectionKeys | undefined, - options?: { writeSignedWith?: boolean } -): utxolib.bitgo.UtxoPsbt; export function signAndVerifyPsbt( psbt: fixedScriptWallet.BitGoPsbt, signerKeychain: bip32.BIP32Interface | BIP32, rootWalletKeys: fixedScriptWallet.RootWalletKeys, replayProtection: ReplayProtectionKeys, - options?: { writeSignedWith?: boolean } -): fixedScriptWallet.BitGoPsbt; -export function signAndVerifyPsbt( - psbt: utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt, - signerKeychain: bip32.BIP32Interface | BIP32, - rootWalletKeys: fixedScriptWallet.RootWalletKeys, - replayProtection: ReplayProtectionKeys, - options?: { writeSignedWith?: boolean } -): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt; -export function signAndVerifyPsbt( - psbt: utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt, - signerKeychain: bip32.BIP32Interface | BIP32, - rootWalletKeys: fixedScriptWallet.RootWalletKeys | undefined, - replayProtection: ReplayProtectionKeys | undefined, options: { writeSignedWith?: boolean } = {} -): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt { - if (psbt instanceof bitgo.UtxoPsbt) { - return signAndVerifyPsbtUtxolib(psbt, toUtxolibBIP32(signerKeychain)); - } +): fixedScriptWallet.BitGoPsbt { assert(rootWalletKeys, 'rootWalletKeys required for wasm-utxo signing'); assert(replayProtection, 'replayProtection required for wasm-utxo signing'); return signAndVerifyPsbtWasm(psbt, signerKeychain, rootWalletKeys, replayProtection, options); } -export async function signTransaction< - T extends utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction | fixedScriptWallet.BitGoPsbt ->( - coin: Musig2Participant | Musig2Participant, - tx: T, +export async function signTransaction( + coin: Musig2Participant, + tx: fixedScriptWallet.BitGoPsbt, signerKeychain: bip32.BIP32Interface | undefined, coinName: UtxoCoinName, params: { @@ -75,9 +41,7 @@ export async function signTransaction< extractTransaction?: boolean; writeSignedWith?: boolean; } -): Promise< - utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction | fixedScriptWallet.BitGoPsbt | Buffer -> { +): Promise { let isLastSignature = false; if (_.isBoolean(params.isLastSignature)) { // if build is called instead of buildIncomplete, no signature placeholders are left in the sig script @@ -86,59 +50,23 @@ export async function signTransaction< const { extractTransaction = true } = params; - if (tx instanceof bitgo.UtxoPsbt) { - const signedPsbt = await signPsbtWithMusig2ParticipantUtxolib( - coin as Musig2Participant, - tx, - signerKeychain ? toUtxolibBIP32(signerKeychain) : undefined, - { - signingStep: params.signingStep, - walletId: params.walletId, - } - ); - if (isLastSignature) { - if (extractTransaction) { - signedPsbt.finalizeAllInputs(); - return signedPsbt.extractTransaction(); - } - // Return signed PSBT without finalizing to preserve derivation info - return signedPsbt; - } - return signedPsbt; - } else if (tx instanceof fixedScriptWallet.BitGoPsbt) { - assert(params.pubs, 'pubs are required for fixed script signing'); - assert(isTriple(params.pubs), 'pubs must be a triple'); - const rootWalletKeys = fixedScriptWallet.RootWalletKeys.fromXpubs(params.pubs); - const signedPsbt = await signPsbtWithMusig2ParticipantWasm( - coin as Musig2Participant, - tx, - signerKeychain, - rootWalletKeys, - { - replayProtection: { - publicKeys: getReplayProtectionPubkeys(coinName), - }, - signingStep: params.signingStep, - walletId: params.walletId, - writeSignedWith: params.writeSignedWith, - } - ); - if (isLastSignature) { - if (extractTransaction) { - signedPsbt.finalizeAllInputs(); - return Buffer.from(signedPsbt.extractTransaction().toBytes()); - } - // Return finalized PSBT without extracting to legacy format - return signedPsbt; + assert(params.pubs, 'pubs are required for fixed script signing'); + assert(isTriple(params.pubs), 'pubs must be a triple'); + const rootWalletKeys = fixedScriptWallet.RootWalletKeys.fromXpubs(params.pubs); + const signedPsbt = await signPsbtWithMusig2ParticipantWasm(coin, tx, signerKeychain, rootWalletKeys, { + replayProtection: { + publicKeys: getReplayProtectionPubkeys(coinName), + }, + signingStep: params.signingStep, + walletId: params.walletId, + writeSignedWith: params.writeSignedWith, + }); + if (isLastSignature) { + if (extractTransaction) { + signedPsbt.finalizeAllInputs(); + return Buffer.from(signedPsbt.extractTransaction().toBytes()); } return signedPsbt; } - - return signLegacyTransaction(tx, signerKeychain, coinName, { - isLastSignature, - signingStep: params.signingStep, - txInfo: params.txInfo, - pubs: params.pubs, - cosignerPub: params.cosignerPub, - }); + return signedPsbt; } diff --git a/modules/abstract-utxo/src/transaction/index.ts b/modules/abstract-utxo/src/transaction/index.ts index a14f794f7d..075ef05742 100644 --- a/modules/abstract-utxo/src/transaction/index.ts +++ b/modules/abstract-utxo/src/transaction/index.ts @@ -5,5 +5,5 @@ export { parseTransaction } from './parseTransaction'; export { verifyTransaction } from './verifyTransaction'; export * from './fetchInputs'; export * as bip322 from './bip322'; -export { decodePsbt, decodePsbtWith } from './decode'; +export { decodePsbt } from './decode'; export * from './fixedScript'; diff --git a/modules/abstract-utxo/src/transaction/signTransaction.ts b/modules/abstract-utxo/src/transaction/signTransaction.ts index ace4ce0dac..23f27de260 100644 --- a/modules/abstract-utxo/src/transaction/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/signTransaction.ts @@ -6,11 +6,10 @@ import buildDebug from 'debug'; import { AbstractUtxoCoin, SignTransactionOptions } from '../abstractUtxoCoin'; import { getDescriptorMapFromWallet, getPolicyForEnv, isDescriptorWallet } from '../descriptor'; import { fetchKeychains, toBip32Triple } from '../keychains'; -import { isUtxoLibPsbt } from '../wasmUtil'; import * as fixedScript from './fixedScript'; import * as descriptor from './descriptor'; -import { decodeDescriptorPsbt, decodePsbtWith, encodeTransaction } from './decode'; +import { decodeDescriptorPsbt, encodeTransaction } from './decode'; const debug = buildDebug('bitgo:abstract-utxo:transaction:signTransaction'); @@ -59,13 +58,7 @@ export async function signTransaction( }); return { txHex: Buffer.from(psbt.serialize()).toString('hex') }; } else { - let tx = coin.decodeTransactionFromPrebuild(params.txPrebuild); - - // When returnLegacyFormat is set, ensure we use wasm-utxo's BitGoPsbt so - // getHalfSignedLegacyFormat() is available after signing. - if (params.returnLegacyFormat && isUtxoLibPsbt(tx)) { - tx = decodePsbtWith(tx.toBuffer(), coin.name, 'wasm-utxo'); - } + const tx = coin.decodeTransactionFromPrebuild(params.txPrebuild); const signedTx = await fixedScript.signTransaction(coin, tx, getSignerKeychain(params.prv), coin.name, { walletId: params.txPrebuild.walletId, diff --git a/modules/abstract-utxo/src/transaction/types.ts b/modules/abstract-utxo/src/transaction/types.ts index 329acace5b..2ed4bdd86b 100644 --- a/modules/abstract-utxo/src/transaction/types.ts +++ b/modules/abstract-utxo/src/transaction/types.ts @@ -1,4 +1,3 @@ -import * as utxolib from '@bitgo/utxo-lib'; import { fixedScriptWallet } from '@bitgo/wasm-utxo'; import type { UtxoNamedKeychains } from '../keychains'; @@ -7,16 +6,8 @@ import type { CustomChangeOptions } from './fixedScript'; export type BitGoPsbt = fixedScriptWallet.BitGoPsbt; -export type SdkBackend = 'utxolib' | 'wasm-utxo'; - -export function isSdkBackend(backend: string): backend is SdkBackend { - return backend === 'utxolib' || backend === 'wasm-utxo'; -} - -export type DecodedTransaction = - | utxolib.bitgo.UtxoTransaction - | utxolib.bitgo.UtxoPsbt - | fixedScriptWallet.BitGoPsbt; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export type DecodedTransaction = fixedScriptWallet.BitGoPsbt; export interface BaseOutput { address: string; diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts index 9e527c2e8e..d14cbbbd58 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts @@ -3,17 +3,14 @@ import assert from 'node:assert/strict'; import * as sinon from 'sinon'; import * as utxolib from '@bitgo/utxo-lib'; import { Wallet, VerificationOptions, ITransactionRecipient, Triple } from '@bitgo/sdk-core'; -import { address as wasmAddress, fixedScriptWallet } from '@bitgo/wasm-utxo'; +import { fixedScriptWallet } from '@bitgo/wasm-utxo'; import { parseTransaction } from '../../../../src/transaction/fixedScript/parseTransaction'; import { ParsedTransaction } from '../../../../src/transaction/types'; import { UtxoWallet } from '../../../../src/wallet'; import { getUtxoCoin } from '../../util'; -import { explainLegacyTx, explainPsbt, explainPsbtWasm } from '../../../../src/transaction/fixedScript'; -import type { - TransactionExplanation, - ChangeAddressInfo, -} from '../../../../src/transaction/fixedScript/explainTransaction'; +import { explainPsbt, explainPsbtWasm } from '../../../../src/transaction/fixedScript'; +import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction'; import { getCoinName } from '../../../../src/names'; import { TransactionPrebuild } from '../../../../src/abstractUtxoCoin'; @@ -52,30 +49,6 @@ function getTxParamsFromExplanation( }; } -function getChangeInfoFromPsbt(psbt: utxolib.bitgo.UtxoPsbt): ChangeAddressInfo[] | undefined { - try { - return utxolib.bitgo.findInternalOutputIndices(psbt).map((i) => { - const output = psbt.data.outputs[i]; - const derivations = output.bip32Derivation ?? output.tapBip32Derivation ?? undefined; - if (!derivations || derivations.length !== 3) { - throw new Error('expected 3 derivation paths'); - } - const path = derivations[0].path; - const { chain, index } = utxolib.bitgo.getChainAndIndexFromPath(path); - return { - address: wasmAddress.fromOutputScriptWithCoin(psbt.txOutputs[i].script, getCoinName(psbt.network)), - chain, - index, - }; - }); - } catch (e) { - if (e instanceof utxolib.bitgo.ErrorNoMultiSigInputFound) { - return undefined; - } - throw e; - } -} - function describeParseTransactionWith( acidTest: utxolib.testutil.AcidTest, label: string, @@ -85,7 +58,6 @@ function describeParseTransactionWith( externalCustomChangeAddress = false, expectedExplicitExternalSpendAmount, expectedImplicitExternalSpendAmount, - txFormat = 'psbt', }: { txParams: | { @@ -98,7 +70,6 @@ function describeParseTransactionWith( externalCustomChangeAddress?: boolean; expectedExplicitExternalSpendAmount: bigint; expectedImplicitExternalSpendAmount: bigint; - txFormat?: 'psbt' | 'legacy'; } ) { describe(`${acidTest.name}/${label}`, function () { @@ -117,35 +88,26 @@ function describeParseTransactionWith( const txHash = tx.getId(); let explanation: TransactionExplanation; - if (txFormat === 'psbt') { - if (backend === 'utxolib') { - explanation = explainPsbt(psbt, { pubs: acidTest.rootWalletKeys }, coinName, { - strict: true, - }); - } else if (backend === 'wasm') { - const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes( - psbt.toBuffer(), - utxolib.getNetworkName(acidTest.network)! - ); - explanation = explainPsbtWasm( - wasmPsbt, - acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple, - { - replayProtection: { - publicKeys: [acidTest.getReplayProtectionPublicKey()], - }, - } - ); - } else { - throw new Error(`Invalid backend: ${backend}`); - } - } else if (txFormat === 'legacy') { - const pubs = acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()); - // Extract change info from PSBT to pass to explainLegacyTx - const changeInfo = getChangeInfoFromPsbt(psbt); - explanation = explainLegacyTx(tx, { pubs, changeInfo }, coinName); + if (backend === 'utxolib') { + explanation = explainPsbt(psbt, { pubs: acidTest.rootWalletKeys }, coinName, { + strict: true, + }); + } else if (backend === 'wasm') { + const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes( + psbt.toBuffer(), + utxolib.getNetworkName(acidTest.network)! + ); + explanation = explainPsbtWasm( + wasmPsbt, + acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple, + { + replayProtection: { + publicKeys: [acidTest.getReplayProtectionPublicKey()], + }, + } + ); } else { - throw new Error(`Invalid txFormat: ${txFormat}`); + throw new Error(`Invalid backend: ${backend}`); } // Determine txParams @@ -198,18 +160,9 @@ function describeParseTransactionWith( // Stub explainTransaction to return the explanation without making network calls stubExplainTransaction = sinon.stub(coin, 'explainTransaction').resolves(explanation); - let txPrebuild: TransactionPrebuild; - if (txFormat === 'psbt') { - txPrebuild = { - txHex: psbt.toHex(), - }; - } else if (txFormat === 'legacy') { - txPrebuild = { - txHex: psbt.getUnsignedTx().toHex(), - }; - } else { - throw new Error(`Invalid txFormat: ${txFormat}`); - } + const txPrebuild: TransactionPrebuild = { + txHex: psbt.toHex(), + }; refParsedTransaction = await parseTransaction(coin, { wallet: mockWallet as unknown as UtxoWallet, @@ -280,13 +233,6 @@ function describeTransaction( } // extended test suite for bitcoin - describeParseTransactionWith(test, 'legacy', backend, { - txFormat: 'legacy', - txParams: 'inferFromExplanation', - expectedExplicitExternalSpendAmount: 1800n, - expectedImplicitExternalSpendAmount: 0n, - }); - describeParseTransactionWith(test, 'empty recipients', backend, { txParams: { recipients: [], From 4242ff3925b5e0921989a1e46d8d2951fcea1c4b Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 13:39:43 +0200 Subject: [PATCH 3/4] refactor(abstract-utxo): replace utxolib.bitgo.isPsbt with hasPsbtMagic Use wasm-utxo's hasPsbtMagic and the local toTNumber helper in place of utxolib.bitgo.isPsbt and utxolib.bitgo.toTNumber in abstractUtxoCoin.ts and the fixed-script verifyTransaction path. Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 4 ++-- .../src/transaction/fixedScript/verifyTransaction.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 9180f93c9d..818bd23de9 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -3,7 +3,7 @@ import { randomBytes } from 'crypto'; import _ from 'lodash'; import * as utxolib from '@bitgo/utxo-lib'; -import { BIP32, fixedScriptWallet } from '@bitgo/wasm-utxo'; +import { BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; import { bitgo, getMainnet } from '@bitgo/utxo-lib'; import { AddressCoinSpecific, @@ -606,7 +606,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici decodeTransaction(input: Buffer | string): fixedScriptWallet.BitGoPsbt { const buffer = typeof input === 'string' ? stringToBufferTryFormats(input, ['hex', 'base64']) : input; - if (!utxolib.bitgo.isPsbt(buffer)) { + if (!hasPsbtMagic(buffer)) { throw new ErrorDeprecatedTxFormat('legacy'); } return decodePsbt(buffer, this.name); diff --git a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts index 9d313cf5b6..26d8501a68 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts @@ -2,10 +2,12 @@ import buildDebug from 'debug'; import _ from 'lodash'; import BigNumber from 'bignumber.js'; import { BitGoBase, TxIntentMismatchError, IBaseCoin } from '@bitgo/sdk-core'; -import * as utxolib from '@bitgo/utxo-lib'; +import { hasPsbtMagic } from '@bitgo/wasm-utxo'; import { AbstractUtxoCoin, VerifyTransactionOptions } from '../../abstractUtxoCoin'; import { Output, ParsedTransaction } from '../types'; +import { toTNumber } from '../../tnumber'; +import { stringToBufferTryFormats } from '../decode'; import { verifyCustomChangeKeySignatures, verifyKeySignature, verifyUserPublicKey } from '../../verifyKey'; import { getPsbtTxInputs, getTxInputs } from '../fetchInputs'; @@ -60,7 +62,7 @@ export async function verifyTransaction( if (!_.isUndefined(verification.disableNetworking) && !_.isBoolean(verification.disableNetworking)) { throw new TypeError('verification.disableNetworking must be a boolean'); } - const isPsbt = txPrebuild.txHex && utxolib.bitgo.isPsbt(txPrebuild.txHex); + const isPsbt = txPrebuild.txHex && hasPsbtMagic(stringToBufferTryFormats(txPrebuild.txHex, ['hex', 'base64'])); if (isPsbt && txPrebuild.txInfo?.unspents) { throw new Error('should not have unspents in txInfo for psbt'); } @@ -195,7 +197,7 @@ export async function verifyTransaction( const inputs = isPsbt ? getPsbtTxInputs(txPrebuild.txHex, coin.name).map((v) => ({ ...v, - value: utxolib.bitgo.toTNumber(v.value, coin.amountType), + value: toTNumber(v.value, coin.amountType), })) : await getTxInputs({ txPrebuild, bitgo, coin, disableNetworking, reqId }); // coins (doge) that can exceed number limits (and thus will use bigint) will have the `valueString` field From f3ecc520fb6aca5e4bfd4d75f89a458209b26d52 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 13:47:04 +0200 Subject: [PATCH 4/4] refactor(abstract-utxo): delete unreachable utxolib explainPsbt path The utxolib variant of explainPsbt is no longer called from any src file (transaction/explainTransaction.ts dispatches only to explainPsbtWasm). Delete it along with helpers that became dead, and migrate the few test callers to explainPsbtWasm. Refs: T1-3279 --- .../src/transaction/explainTransaction.ts | 1 - .../fixedScript/explainTransaction.ts | 353 +----------------- .../src/transaction/fixedScript/index.ts | 1 - .../transaction/fixedScript/explainPsbt.ts | 66 +--- .../unit/transaction/fixedScript/parsePsbt.ts | 56 ++- 5 files changed, 32 insertions(+), 445 deletions(-) diff --git a/modules/abstract-utxo/src/transaction/explainTransaction.ts b/modules/abstract-utxo/src/transaction/explainTransaction.ts index 7cd21f0453..9da1ecc890 100644 --- a/modules/abstract-utxo/src/transaction/explainTransaction.ts +++ b/modules/abstract-utxo/src/transaction/explainTransaction.ts @@ -23,7 +23,6 @@ export function explainTx( pubs?: string[]; customChangeXpubs?: Triple; txInfo?: { unspents?: Unspent[] }; - changeInfo?: fixedScript.ChangeAddressInfo[]; }, coinName: UtxoCoinName ): TransactionExplanationUtxolibPsbt | TransactionExplanationWasm { diff --git a/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts index d38e18fc73..3e5738b7bb 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/explainTransaction.ts @@ -1,17 +1,7 @@ -import * as utxolib from '@bitgo/utxo-lib'; -import { bip322 } from '@bitgo/utxo-core'; -import { bitgo } from '@bitgo/utxo-lib'; -import { ITransactionExplanation as BaseTransactionExplanation, Triple } from '@bitgo/sdk-core'; -import { BIP32 } from '@bitgo/wasm-utxo'; -import * as utxocore from '@bitgo/utxo-core'; +import { ITransactionExplanation as BaseTransactionExplanation } from '@bitgo/sdk-core'; import type { Bip322Message } from '../../abstractUtxoCoin'; import type { Output, FixedScriptWalletOutput } from '../types'; -import { toExtendedAddressFormat } from '../recipient'; -import { getPayGoVerificationPubkey } from '../getPayGoVerificationPubkey'; -import { toBip32Triple } from '../../keychains'; -import { toUtxolibBIP32 } from '../../wasmUtil'; -import { getNetworkFromCoinName, UtxoCoinName } from '../../names'; // ===== Transaction Explanation Type Definitions ===== @@ -61,344 +51,3 @@ export type TransactionExplanationUtxolibPsbt = TransactionExplanationWithSignat export type TransactionExplanationDescriptor = TransactionExplanationWithSignatures; export type TransactionExplanation = TransactionExplanationUtxolibPsbt | TransactionExplanationWasm; - -export type ChangeAddressInfo = { - address: string; - chain: number; - index: number; -}; - -function toChangeOutput( - txOutput: utxolib.TxOutput, - coinName: UtxoCoinName, - changeInfo: ChangeAddressInfo[] | undefined -): FixedScriptWalletOutput | undefined { - if (!changeInfo) { - return undefined; - } - const address = toExtendedAddressFormat(txOutput.script, coinName); - const change = changeInfo.find((change) => change.address === address); - if (!change) { - return undefined; - } - return { - address, - amount: txOutput.value.toString(), - chain: change.chain, - index: change.index, - external: false, - }; -} - -function outputSum(outputs: { amount: string | number }[]): bigint { - return outputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0)); -} - -function explainCommon( - tx: bitgo.UtxoTransaction, - params: { - changeInfo?: ChangeAddressInfo[]; - customChangeInfo?: ChangeAddressInfo[]; - feeInfo?: string; - }, - coinName: UtxoCoinName -) { - const displayOrder = ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs']; - const changeOutputs: FixedScriptWalletOutput[] = []; - const customChangeOutputs: FixedScriptWalletOutput[] = []; - const externalOutputs: Output[] = []; - - const { changeInfo, customChangeInfo } = params; - - tx.outs.forEach((currentOutput) => { - // Try to encode the script pubkey with an address. If it fails, try to parse it as an OP_RETURN output with the prefix. - // If that fails, then it is an unrecognized scriptPubkey and should fail - const currentAddress = toExtendedAddressFormat(currentOutput.script, coinName); - const currentAmount = BigInt(currentOutput.value); - - const changeOutput = toChangeOutput(currentOutput, coinName, changeInfo); - if (changeOutput) { - changeOutputs.push(changeOutput); - return; - } - - const customChangeOutput = toChangeOutput(currentOutput, coinName, customChangeInfo); - if (customChangeOutput) { - customChangeOutputs.push(customChangeOutput); - return; - } - - externalOutputs.push({ - address: currentAddress, - amount: currentAmount.toString(), - // If changeInfo has a length greater than or equal to zero, it means that the change information - // was provided to the function but the output was not identified as change. In this case, - // the output is external, and we can set it as so. If changeInfo is undefined, it means we were - // given no information about change outputs, so we can't determine anything about the output, - // so we leave it undefined. - external: changeInfo ? true : undefined, - }); - }); - - const outputDetails = { - outputs: externalOutputs, - outputAmount: outputSum(externalOutputs).toString(), - - changeOutputs, - changeAmount: outputSum(changeOutputs).toString(), - - customChangeAmount: outputSum(customChangeOutputs).toString(), - customChangeOutputs, - }; - - let fee: string | undefined; - let locktime: number | undefined; - - if (params.feeInfo) { - displayOrder.push('fee'); - fee = params.feeInfo; - } - - if (Number.isInteger(tx.locktime) && tx.locktime > 0) { - displayOrder.push('locktime'); - locktime = tx.locktime; - } - - return { displayOrder, id: tx.getId(), ...outputDetails, fee, locktime }; -} - -function getRootWalletKeys(params: { pubs?: bitgo.RootWalletKeys | string[] }): bitgo.RootWalletKeys | undefined { - if (params.pubs instanceof bitgo.RootWalletKeys) { - return params.pubs; - } - const keys = params.pubs?.map((xpub) => toUtxolibBIP32(BIP32.fromBase58(xpub))); - return keys && keys.length === 3 ? new bitgo.RootWalletKeys(keys as Triple) : undefined; -} - -function getPsbtInputSignaturesCount( - psbt: bitgo.UtxoPsbt, - params: { - pubs?: bitgo.RootWalletKeys | string[]; - } -) { - const rootWalletKeys = getRootWalletKeys(params); - return rootWalletKeys - ? bitgo.getSignatureValidationArrayPsbt(psbt, rootWalletKeys).map((sv) => sv[1].filter((v) => v).length) - : (Array(psbt.data.inputs.length) as number[]).fill(0); -} - -function getChainAndIndexFromBip32Derivations(output: bitgo.PsbtOutput) { - const derivations = output.bip32Derivation ?? output.tapBip32Derivation ?? undefined; - if (!derivations) { - return undefined; - } - const paths = derivations.map((d) => d.path); - if (!paths || paths.length !== 3) { - throw new Error('expected 3 paths in bip32Derivation or tapBip32Derivation'); - } - if (!paths.every((p) => paths[0] === p)) { - throw new Error('expected all paths to be the same'); - } - - paths.forEach((path) => { - if (paths[0] !== path) { - throw new Error( - 'Unable to get a single chain and index on the output because there are different paths for different keys' - ); - } - }); - return utxolib.bitgo.getChainAndIndexFromPath(paths[0]); -} - -function getChangeInfo( - psbt: bitgo.UtxoPsbt, - walletKeys?: Triple | Triple -): ChangeAddressInfo[] | undefined { - let utxolibKeys: Triple; - try { - utxolibKeys = walletKeys - ? (walletKeys.map((k) => toUtxolibBIP32(k)) as Triple) - : utxolib.bitgo.getSortedRootNodes(psbt); - } catch (e) { - if (e instanceof utxolib.bitgo.ErrorNoMultiSigInputFound) { - return undefined; - } - throw e; - } - - return utxolib.bitgo.findWalletOutputIndices(psbt, utxolibKeys).map((i) => { - const derivationInformation = getChainAndIndexFromBip32Derivations(psbt.data.outputs[i]); - if (!derivationInformation) { - throw new Error('could not find derivation information on bip32Derivation or tapBip32Derivation'); - } - return { - address: utxolib.address.fromOutputScript(psbt.txOutputs[i].script, psbt.network), - external: false, - ...derivationInformation, - }; - }); -} - -/** - * Extract PayGo address proof information from the PSBT if present - * @returns Information about the PayGo proof, including the output index and address - */ -function getPayGoVerificationInfo( - psbt: bitgo.UtxoPsbt, - coinName: UtxoCoinName -): { outputIndex: number; verificationPubkey: string } | undefined { - let outputIndex: number | undefined = undefined; - let address: string | undefined = undefined; - // Check if this PSBT has any PayGo address proofs - if (!utxocore.paygo.psbtOutputIncludesPaygoAddressProof(psbt)) { - return undefined; - } - - // This pulls the pubkey depending on given network - const verificationPubkey = getPayGoVerificationPubkey(coinName); - // find which output index that contains the PayGo proof - outputIndex = utxocore.paygo.getPayGoAddressProofOutputIndex(psbt); - if (outputIndex === undefined || !verificationPubkey) { - return undefined; - } - const network = getNetworkFromCoinName(coinName); - const output = psbt.txOutputs[outputIndex]; - address = utxolib.address.fromOutputScript(output.script, network); - if (!address) { - throw new Error(`Can not derive address ${address} Pay Go Attestation.`); - } - - return { outputIndex, verificationPubkey }; -} - -/** - * Extract the BIP322 messages and addresses from the PSBT inputs and perform - * verification on the transaction to ensure that it meets the BIP322 requirements. - * @returns An array of objects containing the message and address for each input, - * or undefined if no BIP322 messages are found. - */ -function getBip322MessageInfoAndVerify(psbt: bitgo.UtxoPsbt, coinName: UtxoCoinName): Bip322Message[] | undefined { - const network = getNetworkFromCoinName(coinName); - const bip322Messages: { message: string; address: string }[] = []; - for (let i = 0; i < psbt.data.inputs.length; i++) { - const message = bip322.getBip322ProofMessageAtIndex(psbt, i); - if (message) { - const input = psbt.data.inputs[i]; - if (!input.witnessUtxo) { - throw new Error(`Missing witnessUtxo for input index ${i}`); - } - const scriptPubKey = input.witnessUtxo.script; - - // Verify that the toSpend transaction can be recreated in the PSBT and is encoded correctly in the nonWitnessUtxo - const toSpend = bip322.buildToSpendTransaction(scriptPubKey, message); - - // Verify that the toSpend transaction ID matches the input's referenced transaction ID - if (toSpend.getId() !== utxolib.bitgo.getOutputIdForInput(psbt.txInputs[i]).txid) { - throw new Error(`ToSpend transaction ID does not match the input at index ${i}`); - } - - // Verify the input specifics - if (psbt.txInputs[i].sequence !== 0) { - throw new Error(`Unexpected sequence number at input index ${i}: ${psbt.txInputs[i].sequence}. Expected 0.`); - } - if (psbt.txInputs[i].index !== 0) { - throw new Error(`Unexpected input index at position ${i}: ${psbt.txInputs[i].index}. Expected 0.`); - } - - bip322Messages.push({ - message: message.toString('utf8'), - address: utxolib.address.fromOutputScript(scriptPubKey, network), - }); - } - } - - if (bip322Messages.length > 0) { - // If there is a BIP322 message in any input, all inputs must have one. - if (bip322Messages.length !== psbt.data.inputs.length) { - throw new Error('Inconsistent BIP322 messages across inputs.'); - } - - // Verify the transaction specifics for BIP322 - if (psbt.version !== 0 && psbt.version !== 2) { - throw new Error(`Unsupported PSBT version for BIP322: ${psbt.version}. Expected 0 `); - } - if ( - psbt.data.outputs.length !== 1 || - psbt.txOutputs[0].script.toString('hex') !== '6a' || - psbt.txOutputs[0].value !== 0n - ) { - throw new Error(`Invalid PSBT outputs for BIP322. Expected exactly one OP_RETURN output with zero value.`); - } - - return bip322Messages; - } - - return undefined; -} - -/** - * Decompose a raw psbt into useful information, such as the total amounts, - * change amounts, and transaction outputs. - * - * @param psbt {bitgo.UtxoPsbt} The PSBT to explain - * @param pubs {bitgo.RootWalletKeys | string[]} The public keys to use for the explanation - * @param coinName {UtxoCoinName} The coin name to use for the explanation - * @param strict {boolean} Whether to throw an error if the PayGo address proof is invalid - */ -export function explainPsbt( - psbt: bitgo.UtxoPsbt, - params: { - pubs?: bitgo.RootWalletKeys | string[]; - customChangePubs?: bitgo.RootWalletKeys | string[]; - }, - coinName: UtxoCoinName, - { strict = true }: { strict?: boolean } = {} -): TransactionExplanationUtxolibPsbt { - const network = getNetworkFromCoinName(coinName); - const payGoVerificationInfo = getPayGoVerificationInfo(psbt, coinName); - if (payGoVerificationInfo) { - try { - utxocore.paygo.verifyPayGoAddressProof( - psbt, - payGoVerificationInfo.outputIndex, - Buffer.from(BIP32.fromBase58(payGoVerificationInfo.verificationPubkey).publicKey) - ); - } catch (e) { - if (strict) { - throw e; - } - console.error(e); - } - } - - const messages = getBip322MessageInfoAndVerify(psbt, coinName); - const changeInfo = getChangeInfo(psbt); - const customChangeInfo = params.customChangePubs - ? getChangeInfo(psbt, toBip32Triple(params.customChangePubs)) - : undefined; - const tx = psbt.getUnsignedTx(); - const common = explainCommon(tx, { ...params, changeInfo, customChangeInfo }, coinName); - const inputSignaturesCount = getPsbtInputSignaturesCount(psbt, params); - - // Set fee from subtracting inputs from outputs - const outputAmount = psbt.txOutputs.reduce((cumulative, curr) => cumulative + BigInt(curr.value), BigInt(0)); - const inputAmount = psbt.txInputs.reduce((cumulative, txInput, i) => { - const data = psbt.data.inputs[i]; - if (data.witnessUtxo) { - return cumulative + BigInt(data.witnessUtxo.value); - } else if (data.nonWitnessUtxo) { - const tx = bitgo.createTransactionFromBuffer(data.nonWitnessUtxo, network, { amountType: 'bigint' }); - return cumulative + BigInt(tx.outs[txInput.index].value); - } else { - throw new Error('could not find value on input'); - } - }, BigInt(0)); - - return { - ...common, - fee: (inputAmount - outputAmount).toString(), - inputSignatures: inputSignaturesCount, - signatures: inputSignaturesCount.reduce((prev, curr) => (curr > prev ? curr : prev), 0), - messages, - }; -} diff --git a/modules/abstract-utxo/src/transaction/fixedScript/index.ts b/modules/abstract-utxo/src/transaction/fixedScript/index.ts index e9fd1cbdc7..3fdccbe180 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/index.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/index.ts @@ -1,4 +1,3 @@ -export { explainPsbt, ChangeAddressInfo } from './explainTransaction'; export { explainPsbtWasm, explainPsbtWasmBigInt, diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts index fe19d2a92a..1d7e29d091 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts @@ -4,31 +4,21 @@ import * as utxolib from '@bitgo/utxo-lib'; import { testutil } from '@bitgo/utxo-lib'; import { fixedScriptWallet } from '@bitgo/wasm-utxo'; -import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction'; import { - explainPsbt, explainPsbtWasm, explainPsbtWasmBigInt, aggregateTransactionExplanations, type TransactionExplanationBigInt, } from '../../../../src/transaction/fixedScript'; -import { getCoinName } from '../../../../src/names'; function describeTransactionWith(acidTest: testutil.AcidTest) { describe(`${acidTest.name}`, function () { - let psbt: utxolib.bitgo.UtxoPsbt; - let psbtBytes: Buffer; let walletXpubs: fixedScriptWallet.RootWalletKeys; let customChangeWalletXpubs: fixedScriptWallet.RootWalletKeys | undefined; let wasmPsbt: fixedScriptWallet.BitGoPsbt; - let refExplanation: TransactionExplanation; before('prepare', function () { - psbt = acidTest.createPsbt(); - const coinName = getCoinName(acidTest.network); - refExplanation = explainPsbt(psbt, { pubs: acidTest.rootWalletKeys }, coinName, { - strict: true, - }); - psbtBytes = psbt.toBuffer(); + const psbt = acidTest.createPsbt(); + const psbtBytes = psbt.toBuffer(); const networkName = utxolib.getNetworkName(acidTest.network); assert(networkName); walletXpubs = fixedScriptWallet.RootWalletKeys.from(acidTest.rootWalletKeys); @@ -36,55 +26,19 @@ function describeTransactionWith(acidTest: testutil.AcidTest) { wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbtBytes, networkName); }); - it('should match the expected values for explainPsbt', function () { - // note: `outputs` means external outputs here - assert.strictEqual(refExplanation.outputs.length, 3); - assert.strictEqual(refExplanation.changeOutputs.length, acidTest.outputs.length - 3); - assert.strictEqual(refExplanation.outputAmount, '1800'); - assert.strictEqual(refExplanation.changeOutputs.length, acidTest.outputs.length - 3); - refExplanation.changeOutputs.forEach((change) => { - assert.strictEqual(change.amount, '900'); - assert.strictEqual(typeof change.address, 'string'); - }); - }); - - it('reference implementation should support custom change outputs', function () { - const coinName = getCoinName(acidTest.network); - const customChangeExplanation = explainPsbt( - psbt, - { pubs: acidTest.rootWalletKeys, customChangePubs: acidTest.otherWalletKeys }, - coinName, - { strict: true } - ); - assert.ok(customChangeExplanation.customChangeOutputs); - assert.strictEqual(customChangeExplanation.changeOutputs.length, refExplanation.changeOutputs.length); - assert.strictEqual(customChangeExplanation.outputs.length, refExplanation.outputs.length - 1); - assert.strictEqual(customChangeExplanation.customChangeOutputs.length, 1); - assert.strictEqual(customChangeExplanation.customChangeOutputs[0].amount, '900'); - }); - - it('should match explainPsbtWasm', function () { + it('should return expected outputs from explainPsbtWasm', function () { const wasmExplanation = explainPsbtWasm(wasmPsbt, walletXpubs, { replayProtection: { publicKeys: [acidTest.getReplayProtectionPublicKey()], }, }); - - for (const key of Object.keys(refExplanation)) { - const refValue = refExplanation[key]; - const wasmValue = wasmExplanation[key]; - switch (key) { - case 'displayOrder': - case 'inputSignatures': - case 'signatures': - // these are deprecated fields that we want to get rid of - assert.deepStrictEqual(wasmValue, undefined); - break; - default: - assert.deepStrictEqual(wasmValue, refValue, `mismatch for key ${key}`); - break; - } - } + assert.strictEqual(wasmExplanation.outputs.length, 3); + assert.strictEqual(wasmExplanation.changeOutputs.length, acidTest.outputs.length - 3); + assert.strictEqual(wasmExplanation.outputAmount, '1800'); + wasmExplanation.changeOutputs.forEach((change) => { + assert.strictEqual(change.amount, '900'); + assert.strictEqual(typeof change.address, 'string'); + }); // verify new fields are present and stringified assert.strictEqual(typeof wasmExplanation.inputAmount, 'string'); diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts index d14cbbbd58..d129f40614 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts @@ -9,7 +9,7 @@ import { parseTransaction } from '../../../../src/transaction/fixedScript/parseT import { ParsedTransaction } from '../../../../src/transaction/types'; import { UtxoWallet } from '../../../../src/wallet'; import { getUtxoCoin } from '../../util'; -import { explainPsbt, explainPsbtWasm } from '../../../../src/transaction/fixedScript'; +import { explainPsbtWasm } from '../../../../src/transaction/fixedScript'; import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction'; import { getCoinName } from '../../../../src/names'; import { TransactionPrebuild } from '../../../../src/abstractUtxoCoin'; @@ -52,7 +52,6 @@ function getTxParamsFromExplanation( function describeParseTransactionWith( acidTest: utxolib.testutil.AcidTest, label: string, - backend: 'utxolib' | 'wasm', { txParams, externalCustomChangeAddress = false, @@ -87,28 +86,19 @@ function describeParseTransactionWith( const tx = psbt.getUnsignedTx(); const txHash = tx.getId(); - let explanation: TransactionExplanation; - if (backend === 'utxolib') { - explanation = explainPsbt(psbt, { pubs: acidTest.rootWalletKeys }, coinName, { - strict: true, - }); - } else if (backend === 'wasm') { - const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes( - psbt.toBuffer(), - utxolib.getNetworkName(acidTest.network)! - ); - explanation = explainPsbtWasm( - wasmPsbt, - acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple, - { - replayProtection: { - publicKeys: [acidTest.getReplayProtectionPublicKey()], - }, - } - ); - } else { - throw new Error(`Invalid backend: ${backend}`); - } + const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes( + psbt.toBuffer(), + utxolib.getNetworkName(acidTest.network)! + ); + const explanation: TransactionExplanation = explainPsbtWasm( + wasmPsbt, + acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple, + { + replayProtection: { + publicKeys: [acidTest.getReplayProtectionPublicKey()], + }, + } + ); // Determine txParams let resolvedTxParams; @@ -213,16 +203,13 @@ function describeParseTransactionWith( }); } -function describeTransaction( - backend: 'utxolib' | 'wasm', - filter: (test: utxolib.testutil.AcidTest) => boolean = () => true -) { - describe(`parseTransaction (${backend})`, function () { +function describeTransaction(filter: (test: utxolib.testutil.AcidTest) => boolean = () => true) { + describe(`parseTransaction`, function () { utxolib.testutil.AcidTest.suite() .filter(filter) .forEach((test) => { // Default case: psbt format, infer recipients from explanation - describeParseTransactionWith(test, 'default', backend, { + describeParseTransactionWith(test, 'default', { txParams: 'inferFromExplanation', expectedExplicitExternalSpendAmount: 1800n, expectedImplicitExternalSpendAmount: 0n, @@ -233,7 +220,7 @@ function describeTransaction( } // extended test suite for bitcoin - describeParseTransactionWith(test, 'empty recipients', backend, { + describeParseTransactionWith(test, 'empty recipients', { txParams: { recipients: [], }, @@ -241,7 +228,7 @@ function describeTransaction( expectedImplicitExternalSpendAmount: 1800n, }); - describeParseTransactionWith(test, 'rbf', backend, { + describeParseTransactionWith(test, 'rbf', { txParams: { rbfTxIds: ['PLACEHOLDER'], }, @@ -249,7 +236,7 @@ function describeTransaction( expectedImplicitExternalSpendAmount: 0n, }); - describeParseTransactionWith(test, 'allowExternalChangeAddress', backend, { + describeParseTransactionWith(test, 'allowExternalChangeAddress', { txParams: 'inferFromExplanation', externalCustomChangeAddress: true, expectedExplicitExternalSpendAmount: 1800n, @@ -259,5 +246,4 @@ function describeTransaction( }); } -describeTransaction('utxolib'); -describeTransaction('wasm'); +describeTransaction();