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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions modules/abstract-utxo/src/abstractUtxoCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,13 @@ import {
UtxoCoinName,
UtxoCoinNameMainnet,
} from './names';
import { assertFixedScriptWalletAddress } from './address/fixedScript';
import {
assertFixedScriptWalletAddress,
ScriptType2Of3,
scriptTypes2Of3,
UtxolibScriptType,
toUtxolibScriptType,
} from './address/fixedScript';
import { isSdkBackend, ParsedTransaction, SdkBackend } from './transaction/types';
import { decodePsbtWith, encodeTransaction, stringToBufferTryFormats } from './transaction/decode';
import { fetchKeychains, toBip32Triple, UtxoKeychain } from './keychains';
Expand All @@ -95,8 +101,6 @@ import { isUtxoWalletData, UtxoWallet } from './wallet';
import { isDescriptorWalletData } from './descriptor/descriptorWallet';
import type { Unspent } from './unspent';

import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;

export type TxFormat =
// This is a legacy transaction format based around the bitcoinjs-lib serialization of unsigned transactions
// does not include prevOut data and is a bit painful to work with
Expand Down Expand Up @@ -141,8 +145,6 @@ type UtxoCustomSigningFunction<TNumber extends number | bigint> = {
}): Promise<SignedTransaction>;
};

const { isChainCode, scriptTypeForChain, outputScripts } = bitgo;

/**
* Convert ValidationError to TxIntentMismatchRecipientError with structured data
*
Expand Down Expand Up @@ -425,7 +427,7 @@ export abstract class AbstractUtxoCoin

/** @deprecated */
static get validAddressTypes(): ScriptType2Of3[] {
return [...outputScripts.scriptTypes2Of3];
return [...scriptTypes2Of3];
}

/**
Expand Down Expand Up @@ -533,8 +535,10 @@ export abstract class AbstractUtxoCoin
* Determine an address' type based on its witness and redeem script presence
* @param addressDetails
*/
static inferAddressType(addressDetails: { chain: number }): ScriptType2Of3 | null {
return isChainCode(addressDetails.chain) ? scriptTypeForChain(addressDetails.chain) : null;
static inferAddressType(addressDetails: { chain: number }): UtxolibScriptType | null {
return fixedScriptWallet.ChainCode.is(addressDetails.chain)
? toUtxolibScriptType(fixedScriptWallet.ChainCode.scriptType(addressDetails.chain))
: null;
}

createTransactionFromHex<TNumber extends number | bigint = number>(
Expand Down Expand Up @@ -716,7 +720,7 @@ export abstract class AbstractUtxoCoin
* @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 @@ -729,7 +733,9 @@ export abstract class AbstractUtxoCoin
* @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))
);
}

keyIdsForSigning(): number[] {
Expand Down
49 changes: 48 additions & 1 deletion modules/abstract-utxo/src/address/fixedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,54 @@ import { fixedScriptWallet } from '@bitgo/wasm-utxo';

import { UtxoCoinName } from '../names';

type ScriptType2Of3 = fixedScriptWallet.OutputScriptType;
/**
* Script type for 2-of-3 multisig outputs.
* This is the wasm-utxo OutputScriptType which uses 'p2trLegacy' for taproot.
*/
export type ScriptType2Of3 = fixedScriptWallet.OutputScriptType;

/**
* utxolib script type format - uses 'p2tr' instead of 'p2trLegacy'.
* This is the format expected by utxolib functions.
*/
export type UtxolibScriptType = 'p2sh' | 'p2shP2wsh' | 'p2wsh' | 'p2tr' | 'p2trMusig2';

/**
* All 2-of-3 multisig script types.
* Uses wasm-utxo naming ('p2trLegacy' for taproot).
*/
export const scriptTypes2Of3: readonly ScriptType2Of3[] = ['p2sh', 'p2shP2wsh', 'p2wsh', 'p2trLegacy', 'p2trMusig2'];

/**
* All 2-of-3 multisig script types in utxolib format.
* Uses utxolib naming ('p2tr' for taproot).
*/
export const utxolibScriptTypes2Of3: readonly UtxolibScriptType[] = [
'p2sh',
'p2shP2wsh',
'p2wsh',
'p2tr',
'p2trMusig2',
];

/**
* Convert ScriptType2Of3 to utxolib-compatible format.
* ScriptType2Of3 uses 'p2trLegacy' while utxolib uses 'p2tr'.
*/
export function toUtxolibScriptType(scriptType: ScriptType2Of3): UtxolibScriptType {
return scriptType === 'p2trLegacy' ? 'p2tr' : scriptType;
}

/**
* Check if a script type requires witness data.
* Witness data is required for segwit and taproot script types.
*/
export function hasWitnessData(scriptType: ScriptType2Of3): boolean {
return (
scriptType === 'p2shP2wsh' || scriptType === 'p2wsh' || scriptType === 'p2trLegacy' || scriptType === 'p2trMusig2'
);
}

type ChainCode = fixedScriptWallet.ChainCode;

export interface FixedScriptAddressCoinSpecific {
Expand Down
6 changes: 6 additions & 0 deletions modules/abstract-utxo/src/address/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@ export {
generateAddressWithChainAndIndex,
assertFixedScriptWalletAddress,
FixedScriptAddressCoinSpecific,
ScriptType2Of3,
UtxolibScriptType,
scriptTypes2Of3,
utxolibScriptTypes2Of3,
toUtxolibScriptType,
hasWitnessData,
} from './fixedScript';
28 changes: 17 additions & 11 deletions modules/abstract-utxo/src/recovery/backupKeyRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { fixedScriptWallet } from '@bitgo/wasm-utxo';

import { AbstractUtxoCoin } from '../abstractUtxoCoin';
import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction';
import { generateAddressWithChainAndIndex } from '../address';
import { generateAddressWithChainAndIndex, ScriptType2Of3, scriptTypes2Of3, hasWitnessData } from '../address';
import { encodeTransaction } from '../transaction/decode';
import { getReplayProtectionPubkeys } from '../transaction/fixedScript/replayProtection';
import { isTestnetCoin, UtxoCoinName } from '../names';
Expand All @@ -26,15 +26,12 @@ import { MempoolApi } from './mempoolApi';
import { CoingeckoApi } from './coingeckoApi';
import { createBackupKeyRecoveryPsbt, getRecoveryAmount, PsbtBackend, toPsbtToUtxolibPsbt } from './psbt';

type ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;
type ChainCode = utxolib.bitgo.ChainCode;
type ChainCode = fixedScriptWallet.ChainCode;
type RootWalletKeys = utxolib.bitgo.RootWalletKeys;
type WalletUnspentJSON = WalletUnspent & {
valueString: string;
};

const { getInternalChainCode, scriptTypeForChain, outputScripts, getExternalChainCode } = utxolib.bitgo;

// V1 only deals with BTC. 50 sat/vbyte is very arbitrary.
export const DEFAULT_RECOVERY_FEERATE_SAT_VBYTE_V1 = 50;

Expand Down Expand Up @@ -137,9 +134,8 @@ async function queryBlockchainUnspentsPath(
walletKeys: RootWalletKeys,
chain: ChainCode
): Promise<WalletUnspent<bigint>[]> {
const scriptType = scriptTypeForChain(chain);
const fetchPrevTx =
!utxolib.bitgo.outputScripts.hasWitnessData(scriptType) && getMainnet(coin.network) !== networks.zcash;
const scriptType = fixedScriptWallet.ChainCode.scriptType(chain);
const fetchPrevTx = !hasWitnessData(scriptType) && getMainnet(coin.network) !== networks.zcash;
const recoveryProvider = params.recoveryProvider ?? forCoin(coin.getChain(), params.apiKey);
const MAX_SEQUENTIAL_ADDRESSES_WITHOUT_TXS = params.scan || 20;
let numSequentialAddressesWithoutTxs = 0;
Expand Down Expand Up @@ -315,15 +311,25 @@ export async function backupKeyRecovery(

const unspents: WalletUnspent<bigint>[] = (
await Promise.all(
outputScripts.scriptTypes2Of3
scriptTypes2Of3
.filter(
(addressType) => coin.supportsAddressType(addressType) && !params.ignoreAddressTypes?.includes(addressType)
)
.reduce(
(queries, addressType) => [
...queries,
queryBlockchainUnspentsPath(coin, params, walletKeys, getExternalChainCode(addressType)),
queryBlockchainUnspentsPath(coin, params, walletKeys, getInternalChainCode(addressType)),
queryBlockchainUnspentsPath(
coin,
params,
walletKeys,
fixedScriptWallet.ChainCode.value(addressType, 'external')
),
queryBlockchainUnspentsPath(
coin,
params,
walletKeys,
fixedScriptWallet.ChainCode.value(addressType, 'internal')
),
],
[] as Promise<WalletUnspent<bigint>[]>[]
)
Expand Down
10 changes: 3 additions & 7 deletions modules/abstract-utxo/src/recovery/psbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import { getNetworkFromCoinName, UtxoCoinName } from '../names';
import type { WalletUnspent } from '../unspent';

type RootWalletKeys = utxolib.bitgo.RootWalletKeys;

const { chainCodesP2tr, chainCodesP2trMusig2 } = utxolib.bitgo;

type ChainCode = utxolib.bitgo.ChainCode;
type ChainCode = fixedScriptWallet.ChainCode;

/**
* Backend to use for PSBT creation.
Expand All @@ -22,9 +19,8 @@ export type PsbtBackend = 'wasm-utxo' | 'utxolib';
* Check if a chain code is for a taproot script type
*/
export function isTaprootChain(chain: ChainCode): boolean {
return (
(chainCodesP2tr as readonly number[]).includes(chain) || (chainCodesP2trMusig2 as readonly number[]).includes(chain)
);
const scriptType = fixedScriptWallet.ChainCode.scriptType(chain);
return scriptType === 'p2trLegacy' || scriptType === 'p2trMusig2';
}

/**
Expand Down
4 changes: 2 additions & 2 deletions modules/abstract-utxo/src/unspent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as utxolib from '@bitgo/utxo-lib';
import { fixedScriptWallet } from '@bitgo/wasm-utxo';

/**
* Unspent transaction output (UTXO) type definition
Expand Down Expand Up @@ -35,7 +35,7 @@ export interface Unspent<TNumber extends number | bigint = number> {
* - index: number (index for wallet derivation)
*/
export interface WalletUnspent<TNumber extends number | bigint = number> extends Unspent<TNumber> {
chain: utxolib.bitgo.ChainCode;
chain: fixedScriptWallet.ChainCode;
index: number;
}

Expand Down
11 changes: 7 additions & 4 deletions modules/abstract-utxo/test/unit/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import * as assert from 'assert';
import * as utxolib from '@bitgo/utxo-lib';
const { chainCodes } = utxolib.bitgo;

import { AbstractUtxoCoin, GenerateFixedScriptAddressOptions, generateAddress } from '../../src';
import {
AbstractUtxoCoin,
GenerateFixedScriptAddressOptions,
generateAddress,
utxolibScriptTypes2Of3,
} from '../../src';

import { utxoCoins, keychains as keychainsBip32, getFixture, shouldEqualJSON } from './util';

Expand Down Expand Up @@ -42,9 +47,7 @@ function run(coin: AbstractUtxoCoin) {

describe(`UTXO Addresses ${coin.getChain()}`, function () {
it('address support', function () {
const supportedAddressTypes = utxolib.bitgo.outputScripts.scriptTypes2Of3.filter((t) =>
coin.supportsAddressType(t)
);
const supportedAddressTypes = utxolibScriptTypes2Of3.filter((t) => coin.supportsAddressType(t));
switch (coin.getChain()) {
case 'btc':
case 'tbtc':
Expand Down
17 changes: 12 additions & 5 deletions modules/abstract-utxo/test/unit/prebuildAndSign.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import * as assert from 'assert';

import * as utxolib from '@bitgo/utxo-lib';
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
import nock = require('nock');
import { common, HalfSignedUtxoTransaction, Wallet } from '@bitgo/sdk-core';
import { getSeed } from '@bitgo/sdk-test';

import { AbstractUtxoCoin, getReplayProtectionAddresses } from '../../src';
import {
AbstractUtxoCoin,
getReplayProtectionAddresses,
ScriptType2Of3,
utxolibScriptTypes2Of3,
UtxolibScriptType,
} from '../../src';
import { getMainnetCoinName } from '../../src/names';

import { defaultBitGo, encryptKeychain, getDefaultWalletKeys, getUtxoWallet, keychainsBase58, utxoCoins } from './util';
Expand All @@ -24,7 +31,7 @@ type KeyDoc = {
const walletPassphrase = 'gabagool';
const webauthnWalletPassPhrase = 'just the gabagool';

const scriptTypes = [...utxolib.bitgo.outputScripts.scriptTypes2Of3, 'taprootKeyPathSpend', 'p2shP2pk'] as const;
const scriptTypes = [...utxolibScriptTypes2Of3, 'taprootKeyPathSpend', 'p2shP2pk'] as const;
export type ScriptType = (typeof scriptTypes)[number];

type Input = {
Expand Down Expand Up @@ -188,8 +195,8 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[], txFormat: TxFor
before(async function () {
// Make output address information
const outputAmount = BigInt(inputScripts.length) * BigInt(1e8) - fee;
const outputScriptType: utxolib.bitgo.outputScripts.ScriptType = 'p2sh';
const outputChain = utxolib.bitgo.getExternalChainCode(outputScriptType);
const outputScriptType: UtxolibScriptType = 'p2sh';
const outputChain = fixedScriptWallet.ChainCode.value(outputScriptType, 'external');
const outputAddress = utxolib.bitgo.getWalletAddress(rootWalletKeys, outputChain, 0, coin.network);

recipient = {
Expand Down Expand Up @@ -307,7 +314,7 @@ utxoCoins
.forEach((inputScript) => {
const inputScriptCleaned = (
inputScript === 'taprootKeyPathSpend' ? 'p2trMusig2' : inputScript
) as utxolib.bitgo.outputScripts.ScriptType2Of3;
) as ScriptType2Of3;

if (!coin.supportsAddressType(inputScriptCleaned)) {
return;
Expand Down
24 changes: 20 additions & 4 deletions modules/abstract-utxo/test/unit/recovery/backupKeyRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
BackupKeyRecoveryTransansaction,
CoingeckoApi,
FormattedOfflineVaultTxInfo,
ScriptType2Of3,
toUtxolibScriptType,
} from '../../../src';
import { getCoinName } from '../../../src/names';
import type { Unspent, WalletUnspent } from '../../../src/unspent';
Expand All @@ -37,7 +39,6 @@ import { MockRecoveryProvider } from './mock';

const { toOutput } = utxolib.bitgo;
type RootWalletKeys = utxolib.bitgo.RootWalletKeys;
type ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;

const config = { krsProviders };

Expand Down Expand Up @@ -154,9 +155,24 @@ function run(
before('create recovery data', async function () {
this.timeout(10_000);
recoverUnspents = scriptTypes.flatMap((scriptType, index) => [
utxolib.testutil.toUnspent({ scriptType, value: BigInt(1e8) * valueMul }, index, coin.network, walletKeys),
utxolib.testutil.toUnspent({ scriptType, value: BigInt(2e8) * valueMul }, index, coin.network, walletKeys),
utxolib.testutil.toUnspent({ scriptType, value: BigInt(3e8) * valueMul }, index, coin.network, walletKeys),
utxolib.testutil.toUnspent(
{ scriptType: toUtxolibScriptType(scriptType), value: BigInt(1e8) * valueMul },
index,
coin.network,
walletKeys
),
utxolib.testutil.toUnspent(
{ scriptType: toUtxolibScriptType(scriptType), value: BigInt(2e8) * valueMul },
index,
coin.network,
walletKeys
),
utxolib.testutil.toUnspent(
{ scriptType: toUtxolibScriptType(scriptType), value: BigInt(3e8) * valueMul },
index,
coin.network,
walletKeys
),
]);

// If the coin is bch, convert the mocked unspent address to cashaddr format since that is the format that blockchair
Expand Down
Loading