From b1b82deec28bf42e36bb8144630e7dac0738e0d8 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 13:08:07 +0200 Subject: [PATCH 01/13] feat(abstract-utxo): disable legacy tx format and utxolib backend for all coins Set supportedTxFormats.legacy and supportedSdkBackends.utxolib to false unconditionally, completing the rollout to all coins. Refs: T1-3245, T1-3280 Co-authored-by: llm-git --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 8c84f8db46..0a14c88fa0 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -436,11 +436,11 @@ export abstract class AbstractUtxoCoin protected supportedTxFormats: { psbt: boolean; legacy: boolean } = { psbt: true, - legacy: this.getChain() === 'btc', + legacy: false, }; protected supportedSdkBackends: { utxolib: boolean; 'wasm-utxo': boolean } = { - utxolib: this.getChain() === 'btc', + utxolib: false, 'wasm-utxo': true, }; From 3e49cb6153f59010eb195e49ffcb49b5fe15ed40 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 13:09:28 +0200 Subject: [PATCH 02/13] 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 0a14c88fa0..5f32949295 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -434,16 +434,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; @@ -634,25 +624,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 5bc2decdf6963b2508d9264b38260458d68a534f Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 13:18:14 +0200 Subject: [PATCH 03/13] 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 5f32949295..fc330faa64 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -85,13 +85,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'; @@ -249,7 +244,6 @@ export interface ExplainTransactionOptions; customChangeXpubs?: Triple; - decodeWith?: SdkBackend; } export interface DecoratedExplainTransactionOptions @@ -262,7 +256,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). @@ -328,7 +321,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; @@ -417,10 +409,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; /** @@ -439,8 +428,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. @@ -614,52 +601,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); } /** @@ -811,26 +775,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 6c166906b6dbc0bbcbcea5c736e466afd5d79d51 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 13:39:43 +0200 Subject: [PATCH 04/13] 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 fc330faa64..8da03e48c7 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, @@ -603,7 +603,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 c7a2f9ab5d..986df757d5 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'); } @@ -180,7 +182,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 e63a77a9b26994256cd527dcd36340cbfbe8f620 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 13:47:04 +0200 Subject: [PATCH 05/13] 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(); From 00f39174e46f9c33e98ef7de9f7ac96fb9824a12 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 13:54:30 +0200 Subject: [PATCH 06/13] refactor(abstract-utxo): eliminate fetchInputs by reusing explainPsbtWasm inputAmount The legacy getTxInputs branch is unreachable (decodeTransaction throws ErrorDeprecatedTxFormat for non-PSBT input), and getPsbtTxInputs's only remaining caller in verifyTransaction just sums input values to check fee >= 0. explainPsbtWasm already returns inputAmount, so reuse that and delete fetchInputs.ts entirely. Refs: T1-3279 --- .../src/transaction/fetchInputs.ts | 86 ------------------- .../fixedScript/verifyTransaction.ts | 24 +----- .../abstract-utxo/src/transaction/index.ts | 1 - 3 files changed, 1 insertion(+), 110 deletions(-) delete mode 100644 modules/abstract-utxo/src/transaction/fetchInputs.ts diff --git a/modules/abstract-utxo/src/transaction/fetchInputs.ts b/modules/abstract-utxo/src/transaction/fetchInputs.ts deleted file mode 100644 index 01459430a4..0000000000 --- a/modules/abstract-utxo/src/transaction/fetchInputs.ts +++ /dev/null @@ -1,86 +0,0 @@ -import * as utxolib from '@bitgo/utxo-lib'; -import { BitGoBase, IRequestTracer } from '@bitgo/sdk-core'; - -import { AbstractUtxoCoin, TransactionPrebuild } from '../abstractUtxoCoin'; -import { getNetworkFromCoinName, UtxoCoinName } from '../names'; - -/** - * Get the inputs for a psbt from a prebuild. - */ -export function getPsbtTxInputs( - psbtArg: string | utxolib.bitgo.UtxoPsbt, - coinName: UtxoCoinName -): { address: string; value: bigint; valueString: string }[] { - const network = getNetworkFromCoinName(coinName); - const psbt = psbtArg instanceof utxolib.bitgo.UtxoPsbt ? psbtArg : utxolib.bitgo.createPsbtFromHex(psbtArg, network); - const txInputs = psbt.txInputs; - return psbt.data.inputs.map((input, index) => { - let address: string; - let value: bigint; - if (input.witnessUtxo) { - address = utxolib.address.fromOutputScript(input.witnessUtxo.script, network); - value = input.witnessUtxo.value; - } else if (input.nonWitnessUtxo) { - const tx = utxolib.bitgo.createTransactionFromBuffer(input.nonWitnessUtxo, network, { - amountType: 'bigint', - }); - const txId = (Buffer.from(txInputs[index].hash).reverse() as Buffer).toString('hex'); - if (tx.getId() !== txId) { - throw new Error('input transaction hex does not match id'); - } - const prevTxOutputIndex = txInputs[index].index; - address = utxolib.address.fromOutputScript(tx.outs[prevTxOutputIndex].script, network); - value = tx.outs[prevTxOutputIndex].value; - } else { - throw new Error('psbt input is missing both witnessUtxo and nonWitnessUtxo'); - } - return { address, value, valueString: value.toString() }; - }); -} - -/** - * Get the inputs for a transaction from a prebuild. - */ -export async function getTxInputs(params: { - txPrebuild: TransactionPrebuild; - bitgo: BitGoBase; - coin: AbstractUtxoCoin; - disableNetworking: boolean; - reqId?: IRequestTracer; -}): Promise<{ address: string; value: TNumber; valueString: string }[]> { - const { txPrebuild, bitgo, coin, disableNetworking, reqId } = params; - if (!txPrebuild.txHex) { - throw new Error(`txPrebuild.txHex not set`); - } - const transaction = coin.createTransactionFromHex(txPrebuild.txHex); - const transactionCache = {}; - return await Promise.all( - transaction.ins.map(async (currentInput): Promise<{ address: string; value: TNumber; valueString: string }> => { - const transactionId = (Buffer.from(currentInput.hash).reverse() as Buffer).toString('hex'); - const txHex = txPrebuild.txInfo?.txHexes?.[transactionId]; - if (txHex) { - const localTx = coin.createTransactionFromHex(txHex); - if (localTx.getId() !== transactionId) { - throw new Error('input transaction hex does not match id'); - } - const currentOutput = localTx.outs[currentInput.index]; - const address = utxolib.address.fromOutputScript(currentOutput.script, coin.network); - return { - address, - value: currentOutput.value, - valueString: currentOutput.value.toString(), - }; - } else if (!transactionCache[transactionId]) { - if (disableNetworking) { - throw new Error('attempting to retrieve transaction details externally with networking disabled'); - } - if (reqId) { - bitgo.setRequestTracer(reqId); - } - transactionCache[transactionId] = await bitgo.get(coin.url(`/public/tx/${transactionId}`)).result(); - } - const transactionDetails = transactionCache[transactionId]; - return transactionDetails.outputs[currentInput.index]; - }) - ); -} diff --git a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts index 986df757d5..8b7659953b 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts @@ -5,11 +5,9 @@ import { BitGoBase, TxIntentMismatchError, IBaseCoin } from '@bitgo/sdk-core'; import { hasPsbtMagic } from '@bitgo/wasm-utxo'; import { AbstractUtxoCoin, VerifyTransactionOptions } from '../../abstractUtxoCoin'; -import { Output, ParsedTransaction } from '../types'; -import { toTNumber } from '../../tnumber'; +import { ParsedTransaction } from '../types'; import { stringToBufferTryFormats } from '../decode'; import { verifyCustomChangeKeySignatures, verifyKeySignature, verifyUserPublicKey } from '../../verifyKey'; -import { getPsbtTxInputs, getTxInputs } from '../fetchInputs'; const debug = buildDebug('bitgo:abstract-utxo:verifyTransaction'); @@ -175,29 +173,9 @@ export async function verifyTransaction( } } - const allOutputs = parsedTransaction.outputs; if (!txPrebuild.txHex) { throw new Error(`txPrebuild.txHex not set`); } - const inputs = isPsbt - ? getPsbtTxInputs(txPrebuild.txHex, coin.name).map((v) => ({ - ...v, - 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 - const inputAmount = inputs.reduce( - (sum: bigint, i) => sum + BigInt(coin.amountType === 'bigint' ? i.valueString : i.value), - BigInt(0) - ); - const outputAmount = allOutputs.reduce((sum: bigint, o: Output) => sum + BigInt(o.amount), BigInt(0)); - const fee = inputAmount - outputAmount; - - if (fee < 0) { - throw new Error( - `attempting to spend ${outputAmount} satoshis, which exceeds the input amount (${inputAmount} satoshis) by ${-fee}` - ); - } return true; } diff --git a/modules/abstract-utxo/src/transaction/index.ts b/modules/abstract-utxo/src/transaction/index.ts index 075ef05742..d0231ff6cb 100644 --- a/modules/abstract-utxo/src/transaction/index.ts +++ b/modules/abstract-utxo/src/transaction/index.ts @@ -3,7 +3,6 @@ export * from './recipient'; export { explainTx } from './explainTransaction'; export { parseTransaction } from './parseTransaction'; export { verifyTransaction } from './verifyTransaction'; -export * from './fetchInputs'; export * as bip322 from './bip322'; export { decodePsbt } from './decode'; export * from './fixedScript'; From a0b4aacbb04f767b65dcf9c7000bbd7a59212d5a Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 14:11:03 +0200 Subject: [PATCH 07/13] refactor(abstract-utxo): remove createTransactionFromHex from AbstractUtxoCoin The only remaining caller was the deleted fetchInputs and the doge override that just delegated to super. Remove the method, drop the doge override, and remove the now-unused createTransactionFromHex stubs from verifyTransaction tests. Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 6 ------ modules/abstract-utxo/src/impl/doge/doge.ts | 5 ----- .../src/recovery/backupKeyRecovery.ts | 6 +++--- .../abstract-utxo/test/unit/verifyTransaction.ts | 16 ---------------- 4 files changed, 3 insertions(+), 30 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 8da03e48c7..b820c0246a 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -595,12 +595,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici return isChainCode(addressDetails.chain) ? scriptTypeForChain(addressDetails.chain) : null; } - createTransactionFromHex( - hex: string - ): utxolib.bitgo.UtxoTransaction { - return utxolib.bitgo.createTransactionFromHex(hex, this.network, this.amountType); - } - decodeTransaction(input: Buffer | string): fixedScriptWallet.BitGoPsbt { const buffer = typeof input === 'string' ? stringToBufferTryFormats(input, ['hex', 'base64']) : input; if (!hasPsbtMagic(buffer)) { diff --git a/modules/abstract-utxo/src/impl/doge/doge.ts b/modules/abstract-utxo/src/impl/doge/doge.ts index 79fe2da15c..f1ba54f030 100644 --- a/modules/abstract-utxo/src/impl/doge/doge.ts +++ b/modules/abstract-utxo/src/impl/doge/doge.ts @@ -1,5 +1,4 @@ import { BitGoBase, HalfSignedUtxoTransaction, SignedTransaction } from '@bitgo/sdk-core'; -import { bitgo } from '@bitgo/utxo-lib'; import { AbstractUtxoCoin, @@ -75,10 +74,6 @@ export class Doge extends AbstractUtxoCoin { /* postProcessPrebuild, isBitGoTaintedUnspent, verifyCustomChangeKeySignatures do not care whether they receive number or bigint */ - createTransactionFromHex(hex: string): bitgo.UtxoTransaction { - return super.createTransactionFromHex(hex); - } - async parseTransaction( params: ParseTransactionOptions ): /* diff --git a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts index 8c902d1401..68d3f4b9c1 100644 --- a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts +++ b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts @@ -8,7 +8,7 @@ import { krsProviders, Triple, } from '@bitgo/sdk-core'; -import { BIP32, fixedScriptWallet } from '@bitgo/wasm-utxo'; +import { BIP32, fixedScriptWallet, Transaction } from '@bitgo/wasm-utxo'; import { AbstractUtxoCoin } from '../abstractUtxoCoin'; import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction'; @@ -172,8 +172,8 @@ async function queryBlockchainUnspentsPath( // json parse won't parse it correctly, so we requery the txid for the tx hex to decode here if (!Number.isSafeInteger(u.value)) { const txHex = await getPrevTx(txid); - const tx = coin.createTransactionFromHex(txHex); - val = tx.outs[vout].value; + const tx = Transaction.fromBytes(Buffer.from(txHex, 'hex')); + val = tx.getOutputs()[vout].value; } } // the api may return cashaddr's instead of legacy for BCH and BCHA diff --git a/modules/abstract-utxo/test/unit/verifyTransaction.ts b/modules/abstract-utxo/test/unit/verifyTransaction.ts index b7eabebf13..fc81f28f07 100644 --- a/modules/abstract-utxo/test/unit/verifyTransaction.ts +++ b/modules/abstract-utxo/test/unit/verifyTransaction.ts @@ -1,6 +1,5 @@ import assert from 'assert'; -import * as utxolib from '@bitgo/utxo-lib'; import * as sinon from 'sinon'; import { Wallet } from '@bitgo/sdk-core'; @@ -217,10 +216,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(coin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await coin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -234,7 +229,6 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); it('should not allow any implicit external outputs if paygo outputs are disallowed', async () => { @@ -284,10 +278,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(coin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await coin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -302,7 +292,6 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); it('should work with bigint amounts', async () => { @@ -332,10 +321,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(bigintCoin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await bigintCoin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -350,6 +335,5 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); }); From 8ce4d89e7b72f18cc93506221237704d7408130a Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 14:20:57 +0200 Subject: [PATCH 08/13] refactor(abstract-utxo): pass coinName directly to BitGoPsbt.fromBytes BitGoPsbt.fromBytes accepts NetworkName = UtxolibName | CoinName, so the round-trip through utxolib.getNetworkName is unnecessary. Drop the utxolib import from decode.ts. Refs: T1-3279 --- modules/abstract-utxo/src/transaction/decode.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/modules/abstract-utxo/src/transaction/decode.ts b/modules/abstract-utxo/src/transaction/decode.ts index 556e5e73d4..35f4337582 100644 --- a/modules/abstract-utxo/src/transaction/decode.ts +++ b/modules/abstract-utxo/src/transaction/decode.ts @@ -1,7 +1,6 @@ -import * as utxolib from '@bitgo/utxo-lib'; -import { fixedScriptWallet, hasPsbtMagic, Psbt as WasmPsbt, utxolibCompat } from '@bitgo/wasm-utxo'; +import { fixedScriptWallet, hasPsbtMagic, Psbt as WasmPsbt } from '@bitgo/wasm-utxo'; -import { getNetworkFromCoinName, UtxoCoinName } from '../names'; +import { UtxoCoinName } from '../names'; import { BitGoPsbt } from './types'; @@ -22,20 +21,11 @@ export function stringToBufferTryFormats(input: string, formats: BufferEncoding[ throw new Error('input must be a valid hex or base64 string'); } -function toNetworkName(coinName: UtxoCoinName): utxolibCompat.UtxolibName { - const network = getNetworkFromCoinName(coinName); - const networkName = utxolib.getNetworkName(network); - if (!networkName) { - throw new Error(`Invalid coinName: ${coinName}`); - } - return networkName; -} - export function decodePsbt(psbt: string | Buffer, coinName: UtxoCoinName): BitGoPsbt { if (typeof psbt === 'string') { psbt = Buffer.from(psbt, 'hex'); } - return fixedScriptWallet.BitGoPsbt.fromBytes(psbt, toNetworkName(coinName)); + return fixedScriptWallet.BitGoPsbt.fromBytes(psbt, coinName); } export type PrebuildLike = { From 0b8ec7c537123fe7b3d461939592b303f4aff469 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 14:38:48 +0200 Subject: [PATCH 09/13] refactor(abstract-utxo): replace 3 utxolib network/script helpers with wasm/coinName equivalents - supportsBlockTarget: compare coinName instead of utxolib network objects - isSupportedScriptType: use fixedScriptWallet.supportsScriptType from wasm-utxo - postProcessPrebuild: delete the psbt-lite detection block and the allowNonSegwitSigningWithoutPrevTx flag end-to-end (wasm-utxo signing is permissive by default and never read the flag) Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 29 +++---------------- .../fixedScript/signTransaction.ts | 2 -- .../src/transaction/signTransaction.ts | 1 - 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index b820c0246a..b1a56eab23 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -4,7 +4,7 @@ import { randomBytes } from 'crypto'; import _ from 'lodash'; import * as utxolib from '@bitgo/utxo-lib'; import { BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; -import { bitgo, getMainnet } from '@bitgo/utxo-lib'; +import { bitgo } from '@bitgo/utxo-lib'; import { AddressCoinSpecific, BaseCoin, @@ -331,11 +331,6 @@ type UtxoBaseSignTransactionOptions = * When false, creates half-signed transaction with placeholder signatures. */ isLastSignature?: boolean; - /** - * If true, allows signing a non-segwit input with a witnessUtxo instead requiring a previous - * transaction (nonWitnessUtxo) - */ - allowNonSegwitSigningWithoutPrevTx?: boolean; /** * When true, the signed transaction will be converted from PSBT to legacy format before returning. * Set automatically by presignTransaction() when the caller explicitly requested txFormat: 'legacy'. @@ -451,13 +446,8 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici /** Indicates whether the coin supports a block target */ supportsBlockTarget(): boolean { // FIXME: the SDK does not seem to use this anywhere so it is unclear what the purpose of this method is - switch (getMainnet(this.network)) { - case utxolib.networks.bitcoin: - case utxolib.networks.dogecoin: - return true; - default: - return false; - } + const mainnet = getMainnetCoinName(this.name); + return mainnet === 'btc' || mainnet === 'doge'; } sweepWithSendMany(): boolean { @@ -744,7 +734,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici * @returns true iff coin supports spending from unspentType */ supportsAddressType(addressType: ScriptType2Of3): boolean { - return utxolib.bitgo.outputScripts.isSupportedScriptType(this.network, addressType); + return fixedScriptWallet.supportsScriptType(this.name, addressType); } /** inherited doc */ @@ -1046,17 +1036,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici } const returnLegacyFormat = (params as Record).txFormat === 'legacy'; - - // In the case that we have a 'psbt-lite' transaction format, we want to indicate in signing to not fail - const txHex = (params.txHex ?? params.txPrebuild?.txHex) as string; - if ( - txHex && - utxolib.bitgo.isPsbt(txHex as string) && - utxolib.bitgo.isPsbtLite(utxolib.bitgo.createPsbtFromHex(txHex, this.network)) && - params.allowNonSegwitSigningWithoutPrevTx === undefined - ) { - return { ...params, allowNonSegwitSigningWithoutPrevTx: true, returnLegacyFormat }; - } return { ...params, returnLegacyFormat }; } diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts index 49c333c4f0..a9dec08532 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts @@ -33,8 +33,6 @@ export async function signTransaction( txInfo: { unspents?: Unspent[] } | undefined; isLastSignature: boolean; signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined; - /** deprecated */ - allowNonSegwitSigningWithoutPrevTx: boolean; pubs: string[] | undefined; cosignerPub: string | undefined; /** When true (default), extract finalized PSBT to legacy transaction format. When false, return finalized PSBT. */ diff --git a/modules/abstract-utxo/src/transaction/signTransaction.ts b/modules/abstract-utxo/src/transaction/signTransaction.ts index 23f27de260..7a08d0b79f 100644 --- a/modules/abstract-utxo/src/transaction/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/signTransaction.ts @@ -65,7 +65,6 @@ export async function signTransaction( txInfo: params.txPrebuild.txInfo, isLastSignature: params.isLastSignature ?? false, signingStep: params.signingStep, - allowNonSegwitSigningWithoutPrevTx: params.allowNonSegwitSigningWithoutPrevTx ?? false, pubs: params.pubs, cosignerPub: params.cosignerPub, extractTransaction: params.extractTransaction, From 8e3b4fbbddf48e2d076601723c614f7b826d27ae Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 14:43:35 +0200 Subject: [PATCH 10/13] refactor(abstract-utxo): migrate isValidAddress off utxolib.addressFormat Replace utxolib.addressFormat.toOutputScriptAndFormat / fromOutputScriptWithFormat with wasm-utxo's address.toOutputScriptWithCoin / fromOutputScriptWithCoin. Detect address format by trying each candidate and checking which round-trips byte-equal. Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index b1a56eab23..210887f4d8 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, hasPsbtMagic } from '@bitgo/wasm-utxo'; +import { address as wasmAddress, BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; import { bitgo } from '@bitgo/utxo-lib'; import { AddressCoinSpecific, @@ -494,15 +494,20 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici // At the time of writing, the only additional address format is bch cashaddr. const anyFormat = (param as { anyFormat: boolean } | undefined)?.anyFormat ?? true; try { - // Find out if the address is valid for any format. Tries all supported formats by default. - // Throws if address cannot be decoded with any format. - const [format, script] = utxolib.addressFormat.toOutputScriptAndFormat(address, this.network); - // unless anyFormat is set, only 'default' is allowed. - if (!anyFormat && format !== 'default') { - return false; + const script = wasmAddress.toOutputScriptWithCoin(address, this.name); + // Determine which format the input address was in by round-tripping + // through each candidate and checking byte-equality. 'default' is tried + // first so canonical default-format addresses early-exit. + for (const format of ['default', 'cashaddr'] as const) { + try { + if (wasmAddress.fromOutputScriptWithCoin(script, this.name, format) === address) { + return anyFormat || format === 'default'; + } + } catch { + // coin doesn't support this format; try the next one + } } - // make sure that address is in normal representation for given format. - return address === utxolib.addressFormat.fromOutputScriptWithFormat(script, format, this.network); + return false; } catch (e) { return false; } From 57edb6a007d6d0a26bbad78602809b99142d692c Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 14:50:17 +0200 Subject: [PATCH 11/13] refactor(abstract-utxo): replace utxolib.bitgo helpers and drop dead branches - isChainCode / scriptTypeForChain -> ChainCode.is / ChainCode.scriptType (wasm-utxo) - hasKeyPathSpendInput: drop unreachable utxolib.bitgo.UtxoPsbt branch and isTransactionWithKeyPathSpendInput call (DecodedTransaction is now BitGoPsbt only) - validAddressTypes: inline the 2-of-3 list instead of utxolib.outputScripts.scriptTypes2Of3 - Drop unused RootWalletKeys / UtxoNetwork type re-exports and the now-empty utxolib bitgo import Three utxolib references remain in abstractUtxoCoin: the ScriptType2Of3 type re-export, the deprecated network getter (still used by tests), and the top-level utxolib import that backs both. Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 210887f4d8..1a6911625d 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -4,7 +4,6 @@ import { randomBytes } from 'crypto'; import _ from 'lodash'; import * as utxolib from '@bitgo/utxo-lib'; import { address as wasmAddress, BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; -import { bitgo } from '@bitgo/utxo-lib'; import { AddressCoinSpecific, BaseCoin, @@ -61,7 +60,6 @@ import { getReplayProtectionPubkeys, isReplayProtectionUnspent } from './transac import { supportedCrossChainRecoveries } from './config'; import { assertValidTransactionRecipient, - DecodedTransaction, explainTx, fromExtendedAddressFormat, isScriptRecipient, @@ -141,28 +139,21 @@ type UtxoCustomSigningFunction = { }): Promise; }; -const { isChainCode, scriptTypeForChain, outputScripts } = bitgo; +const { ChainCode } = fixedScriptWallet; /** * Check if a decoded transaction has at least one taproot key path spend (MuSig2) input. - * Works for both utxolib UtxoPsbt and wasm-utxo BitGoPsbt. */ -function hasKeyPathSpendInput( - tx: DecodedTransaction, +function hasKeyPathSpendInput( + tx: fixedScriptWallet.BitGoPsbt, pubs: string[] | undefined, coinName: UtxoCoinName ): boolean { - if (tx instanceof bitgo.UtxoPsbt) { - return bitgo.isTransactionWithKeyPathSpendInput(tx); - } - if (tx instanceof fixedScriptWallet.BitGoPsbt) { - assert(pubs && isTriple(pubs), 'pub triple is required to check for key path spend inputs in wasm-utxo PSBT'); - const rootWalletKeys = fixedScriptWallet.RootWalletKeys.fromXpubs(pubs); - const replayProtection = { publicKeys: getReplayProtectionPubkeys(coinName) }; - const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, { replayProtection }); - return parsed.inputs.some((input) => input.scriptType === 'p2trMusig2KeyPath'); - } - return false; + assert(pubs && isTriple(pubs), 'pub triple is required to check for key path spend inputs in wasm-utxo PSBT'); + const rootWalletKeys = fixedScriptWallet.RootWalletKeys.fromXpubs(pubs); + const replayProtection = { publicKeys: getReplayProtectionPubkeys(coinName) }; + const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, { replayProtection }); + return parsed.inputs.some((input) => input.scriptType === 'p2trMusig2KeyPath'); } /** @@ -215,8 +206,6 @@ function convertValidationErrorToTxIntentMismatch( export type { DecodedTransaction } from './transaction/types'; -export type RootWalletKeys = bitgo.RootWalletKeys; - export type UtxoCoinSpecific = AddressCoinSpecific | DescriptorAddressCoinSpecific; export interface VerifyAddressOptions extends BaseVerifyAddressOptions { @@ -251,8 +240,6 @@ export interface DecoratedExplainTransactionOptions extends BaseTransactionPrebuild { txInfo?: TransactionInfo; blockHeight?: number; @@ -456,7 +443,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici /** @deprecated */ static get validAddressTypes(): ScriptType2Of3[] { - return [...outputScripts.scriptTypes2Of3]; + return ['p2sh', 'p2shP2wsh', 'p2wsh', 'p2tr', 'p2trMusig2']; } /** @@ -587,7 +574,9 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici * @param addressDetails */ static inferAddressType(addressDetails: { chain: number }): ScriptType2Of3 | null { - return isChainCode(addressDetails.chain) ? scriptTypeForChain(addressDetails.chain) : null; + return fixedScriptWallet.ChainCode.is(addressDetails.chain) + ? (fixedScriptWallet.ChainCode.scriptType(addressDetails.chain) as ScriptType2Of3) + : null; } decodeTransaction(input: Buffer | string): fixedScriptWallet.BitGoPsbt { @@ -752,7 +741,10 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici * @return true iff coin supports spending from chain */ supportsAddressChain(chain: number): boolean { - return isChainCode(chain) && this.supportsAddressType(utxolib.bitgo.scriptTypeForChain(chain)); + return ( + fixedScriptWallet.ChainCode.is(chain) && + this.supportsAddressType(fixedScriptWallet.ChainCode.scriptType(chain) as ScriptType2Of3) + ); } keyIdsForSigning(): number[] { From b53bc53f13b229cd1474d8e30140a5f6852e2544 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 15:01:54 +0200 Subject: [PATCH 12/13] refactor(abstract-utxo): remove deprecated network getter and getNetworkFromCoinName Drop the deprecated coin.network getter from AbstractUtxoCoin and the getNetworkFromCoinName helper from names.ts. Tests that previously called coin.network now call the test-local getNetworkForCoinName(coin.name) from util/utxoCoins. With this, names.ts no longer depends on utxolib. Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 17 +-- modules/abstract-utxo/src/names.ts | 108 ------------------ .../test/unit/buildSignSendLegacyFormat.ts | 10 +- .../abstract-utxo/test/unit/customSigner.ts | 13 ++- .../test/unit/prebuildAndSign.ts | 14 ++- .../test/unit/signTransaction.ts | 33 ++++-- .../abstract-utxo/test/unit/transaction.ts | 23 ++-- .../unit/transaction/fixedScript/parsePsbt.ts | 5 +- modules/abstract-utxo/test/unit/wallet.ts | 6 +- 9 files changed, 73 insertions(+), 156 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 1a6911625d..d33d1b7dd8 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -74,14 +74,7 @@ import { ErrorImplicitExternalOutputs, } from './transaction/descriptor/verifyTransaction'; import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptorWallet } from './descriptor'; -import { - getFullNameFromCoinName, - getMainnetCoinName, - getNetworkFromCoinName, - isMainnetCoin, - UtxoCoinName, - UtxoCoinNameMainnet, -} from './names'; +import { getFullNameFromCoinName, getMainnetCoinName, isMainnetCoin, UtxoCoinName, UtxoCoinNameMainnet } from './names'; import { assertFixedScriptWalletAddress } from './address/fixedScript'; import { ParsedTransaction } from './transaction/types'; import { decodeDescriptorPsbt, decodePsbt, encodeTransaction, stringToBufferTryFormats } from './transaction/decode'; @@ -410,14 +403,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici this.amountType = amountType; } - /** - * @deprecated - will be removed when we drop support for utxolib - * Use `name` property instead. - */ - get network(): utxolib.Network { - return getNetworkFromCoinName(this.name); - } - getChain(): UtxoCoinName { return this.name; } diff --git a/modules/abstract-utxo/src/names.ts b/modules/abstract-utxo/src/names.ts index 555797ae3c..9e1a5c74c2 100644 --- a/modules/abstract-utxo/src/names.ts +++ b/modules/abstract-utxo/src/names.ts @@ -1,5 +1,3 @@ -import * as utxolib from '@bitgo/utxo-lib'; - export const utxoCoinsMainnet = ['btc', 'bch', 'bcha', 'bsv', 'btg', 'dash', 'doge', 'ltc', 'zec'] as const; export const utxoCoinsTestnet = [ 'tbtc', @@ -46,107 +44,6 @@ export function getMainnetCoinName(coinName: UtxoCoinName): UtxoCoinNameMainnet } } -function getNetworkName(n: utxolib.Network): utxolib.NetworkName { - const name = utxolib.getNetworkName(n); - if (!name) { - throw new Error('Unknown network'); - } - return name; -} - -/** - * @deprecated - will be removed when we drop support for utxolib - * @param n - * @returns the family name for a network. Testnets and mainnets of the same coin share the same family name. - */ -export function getFamilyFromNetwork(n: utxolib.Network): UtxoCoinNameMainnet { - switch (getNetworkName(n)) { - case 'bitcoin': - case 'testnet': - case 'bitcoinPublicSignet': - case 'bitcoinTestnet4': - case 'bitcoinBitGoSignet': - return 'btc'; - case 'bitcoincash': - case 'bitcoincashTestnet': - return 'bch'; - case 'ecash': - case 'ecashTest': - return 'bcha'; - case 'bitcoingold': - case 'bitcoingoldTestnet': - return 'btg'; - case 'bitcoinsv': - case 'bitcoinsvTestnet': - return 'bsv'; - case 'dash': - case 'dashTest': - return 'dash'; - case 'dogecoin': - case 'dogecoinTest': - return 'doge'; - case 'litecoin': - case 'litecoinTest': - return 'ltc'; - case 'zcash': - case 'zcashTest': - return 'zec'; - } -} - -/** - * @deprecated - will be removed when we drop support for utxolib - * Get the chain name for a network. - * The chain is different for every network. - */ -export function getCoinName(n: utxolib.Network): UtxoCoinName { - switch (getNetworkName(n)) { - case 'bitcoinPublicSignet': - return 'tbtcsig'; - case 'bitcoinTestnet4': - return 'tbtc4'; - case 'bitcoinBitGoSignet': - return 'tbtcbgsig'; - case 'bitcoin': - case 'testnet': - case 'bitcoincash': - case 'bitcoincashTestnet': - case 'ecash': - case 'ecashTest': - case 'bitcoingold': - case 'bitcoingoldTestnet': - case 'bitcoinsv': - case 'bitcoinsvTestnet': - case 'dash': - case 'dashTest': - case 'dogecoin': - case 'dogecoinTest': - case 'litecoin': - case 'litecoinTest': - case 'zcash': - case 'zcashTest': - const mainnetName = getFamilyFromNetwork(n); - return utxolib.isTestnet(n) ? `t${mainnetName}` : mainnetName; - } -} - -/** - * @deprecated - will be removed when we drop support for utxolib - * @param coinName - the name of the coin (e.g. 'btc', 'bch', 'ltc'). Also called 'chain' in some contexts. - * @returns the network for a coin. This is the mainnet network for the coin. - */ -export function getNetworkFromCoinName(coinName: string): utxolib.Network { - for (const network of utxolib.getNetworkList()) { - if (getCoinName(network) === coinName) { - return network; - } - } - throw new Error(`Unknown coin name ${coinName}`); -} - -/** @deprecated - use getNetworkFromCoinName instead */ -export const getNetworkFromChain = getNetworkFromCoinName; - function getBaseNameFromMainnet(coinName: UtxoCoinNameMainnet): string { switch (coinName) { case 'btc': @@ -189,11 +86,6 @@ export function getFullNameFromCoinName(coinName: UtxoCoinName): string { return prefix + getBaseNameFromMainnet(getMainnetCoinName(coinName)); } -/** @deprecated - use getFullNameFromCoinName instead */ -export function getFullNameFromNetwork(n: utxolib.Network): string { - return getFullNameFromCoinName(getCoinName(n)); -} - export function isTestnetCoin(coinName: UtxoCoinName): boolean { return isUtxoCoinNameTestnet(coinName); } diff --git a/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts b/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts index 2beb751815..a6b064904b 100644 --- a/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts +++ b/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts @@ -11,6 +11,7 @@ import { encryptKeychain, getDefaultWalletKeys, getMinUtxoCoins, + getNetworkForCoinName, getUtxoWallet, keychainsBase58, getScriptTypes, @@ -46,12 +47,17 @@ describe('prebuildAndSign-returnLegacyFormat', function () { const outputAmount = BigInt(inputScripts.length) * BigInt(1e8) - fee; const outputScriptType: utxolib.bitgo.outputScripts.ScriptType = 'p2sh'; const outputChain = utxolib.bitgo.getExternalChainCode(outputScriptType); - const outputAddress = utxolib.bitgo.getWalletAddress(rootWalletKeys, outputChain, 0, coin.network); + const outputAddress = utxolib.bitgo.getWalletAddress( + rootWalletKeys, + outputChain, + 0, + getNetworkForCoinName(coin.name) + ); recipient = { address: outputAddress, amount: outputAmount.toString() }; prebuild = utxolib.testutil.constructPsbt( inputScripts.map((s) => ({ scriptType: s, value: BigInt(1e8) })), [{ scriptType: outputScriptType, value: outputAmount }], - coin.network, + getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned' ); diff --git a/modules/abstract-utxo/test/unit/customSigner.ts b/modules/abstract-utxo/test/unit/customSigner.ts index 0e673009aa..681c4155ff 100644 --- a/modules/abstract-utxo/test/unit/customSigner.ts +++ b/modules/abstract-utxo/test/unit/customSigner.ts @@ -3,7 +3,14 @@ import nock = require('nock'); import * as sinon from 'sinon'; import { CustomSigningFunction, common } from '@bitgo/sdk-core'; -import { defaultBitGo, getDefaultWalletKeys, getUtxoCoin, getUtxoWallet, assertHasProperty } from './util'; +import { + defaultBitGo, + getDefaultWalletKeys, + getNetworkForCoinName, + getUtxoCoin, + getUtxoWallet, + assertHasProperty, +} from './util'; nock.disableNetConnect(); @@ -56,7 +63,7 @@ describe('UTXO Custom Signer Function', function () { const psbt = utxoLib.testutil.constructPsbt( [{ scriptType: 'taprootKeyPathSpend', value: BigInt(1000) }], [{ scriptType: 'p2sh', value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ); @@ -72,7 +79,7 @@ describe('UTXO Custom Signer Function', function () { const psbt = utxoLib.testutil.constructPsbt( [{ scriptType: 'p2wsh', value: BigInt(1000) }], [{ scriptType: 'p2sh', value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ); diff --git a/modules/abstract-utxo/test/unit/prebuildAndSign.ts b/modules/abstract-utxo/test/unit/prebuildAndSign.ts index e2ae0dd0ec..24910bea5b 100644 --- a/modules/abstract-utxo/test/unit/prebuildAndSign.ts +++ b/modules/abstract-utxo/test/unit/prebuildAndSign.ts @@ -12,6 +12,7 @@ import { defaultBitGo, getDefaultWalletKeys, getMinUtxoCoins, + getNetworkForCoinName, getUtxoWallet, keychainsBase58, getScriptTypes, @@ -62,7 +63,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { const psbt = utxolib.testutil.constructPsbt( inputs as utxolib.testutil.Input[], outputs, - coin.network, + getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned', { @@ -178,7 +179,12 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { const outputAmount = BigInt(inputScripts.length) * BigInt(1e8) - fee; const outputScriptType: utxolib.bitgo.outputScripts.ScriptType = 'p2sh'; const outputChain = utxolib.bitgo.getExternalChainCode(outputScriptType); - const outputAddress = utxolib.bitgo.getWalletAddress(rootWalletKeys, outputChain, 0, coin.network); + const outputAddress = utxolib.bitgo.getWalletAddress( + rootWalletKeys, + outputChain, + 0, + getNetworkForCoinName(coin.name) + ); recipient = { address: outputAddress, @@ -222,7 +228,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { nocks.forEach((nock) => assert.ok(nock.isDone())); - assertSignable(res.txHex, inputScripts, coin.network); + assertSignable(res.txHex, inputScripts, getNetworkForCoinName(coin.name)); }); [true, false].forEach((selfSend) => { @@ -254,7 +260,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { nocks.forEach((nock) => assert.ok(nock.isDone())); - assertSignable(res.txHex, inputScripts, coin.network); + assertSignable(res.txHex, inputScripts, getNetworkForCoinName(coin.name)); }); }); }); diff --git a/modules/abstract-utxo/test/unit/signTransaction.ts b/modules/abstract-utxo/test/unit/signTransaction.ts index 7491ecf82b..f5dfd0e111 100644 --- a/modules/abstract-utxo/test/unit/signTransaction.ts +++ b/modules/abstract-utxo/test/unit/signTransaction.ts @@ -9,7 +9,14 @@ import { common, Triple } from '@bitgo/sdk-core'; import { getReplayProtectionPubkeys, ErrorDeprecatedTxFormat } from '../../src'; import type { Unspent } from '../../src/unspent'; -import { getUtxoWallet, getDefaultWalletKeys, getUtxoCoin, keychainsBase58, defaultBitGo } from './util'; +import { + getUtxoWallet, + getDefaultWalletKeys, + getNetworkForCoinName, + getUtxoCoin, + keychainsBase58, + defaultBitGo, +} from './util'; describe('signTransaction', function () { const bgUrl = common.Environments[defaultBitGo.getEnv()].uri; @@ -21,7 +28,7 @@ describe('signTransaction', function () { const pubs = keychainsBase58.map((v) => v.pub) as Triple; function validatePsbt(txHex: string, targetSigCount: 0 | 1, targetNonceCount?: 1 | 2) { - const psbt = utxolib.bitgo.createPsbtFromHex(txHex, coin.network); + const psbt = utxolib.bitgo.createPsbtFromHex(txHex, getNetworkForCoinName(coin.name)); psbt.data.inputs.forEach((input, index) => { const parsed = utxolib.bitgo.parsePsbtInput(input); if (parsed.scriptType === 'taprootKeyPathSpend') { @@ -38,7 +45,7 @@ describe('signTransaction', function () { } function validateTx(txHex: string, unspents: Unspent[], targetSigCount: 0 | 1) { - const tx = utxolib.bitgo.createTransactionFromHex(txHex, coin.network); + const tx = utxolib.bitgo.createTransactionFromHex(txHex, getNetworkForCoinName(coin.name)); unspents.forEach((u, i) => { const sigCount = utxolib.bitgo.getStrictSignatureCount(tx.ins[i]); const expectedSigCount = utxolib.bitgo.isWalletUnspent(u) && !!targetSigCount ? 1 : 0; @@ -56,7 +63,7 @@ describe('signTransaction', function () { const txHex = tx.toHex(); function nockSignPsbt(psbtHex: string): nock.Scope { - const psbt = utxolib.bitgo.createPsbtFromHex(psbtHex, coin.network); + const psbt = utxolib.bitgo.createPsbtFromHex(psbtHex, getNetworkForCoinName(coin.name)); return nock(bgUrl) .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/signpsbt`, (body) => body.psbt) .reply(200, { psbt: psbt.setAllInputsMusig2NonceHD(rootWalletKeys.bitgo).toHex() }); @@ -147,7 +154,7 @@ describe('signTransaction', function () { .map((scriptType) => ({ scriptType, value: BigInt(1000) })); const unspentSum = inputs.reduce((prev: bigint, curr) => prev + curr.value, BigInt(0)); const outputs: testutil.Output[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, rootWalletKeys, 'unsigned', { + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned', { p2shP2pkKey: replayProtectionKey, }); @@ -163,7 +170,7 @@ describe('signTransaction', function () { .map((scriptType) => ({ scriptType, value: BigInt(1000) })); const unspentSum = inputs.reduce((prev: bigint, cur) => prev + cur.value, BigInt(0)); const outputs: testutil.Output[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, rootWalletKeys, 'unsigned', { + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned', { p2shP2pkKey: replayProtectionKey, }); @@ -181,8 +188,16 @@ describe('signTransaction', function () { })); const unspentSum = inputs.reduce((prev: bigint, curr) => prev + curr.value, BigInt(0)); const outputs: testutil.TxnOutput[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const txBuilder = testutil.constructTxnBuilder(inputs, outputs, coin.network, rootWalletKeys, 'unsigned'); - const unspents = inputs.map((v, i) => testutil.toTxnUnspent(v, i, coin.network, rootWalletKeys)); + const txBuilder = testutil.constructTxnBuilder( + inputs, + outputs, + getNetworkForCoinName(coin.name), + rootWalletKeys, + 'unsigned' + ); + const unspents = inputs.map((v, i) => + testutil.toTxnUnspent(v, i, getNetworkForCoinName(coin.name), rootWalletKeys) + ); // Legacy format transactions are now deprecated and should throw ErrorDeprecatedTxFormat await assert.rejects(async () => { @@ -194,7 +209,7 @@ describe('signTransaction', function () { const inputs: testutil.Input[] = [{ scriptType: 'taprootKeyPathSpend', value: BigInt(1000) }]; const unspentSum = inputs.reduce((prev: bigint, curr) => prev + curr.value, BigInt(0)); const outputs: testutil.Output[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, rootWalletKeys, 'unsigned'); + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned'); await assert.rejects( async () => { diff --git a/modules/abstract-utxo/test/unit/transaction.ts b/modules/abstract-utxo/test/unit/transaction.ts index ed69fa610e..09f8d51201 100644 --- a/modules/abstract-utxo/test/unit/transaction.ts +++ b/modules/abstract-utxo/test/unit/transaction.ts @@ -31,6 +31,7 @@ import { getWalletKeys, defaultBitGo, getMinUtxoCoins, + getNetworkForCoinName, getScriptTypes, } from './util'; @@ -60,7 +61,7 @@ function run( return testutil.toUnspent( { scriptType: t, value: t === 'p2shP2pk' ? BigInt(1000) : BigInt(value) }, index, - coin.network, + getNetworkForCoinName(coin.name), walletKeys ); }); @@ -72,7 +73,7 @@ function run( function getUnspents(): Unspent[] { return inputScripts.map((type, i) => - mockUnspent(coin.network, walletKeys, toTxnInputScriptType(type), i, value) + mockUnspent(getNetworkForCoinName(coin.name), walletKeys, toTxnInputScriptType(type), i, value) ); } @@ -113,7 +114,9 @@ function run( scope = nock(bgUrl) .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/signpsbt`, (body) => body.psbt) .reply(200, (_uri: string, requestBody: unknown) => { - const networkName = utxolib.getNetworkName(coin.network) as fixedScriptWallet.NetworkName; + const networkName = utxolib.getNetworkName( + getNetworkForCoinName(coin.name) + ) as fixedScriptWallet.NetworkName; const reqBytes = Buffer.from((requestBody as { psbt: string }).psbt, 'hex'); const reqPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(reqBytes, networkName); const cosignerWasm = BIP32.fromBase58(cosigner.toBase58()); @@ -166,7 +169,7 @@ function run( const outputs: testutil.Output[] = [ { address: getOutputAddress(getWalletKeys('test')), value: unspentSum - BigInt(1000) }, ]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, walletKeys, 'unsigned', { + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), walletKeys, 'unsigned', { p2shP2pkKey: getReplayProtectionPubkeys(coin.name)[0], }); utxolib.bitgo.addXpubsToPsbt(psbt, walletKeys); @@ -177,7 +180,11 @@ function run( const prebuild = txFormat === 'psbt' ? createPrebuildPsbt() - : createPrebuildTransaction(coin.network, getUnspents(), getOutputAddress(walletKeys)); + : createPrebuildTransaction( + getNetworkForCoinName(coin.name), + getUnspents(), + getOutputAddress(walletKeys) + ); const halfSignedUserBitGo = await createHalfSignedTransaction(prebuild, walletKeys.user, walletKeys.bitgo); const fullSignedUserBitGo = @@ -225,7 +232,7 @@ function run( ? undefined : v instanceof utxolib.bitgo.UtxoTransaction ? transactionToObj(v) - : transactionHexToObj(v.txHex, coin.network, amountType) + : transactionHexToObj(v.txHex, getNetworkForCoinName(coin.name), amountType) ) as TransactionObjStages; } @@ -240,7 +247,7 @@ function run( }); function testPsbtValidSignatures(tx: HalfSignedUtxoTransaction, signedBy: BIP32Interface[]) { - const psbt = utxolib.bitgo.createPsbtFromHex(tx.txHex, coin.network); + const psbt = utxolib.bitgo.createPsbtFromHex(tx.txHex, getNetworkForCoinName(coin.name)); const unspents = getUnspentsForPsbt(); psbt.data.inputs.forEach((input, index) => { const unspent = unspents[index]; @@ -278,7 +285,7 @@ function run( const transaction = utxolib.bitgo.createTransactionFromBuffer( Buffer.from(tx.txHex, 'hex'), - coin.network, + getNetworkForCoinName(coin.name), { amountType } ); transaction.ins.forEach((input, index) => { diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts index d129f40614..e6c2676073 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts @@ -8,10 +8,9 @@ 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 { getCoinNameForNetwork, getUtxoCoin } from '../../util'; import { explainPsbtWasm } from '../../../../src/transaction/fixedScript'; import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction'; -import { getCoinName } from '../../../../src/names'; import { TransactionPrebuild } from '../../../../src/abstractUtxoCoin'; function getTxParamsFromExplanation( @@ -78,7 +77,7 @@ function describeParseTransactionWith( let stubExplainTransaction: sinon.SinonStub; before('prepare', async function () { - const coinName = getCoinName(acidTest.network); + const coinName = getCoinNameForNetwork(acidTest.network); coin = getUtxoCoin(coinName); // Create PSBT and explanation diff --git a/modules/abstract-utxo/test/unit/wallet.ts b/modules/abstract-utxo/test/unit/wallet.ts index 7c9b6223e7..4313a70237 100644 --- a/modules/abstract-utxo/test/unit/wallet.ts +++ b/modules/abstract-utxo/test/unit/wallet.ts @@ -5,7 +5,7 @@ import nock = require('nock'); import * as _ from 'lodash'; import { Wallet, ManageUnspentsOptions, common } from '@bitgo/sdk-core'; -import { defaultBitGo, getDefaultWalletKeys, toKeychainObjects, getUtxoCoin } from './util'; +import { defaultBitGo, getDefaultWalletKeys, getNetworkForCoinName, toKeychainObjects, getUtxoCoin } from './util'; const bgUrl = common.Environments[defaultBitGo.getEnv()].uri; const bitgo = defaultBitGo; @@ -35,7 +35,7 @@ describe('manage unspents', function () { utxoLib.testutil.constructPsbt( [{ scriptType, value: BigInt(1000) }], [{ scriptType, value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ) @@ -74,7 +74,7 @@ describe('manage unspents', function () { const psbt = utxoLib.testutil.constructPsbt( [{ scriptType: 'p2wsh', value: BigInt(1000) }], [{ scriptType: 'p2shP2wsh', value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ); From a7af15895e80f67a93fdedbde1445a8b456732df Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 22 May 2026 15:23:48 +0200 Subject: [PATCH 13/13] refactor(abstract-utxo): inline ScriptType2Of3 type definition Replace the utxolib.bitgo.outputScripts.ScriptType2Of3 type re-export with an inline literal union. The values are identical to the utxolib export ('p2sh' | 'p2shP2wsh' | 'p2wsh' | 'p2tr' | 'p2trMusig2'). This removes the last utxolib type reference and the top-level utxolib import from abstractUtxoCoin.ts. Refs: T1-3279 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index d33d1b7dd8..e6392b5dc2 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -2,7 +2,6 @@ import assert from 'assert'; import { randomBytes } from 'crypto'; import _ from 'lodash'; -import * as utxolib from '@bitgo/utxo-lib'; import { address as wasmAddress, BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; import { AddressCoinSpecific, @@ -86,7 +85,7 @@ import { isUtxoWalletData, UtxoWallet } from './wallet'; import { isDescriptorWalletData } from './descriptor/descriptorWallet'; import type { Unspent } from './unspent'; -import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3; +type ScriptType2Of3 = 'p2sh' | 'p2shP2wsh' | 'p2wsh' | 'p2tr' | 'p2trMusig2'; export type TxFormat = // This is a legacy transaction format based around the bitcoinjs-lib serialization of unsigned transactions @@ -132,8 +131,6 @@ type UtxoCustomSigningFunction = { }): Promise; }; -const { ChainCode } = fixedScriptWallet; - /** * Check if a decoded transaction has at least one taproot key path spend (MuSig2) input. */