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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 30 additions & 33 deletions modules/abstract-utxo/src/abstractUtxoCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import assert from 'assert';
import { randomBytes } from 'crypto';

import _ from 'lodash';
import { address as wasmAddress, BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo';
import { BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo';
import {
AddressCoinSpecific,
BaseCoin,
Expand Down Expand Up @@ -72,7 +72,15 @@ import {
ErrorImplicitExternalOutputs,
} from './transaction/descriptor/verifyTransaction';
import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptorWallet } from './descriptor';
import { getFullNameFromCoinName, getMainnetCoinName, isMainnetCoin, UtxoCoinName, UtxoCoinNameMainnet } from './names';
import {
getFullNameFromCoinName,
getMainnetCoinName,
toWasmUtxoCoinName,
isMainnetCoin,
WasmUtxoCoinName,
UtxoCoinName,
UtxoCoinNameMainnet,
} from './names';
import { assertFixedScriptWalletAddress, generateAddress } from './address/fixedScript';
import { ParsedTransaction } from './transaction/types';
import { decodeDescriptorPsbt, decodePsbt, encodeTransaction, stringToBufferTryFormats } from './transaction/decode';
Expand Down Expand Up @@ -414,6 +422,15 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici
return getFullNameFromCoinName(this.name);
}

/** Coin name used by wasm-utxo. Private Bitcoin networks may map to a shared codec. */
get wasmName(): WasmUtxoCoinName {
return toWasmUtxoCoinName(this.name);
}

get addressCodec(): AddressCodec {
return new AddressCodec(this.name, this.wasmName);
}

/** 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
Expand Down Expand Up @@ -461,27 +478,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici
}
}

// By default, allow all address formats.
// At the time of writing, the only additional address format is bch cashaddr.
const anyFormat = (param as { anyFormat: boolean } | undefined)?.anyFormat ?? true;
try {
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
}
}
return false;
} catch (e) {
return false;
}
return this.addressCodec.isValidAddress(address);
}

/**
Expand Down Expand Up @@ -568,7 +565,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici
if (!hasPsbtMagic(buffer)) {
throw new ErrorDeprecatedTxFormat('legacy');
}
return decodePsbt(buffer, this.name);
return decodePsbt(buffer, this.wasmName);
}

decodeTransactionAsPsbt(input: Buffer | string): fixedScriptWallet.BitGoPsbt {
Expand Down Expand Up @@ -596,7 +593,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici
async parseTransaction<TNumber extends number | bigint = number>(
params: ParseTransactionOptions<TNumber>
): Promise<ParsedTransaction<TNumber>> {
return this.parseTransactionWithAddressCodec(params, new AddressCodec(this.name));
return this.parseTransactionWithAddressCodec(params, this.addressCodec);
}

protected parseTransactionWithAddressCodec<TNumber extends number | bigint>(
Expand Down Expand Up @@ -639,7 +636,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici
async verifyTransaction<TNumber extends number | bigint = number>(
params: VerifyTransactionOptions<TNumber>
): Promise<boolean> {
return this.verifyTransactionWithAddressCodec(params, new AddressCodec(this.name));
return this.verifyTransactionWithAddressCodec(params, this.addressCodec);
}

protected async verifyTransactionWithAddressCodec<TNumber extends number | bigint>(
Expand Down Expand Up @@ -693,7 +690,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici
throw new Error('keychains must be a triple');
}
assertDescriptorWalletAddress(
this.name,
this.addressCodec,
params,
getDescriptorMapFromWallet(wallet, toBip32Triple(keychains), getPolicyForEnv(this.bitgo.env))
);
Expand All @@ -710,7 +707,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici
throw new Error('missing required param keychains');
}

assertFixedScriptWalletAddress(this.name, {
assertFixedScriptWalletAddress(this.wasmName, {
address,
keychains,
format: params.format ?? 'base58',
Expand Down Expand Up @@ -739,7 +736,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici
throw new Error('missing required param keychains');
}

const address = generateAddress(this.name, {
const address = generateAddress(this.wasmName, {
// fixed-script (multisig) coins derive from the xpub triple via `pub`
keychains: keychains as { pub: string }[],
chain,
Expand All @@ -755,7 +752,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici
* @returns true iff coin supports spending from unspentType
*/
supportsAddressType(addressType: ScriptType2Of3): boolean {
return fixedScriptWallet.supportsScriptType(this.name, addressType);
return fixedScriptWallet.supportsScriptType(this.wasmName, addressType);
}

/** inherited doc */
Expand Down Expand Up @@ -789,7 +786,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici
.post(this.url('/wallet/' + walletId + '/tx/signpsbt'))
.send({ psbt: buffer.toString('hex') })
.result();
return decodePsbt(response.psbt, this.name);
return decodePsbt(response.psbt, this.wasmName);
}

/**
Expand Down Expand Up @@ -907,9 +904,9 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici
if (wallet && isDescriptorWallet(wallet)) {
// Descriptor wallets decode prebuild bytes straight into the wasm-utxo
// descriptor Psbt, skipping the fixedScriptWallet.BitGoPsbt intermediate.
return explainTx(decodeDescriptorPsbt(params), { ...params, wallet }, this.name);
return explainTx(decodeDescriptorPsbt(params), { ...params, wallet }, this.wasmName);
}
return explainTx(this.decodeTransactionFromPrebuild(params), { ...params, wallet }, this.name);
return explainTx(this.decodeTransactionFromPrebuild(params), { ...params, wallet }, this.wasmName);
}

/**
Expand Down
17 changes: 10 additions & 7 deletions modules/abstract-utxo/src/address/fixedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '@bitgo/sdk-core';
import { fixedScriptWallet } from '@bitgo/wasm-utxo';

import { UtxoCoinName } from '../names';
import { toWasmUtxoCoinName, UtxoCoinName, WasmUtxoCoinName } from '../names';

type ScriptType2Of3 = fixedScriptWallet.OutputScriptType;
type ChainCode = fixedScriptWallet.ChainCode;
Expand All @@ -35,8 +35,8 @@ interface GenerateFixedScriptAddressOptions extends GenerateAddressOptions {
keychains: { pub: string }[];
}

function supportsAddressType(coinName: UtxoCoinName, addressType: ScriptType2Of3): boolean {
return fixedScriptWallet.supportsScriptType(coinName, addressType);
function supportsAddressType(coinName: UtxoCoinName | WasmUtxoCoinName, addressType: ScriptType2Of3): boolean {
return fixedScriptWallet.supportsScriptType(toWasmUtxoCoinName(coinName), addressType);
}

/**
Expand All @@ -47,7 +47,7 @@ function normalizeScriptType(scriptType: ScriptType2Of3 | 'p2tr'): ScriptType2Of
}

export function generateAddressWithChainAndIndex(
coinName: UtxoCoinName,
coinName: UtxoCoinName | WasmUtxoCoinName,
keychains: fixedScriptWallet.WalletKeysArg | Triple<string>,
chain: ChainCode,
index: number,
Expand All @@ -56,7 +56,7 @@ export function generateAddressWithChainAndIndex(
// Convert CreateAddressFormat to AddressFormat for wasm-utxo
// 'base58' -> 'default', 'cashaddr' -> 'cashaddr'
const wasmFormat = format === 'base58' ? 'default' : format;
return fixedScriptWallet.address(keychains, chain, index, coinName, wasmFormat);
return fixedScriptWallet.address(keychains, chain, index, toWasmUtxoCoinName(coinName), wasmFormat);
}

/**
Expand All @@ -70,7 +70,10 @@ export function generateAddressWithChainAndIndex(
* @param params.bech32 {boolean} Deprecated
* @returns {string} The generated address
*/
export function generateAddress(coinName: UtxoCoinName, params: GenerateFixedScriptAddressOptions): string {
export function generateAddress(
coinName: UtxoCoinName | WasmUtxoCoinName,
params: GenerateFixedScriptAddressOptions
): string {
let derivationIndex = 0;
if (_.isInteger(params.index) && (params.index as number) > 0) {
derivationIndex = params.index as number;
Expand Down Expand Up @@ -142,7 +145,7 @@ type Keychain = {
};

export function assertFixedScriptWalletAddress(
coinName: UtxoCoinName,
coinName: UtxoCoinName | WasmUtxoCoinName,
{
chain,
index,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import assert from 'assert';

import { Descriptor, address, descriptorWallet } from '@bitgo/wasm-utxo';
import { Descriptor, descriptorWallet } from '@bitgo/wasm-utxo';

import { UtxoCoinSpecific, VerifyAddressOptions } from '../abstractUtxoCoin';
import { UtxoCoinName } from '../names';
import { AddressCodec } from '../transaction/recipient';

class DescriptorAddressMismatchError extends Error {
constructor(descriptor: Descriptor, index: number, derivedAddress: string, expectedAddress: string) {
Expand All @@ -14,7 +14,7 @@ class DescriptorAddressMismatchError extends Error {
}

export function assertDescriptorWalletAddress(
coinName: UtxoCoinName,
addressCodec: AddressCodec,
params: VerifyAddressOptions<UtxoCoinSpecific>,
descriptors: descriptorWallet.DescriptorMap
): void {
Expand All @@ -33,7 +33,7 @@ export function assertDescriptorWalletAddress(
);
}
const derivedScript = Buffer.from(descriptor.atDerivationIndex(params.index).scriptPubkey());
const derivedAddress = address.fromOutputScriptWithCoin(derivedScript, coinName);
const derivedAddress = addressCodec.toExtendedAddressFormat(derivedScript);
if (params.address !== derivedAddress) {
throw new DescriptorAddressMismatchError(descriptor, params.index, derivedAddress, params.address);
}
Expand Down
43 changes: 38 additions & 5 deletions modules/abstract-utxo/src/impl/bch/bch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@ import { BitGoBase } from '@bitgo/sdk-core';
import { address as wasmAddress } from '@bitgo/wasm-utxo';

import { AbstractUtxoCoin } from '../../abstractUtxoCoin';
import { UtxoCoinName } from '../../names';
import { UtxoCoinName, WasmUtxoCoinName } from '../../names';
import { AddressCodec } from '../../transaction';

type BchAddressFormat = 'default' | 'cashaddr';

class BchAddressCodec extends AddressCodec {
constructor(coinName: UtxoCoinName, wasmName: WasmUtxoCoinName, private readonly format: BchAddressFormat) {
super(coinName, wasmName);
}

override encode(script: Uint8Array): string {
return wasmAddress.fromOutputScriptWithCoin(script, this.wasmName, this.format);
}
}

export class Bch extends AbstractUtxoCoin {
readonly name: UtxoCoinName = 'bch';

Expand All @@ -16,6 +28,26 @@ export class Bch extends AbstractUtxoCoin {
return new Bch(bitgo);
}

private getBchAddressCodec(format: BchAddressFormat): BchAddressCodec {
return new BchAddressCodec(this.name, this.wasmName, format);
}

override get addressCodec(): BchAddressCodec {
return this.getBchAddressCodec('default');
}

override isValidAddress(
address: string,
param?: { anyFormat?: boolean; allowLightning?: boolean } | boolean
): boolean {
const anyFormat = typeof param === 'object' ? param?.anyFormat ?? true : true;
const isDefaultAddress = super.isValidAddress(address, param);
if (isDefaultAddress || !anyFormat) {
return isDefaultAddress;
}
return this.getBchAddressCodec('cashaddr').isValidAddress(address);
}

/**
* Canonicalize a Bitcoin Cash address for a specific version
*
Expand All @@ -34,13 +66,14 @@ export class Bch extends AbstractUtxoCoin {
}

if (version === 'base58') {
const script = wasmAddress.toOutputScriptWithCoin(address, this.name);
return wasmAddress.fromOutputScriptWithCoin(script, this.name, 'default');
const codec = this.addressCodec;
const script = codec.decode(address);
return codec.encode(script);
}

if (version === 'cashaddr') {
const script = wasmAddress.toOutputScriptWithCoin(address, this.name);
return wasmAddress.fromOutputScriptWithCoin(script, this.name, 'cashaddr');
const codec = this.getBchAddressCodec('cashaddr');
return codec.encode(codec.decode(address));
}

throw new Error(`invalid version ${version}`);
Expand Down
6 changes: 3 additions & 3 deletions modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class InscriptionBuilder implements IInscriptionBuilder {
derivedKey.publicKey,
contentType,
inscriptionData,
this.coin.name
this.coin.wasmName
);

// Convert TapLeafScript to utxolib format for backwards compatibility
Expand Down Expand Up @@ -121,7 +121,7 @@ export class InscriptionBuilder implements IInscriptionBuilder {
}

const psbt = createPsbtForSingleInscriptionPassingTransaction(
this.coin.name,
this.coin.wasmName,
{
walletKeys: walletXpubs,
signer,
Expand Down Expand Up @@ -279,7 +279,7 @@ export class InscriptionBuilder implements IInscriptionBuilder {
commitAddress,
recipientAddress,
Buffer.from(halfSignedCommitTransaction.txHex, 'hex'),
this.coin.name
this.coin.wasmName
);

return this.wallet.submitTransaction({
Expand Down
3 changes: 2 additions & 1 deletion modules/abstract-utxo/src/impl/zec/zec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export class Zec extends AbstractUtxoCoin {

isValidAddress(address: string, param?: { anyFormat?: boolean; allowLightning?: boolean } | boolean): boolean {
return (
zcashAddress.hasTransparentReceiver(address, this.name) || zcashAddress.hasOrchardReceiver(address, this.name)
zcashAddress.hasTransparentReceiver(address, this.wasmName) ||
zcashAddress.hasOrchardReceiver(address, this.wasmName)
);
}
}
11 changes: 11 additions & 0 deletions modules/abstract-utxo/src/names.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { isCoinName, type CoinName } from '@bitgo/wasm-utxo';

export type WasmUtxoCoinName = CoinName;

export const utxoCoinsMainnet = ['btc', 'bch', 'bcha', 'bsv', 'btg', 'dash', 'doge', 'ltc', 'pearl', 'zec'] as const;
export const utxoCoinsTestnet = [
'tbtc',
Expand All @@ -19,6 +23,13 @@ export type UtxoCoinNameMainnet = (typeof utxoCoinsMainnet)[number];
export type UtxoCoinNameTestnet = `t${UtxoCoinNameMainnet}` | 'tbtcsig' | 'tbtc4' | 'tbtcbgsig';
export type UtxoCoinName = UtxoCoinNameMainnet | UtxoCoinNameTestnet;

export function toWasmUtxoCoinName(coinName: UtxoCoinName | WasmUtxoCoinName): WasmUtxoCoinName {
if (!isCoinName(coinName)) {
throw new Error(`coin ${coinName} is not supported by wasm-utxo`);
}
return coinName;
}

export function isUtxoCoinNameMainnet(coinName: string): coinName is UtxoCoinNameMainnet {
return utxoCoinsMainnet.includes(coinName as UtxoCoinNameMainnet);
}
Expand Down
Loading
Loading