Skip to content
116 changes: 34 additions & 82 deletions modules/abstract-utxo/src/abstractUtxoCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import assert from 'assert';
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 { address as wasmAddress, BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo';
import {
AddressCoinSpecific,
BaseCoin,
Expand Down Expand Up @@ -62,7 +60,6 @@ import { getReplayProtectionPubkeys, isReplayProtectionUnspent } from './transac
import { supportedCrossChainRecoveries } from './config';
import {
assertValidTransactionRecipient,
DecodedTransaction,
explainTx,
fromExtendedAddressFormat,
isScriptRecipient,
Expand All @@ -77,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';
Expand All @@ -96,7 +86,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
Expand Down Expand Up @@ -142,28 +132,19 @@ type UtxoCustomSigningFunction<TNumber extends number | bigint> = {
}): Promise<SignedTransaction>;
};

const { isChainCode, scriptTypeForChain, outputScripts } = bitgo;

/**
* 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<TNumber extends number | bigint>(
tx: DecodedTransaction<TNumber>,
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');
}

/**
Expand Down Expand Up @@ -216,8 +197,6 @@ function convertValidationErrorToTxIntentMismatch(

export type { DecodedTransaction } from './transaction/types';

export type RootWalletKeys = bitgo.RootWalletKeys;

export type UtxoCoinSpecific = AddressCoinSpecific | DescriptorAddressCoinSpecific;

export interface VerifyAddressOptions<TCoinSpecific extends UtxoCoinSpecific> extends BaseVerifyAddressOptions {
Expand Down Expand Up @@ -252,8 +231,6 @@ export interface DecoratedExplainTransactionOptions<TNumber extends number | big
changeInfo?: { address: string; chain: number; index: number }[];
}

export type UtxoNetwork = utxolib.Network;

export interface TransactionPrebuild<TNumber extends number | bigint = number> extends BaseTransactionPrebuild {
txInfo?: TransactionInfo<TNumber>;
blockHeight?: number;
Expand Down Expand Up @@ -334,11 +311,6 @@ type UtxoBaseSignTransactionOptions<TNumber extends number | bigint = number> =
* 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'.
Expand Down Expand Up @@ -431,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;
}
Expand All @@ -454,13 +418,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 {
Expand All @@ -469,7 +428,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici

/** @deprecated */
static get validAddressTypes(): ScriptType2Of3[] {
return [...outputScripts.scriptTypes2Of3];
return ['p2sh', 'p2shP2wsh', 'p2wsh', 'p2tr', 'p2trMusig2'];
}

/**
Expand Down Expand Up @@ -507,15 +466,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;
}
Expand Down Expand Up @@ -595,13 +559,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;
}

createTransactionFromHex<TNumber extends number | bigint = number>(
hex: string
): utxolib.bitgo.UtxoTransaction<TNumber> {
return utxolib.bitgo.createTransactionFromHex<TNumber>(hex, this.network, this.amountType);
return fixedScriptWallet.ChainCode.is(addressDetails.chain)
? (fixedScriptWallet.ChainCode.scriptType(addressDetails.chain) as ScriptType2Of3)
: null;
}

decodeTransaction(input: Buffer | string): fixedScriptWallet.BitGoPsbt {
Expand Down Expand Up @@ -753,7 +713,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 */
Expand All @@ -766,7 +726,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[] {
Expand Down Expand Up @@ -1055,17 +1018,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici
}

const returnLegacyFormat = (params as Record<string, unknown>).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 };
}

Expand Down
5 changes: 0 additions & 5 deletions modules/abstract-utxo/src/impl/doge/doge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { BitGoBase, HalfSignedUtxoTransaction, SignedTransaction } from '@bitgo/sdk-core';
import { bitgo } from '@bitgo/utxo-lib';

import {
AbstractUtxoCoin,
Expand Down Expand Up @@ -75,10 +74,6 @@ export class Doge extends AbstractUtxoCoin {

/* postProcessPrebuild, isBitGoTaintedUnspent, verifyCustomChangeKeySignatures do not care whether they receive number or bigint */

createTransactionFromHex<TNumber extends number | bigint = bigint>(hex: string): bitgo.UtxoTransaction<TNumber> {
return super.createTransactionFromHex<TNumber>(hex);
}

async parseTransaction<TNumber extends number | bigint = bigint>(
params: ParseTransactionOptions<TNumber>
): /*
Expand Down
108 changes: 0 additions & 108 deletions modules/abstract-utxo/src/names.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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);
}
Expand Down
6 changes: 3 additions & 3 deletions modules/abstract-utxo/src/recovery/backupKeyRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<bigint>(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
Expand Down
Loading