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
124 changes: 7 additions & 117 deletions modules/abstract-utxo/src/abstractUtxoCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { bip32 } from '@bitgo/secp256k1';
import { bitgo, getMainnet, isMainnet, isTestnet } from '@bitgo/utxo-lib';
import {
AddressCoinSpecific,
AddressTypeChainMismatchError,
BaseCoin,
BitGoBase,
CreateAddressFormat,
Expand All @@ -25,24 +24,17 @@ import {
MismatchedRecipient,
MultisigType,
multisigTypes,
P2shP2wshUnsupportedError,
P2trMusig2UnsupportedError,
P2trUnsupportedError,
P2wshUnsupportedError,
ParseTransactionOptions as BaseParseTransactionOptions,
PrecreateBitGoOptions,
PresignTransactionOptions,
RequestTracer,
sanitizeLegacyPath,
SignedTransaction,
SignTransactionOptions as BaseSignTransactionOptions,
SupplementGenerateWalletOptions,
TransactionParams as BaseTransactionParams,
TransactionPrebuild as BaseTransactionPrebuild,
Triple,
TxIntentMismatchRecipientError,
UnexpectedAddressError,
UnsupportedAddressTypeError,
VerificationOptions,
VerifyAddressOptions as BaseVerifyAddressOptions,
VerifyTransactionOptions as BaseVerifyTransactionOptions,
Expand Down Expand Up @@ -81,6 +73,7 @@ import {
} from './transaction/descriptor/verifyTransaction';
import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptorWallet } from './descriptor';
import { getChainFromNetwork, getFamilyFromNetwork, getFullNameFromNetwork } from './names';
import { assertFixedScriptWalletAddress } from './address/fixedScript';
import { CustomChangeOptions } from './transaction/fixedScript';
import { toBip32Triple, UtxoKeychain, UtxoNamedKeychains } from './keychains';
import { verifyKeySignature, verifyUserPublicKey } from './verifyKey';
Expand Down Expand Up @@ -116,7 +109,7 @@ type UtxoCustomSigningFunction<TNumber extends number | bigint> = {
}): Promise<SignedTransaction>;
};

const { getExternalChainCode, isChainCode, scriptTypeForChain, outputScripts } = bitgo;
const { isChainCode, scriptTypeForChain, outputScripts } = bitgo;

type Unspent<TNumber extends number | bigint = number> = bitgo.Unspent<TNumber>;

Expand Down Expand Up @@ -707,7 +700,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
* @throws {UnexpectedAddressError}
*/
async isWalletAddress(params: VerifyAddressOptions<UtxoCoinSpecific>, wallet?: IWallet): Promise<boolean> {
const { address, addressType, keychains, chain, index } = params;
const { address, keychains, chain, index } = params;

if (!this.isValidAddress(address)) {
throw new InvalidAddressError(`invalid address: ${address}`);
Expand Down Expand Up @@ -738,21 +731,15 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
throw new Error('missing required param keychains');
}

const expectedAddress = this.generateAddress({
format: params.format,
addressType: addressType as ScriptType2Of3,
assertFixedScriptWalletAddress(this.network, {
address,
keychains,
threshold: 2,
format: params.format ?? 'base58',
addressType: params.addressType,
chain,
index,
});

if (expectedAddress.address !== address) {
throw new UnexpectedAddressError(
`address validation failure: expected ${expectedAddress.address} but got ${address}`
);
}

return true;
}

Expand Down Expand Up @@ -781,103 +768,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
return [KeyIndices.USER, KeyIndices.BACKUP, KeyIndices.BITGO];
}

/**
* TODO(BG-11487): Remove addressType, segwit, and bech32 params in SDKv6
* Generate an address for a wallet based on a set of configurations
* @param params.addressType {string} Deprecated
* @param params.keychains {[object]} Array of objects with xpubs
* @param params.threshold {number} Minimum number of signatures
* @param params.chain {number} Derivation chain (see https://github.com/BitGo/unspents/blob/master/src/codes.ts for
* the corresponding address type of a given chain code)
* @param params.index {number} Derivation index
* @param params.segwit {boolean} Deprecated
* @param params.bech32 {boolean} Deprecated
* @returns {{chain: number, index: number, coin: number, coinSpecific: {outputScript, redeemScript}}}
*/
generateAddress(params: GenerateFixedScriptAddressOptions): AddressDetails {
let derivationIndex = 0;
if (_.isInteger(params.index) && (params.index as number) > 0) {
derivationIndex = params.index as number;
}

const { keychains, threshold, chain, segwit = false, bech32 = false } = params as GenerateFixedScriptAddressOptions;

let derivationChain = getExternalChainCode('p2sh');
if (_.isNumber(chain) && _.isInteger(chain) && isChainCode(chain)) {
derivationChain = chain;
}

function convertFlagsToAddressType(): ScriptType2Of3 {
if (isChainCode(chain)) {
return utxolib.bitgo.scriptTypeForChain(chain);
}
if (_.isBoolean(segwit) && segwit) {
return 'p2shP2wsh';
} else if (_.isBoolean(bech32) && bech32) {
return 'p2wsh';
} else {
return 'p2sh';
}
}

const addressType = params.addressType || convertFlagsToAddressType();

if (addressType !== utxolib.bitgo.scriptTypeForChain(derivationChain)) {
throw new AddressTypeChainMismatchError(addressType, derivationChain);
}

if (!this.supportsAddressType(addressType)) {
switch (addressType) {
case 'p2sh':
throw new Error(`internal error: p2sh should always be supported`);
case 'p2shP2wsh':
throw new P2shP2wshUnsupportedError();
case 'p2wsh':
throw new P2wshUnsupportedError();
case 'p2tr':
throw new P2trUnsupportedError();
case 'p2trMusig2':
throw new P2trMusig2UnsupportedError();
default:
throw new UnsupportedAddressTypeError();
}
}

let signatureThreshold = 2;
if (_.isInteger(threshold)) {
signatureThreshold = threshold as number;
if (signatureThreshold <= 0) {
throw new Error('threshold has to be positive');
}
if (signatureThreshold > keychains.length) {
throw new Error('threshold cannot exceed number of keys');
}
}

const path = '0/0/' + derivationChain + '/' + derivationIndex;
const hdNodes = keychains.map(({ pub }) => bip32.fromBase58(pub));
const derivedKeys = hdNodes.map((hdNode) => hdNode.derivePath(sanitizeLegacyPath(path)).publicKey);

const { outputScript, redeemScript, witnessScript, address } = this.createMultiSigAddress(
addressType,
signatureThreshold,
derivedKeys
);

return {
address: this.canonicalAddress(address, params.format),
chain: derivationChain,
index: derivationIndex,
coin: this.getChain(),
coinSpecific: {
outputScript: outputScript.toString('hex'),
redeemScript: redeemScript && redeemScript.toString('hex'),
witnessScript: witnessScript && witnessScript.toString('hex'),
},
addressType,
};
}

/**
* @returns input psbt added with deterministic MuSig2 nonce for bitgo key for each MuSig2 inputs.
* @param psbtHex all MuSig2 inputs should contain user MuSig2 nonce
Expand Down
178 changes: 178 additions & 0 deletions modules/abstract-utxo/src/address/fixedScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import _ from 'lodash';
import {
AddressTypeChainMismatchError,
CreateAddressFormat,
InvalidAddressDerivationPropertyError,
UnexpectedAddressError,
P2shP2wshUnsupportedError,
P2trMusig2UnsupportedError,
P2trUnsupportedError,
P2wshUnsupportedError,
UnsupportedAddressTypeError,
sanitizeLegacyPath,
} from '@bitgo/sdk-core';
import * as utxolib from '@bitgo/utxo-lib';
import { bitgo } from '@bitgo/utxo-lib';
import { bip32 } from '@bitgo/secp256k1';

type ScriptType2Of3 = bitgo.outputScripts.ScriptType2Of3;

export interface FixedScriptAddressCoinSpecific {
outputScript?: string;
redeemScript?: string;
witnessScript?: string;
}

export interface GenerateAddressOptions {
addressType?: ScriptType2Of3;
chain?: number;
index?: number;
segwit?: boolean;
bech32?: boolean;
}

interface GenerateFixedScriptAddressOptions extends GenerateAddressOptions {
format?: CreateAddressFormat;
keychains: { pub: string }[];
}

function canonicalAddress(network: utxolib.Network, address: string, format?: CreateAddressFormat): string {
if (format === 'cashaddr') {
const script = utxolib.addressFormat.toOutputScriptTryFormats(address, network);
return utxolib.addressFormat.fromOutputScriptWithFormat(script, format, network);
}
// Default to canonical format (base58 for most coins)
return utxolib.addressFormat.toCanonicalFormat(address, network);
}

function supportsAddressType(network: utxolib.Network, addressType: ScriptType2Of3): boolean {
return utxolib.bitgo.outputScripts.isSupportedScriptType(network, addressType);
}

export function generateAddressWithChainAndIndex(
network: utxolib.Network,
keychains: { pub: string }[],
chain: bitgo.ChainCode,
index: number,
format: CreateAddressFormat | undefined
): string {
const path = '0/0/' + chain + '/' + index;
const hdNodes = keychains.map(({ pub }) => bip32.fromBase58(pub));
const derivedKeys = hdNodes.map((hdNode) => hdNode.derivePath(sanitizeLegacyPath(path)).publicKey);
const addressType = bitgo.scriptTypeForChain(chain);

const { scriptPubKey: outputScript } = utxolib.bitgo.outputScripts.createOutputScript2of3(derivedKeys, addressType);

const address = utxolib.address.fromOutputScript(outputScript, network);

return canonicalAddress(network, address, format);
}

/**
* Generate an address for a wallet based on a set of configurations
* @param params.addressType {string} Deprecated
* @param params.keychains {[object]} Array of objects with xpubs
* @param params.threshold {number} Minimum number of signatures
* @param params.chain {number} Derivation chain (see https://github.com/BitGo/unspents/blob/master/src/codes.ts for
* the corresponding address type of a given chain code)
* @param params.index {number} Derivation index
* @param params.segwit {boolean} Deprecated
* @param params.bech32 {boolean} Deprecated
* @returns {string} The generated address
*/
export function generateAddress(network: utxolib.Network, params: GenerateFixedScriptAddressOptions): string {
let derivationIndex = 0;
if (_.isInteger(params.index) && (params.index as number) > 0) {
derivationIndex = params.index as number;
}

const { keychains, chain, segwit = false, bech32 = false } = params as GenerateFixedScriptAddressOptions;

let derivationChain = bitgo.getExternalChainCode('p2sh');
if (_.isNumber(chain) && _.isInteger(chain) && bitgo.isChainCode(chain)) {
derivationChain = chain;
}

function convertFlagsToAddressType(): ScriptType2Of3 {
if (bitgo.isChainCode(chain)) {
return bitgo.scriptTypeForChain(chain);
}
if (_.isBoolean(segwit) && segwit) {
return 'p2shP2wsh';
} else if (_.isBoolean(bech32) && bech32) {
return 'p2wsh';
} else {
return 'p2sh';
}
}

const addressType = params.addressType || convertFlagsToAddressType();

if (addressType !== utxolib.bitgo.scriptTypeForChain(derivationChain)) {
throw new AddressTypeChainMismatchError(addressType, derivationChain);
}

if (!supportsAddressType(network, addressType)) {
switch (addressType) {
case 'p2sh':
throw new Error(`internal error: p2sh should always be supported`);
case 'p2shP2wsh':
throw new P2shP2wshUnsupportedError();
case 'p2wsh':
throw new P2wshUnsupportedError();
case 'p2tr':
throw new P2trUnsupportedError();
case 'p2trMusig2':
throw new P2trMusig2UnsupportedError();
default:
throw new UnsupportedAddressTypeError();
}
}

return generateAddressWithChainAndIndex(network, keychains, derivationChain, derivationIndex, params.format);
}

type Keychain = {
pub: string;
};

export function assertFixedScriptWalletAddress(
network: utxolib.Network,
{
chain,
index,
keychains,
format,
addressType,
address,
}: {
chain: number | undefined;
index: number;
keychains: Keychain[];
format: CreateAddressFormat;
addressType: string | undefined;
address: string;
}
): void {
if ((_.isUndefined(chain) && _.isUndefined(index)) || !(_.isFinite(chain) && _.isFinite(index))) {
throw new InvalidAddressDerivationPropertyError(
`address validation failure: invalid chain (${chain}) or index (${index})`
);
}

if (!keychains) {
throw new Error('missing required param keychains');
}

const expectedAddress = generateAddress(network, {
format,
addressType: addressType as ScriptType2Of3,
keychains,
chain,
index,
});

if (expectedAddress !== address) {
throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`);
}
}
6 changes: 6 additions & 0 deletions modules/abstract-utxo/src/address/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {
generateAddress,
generateAddressWithChainAndIndex,
assertFixedScriptWalletAddress,
FixedScriptAddressCoinSpecific,
} from './fixedScript';
1 change: 1 addition & 0 deletions modules/abstract-utxo/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './abstractUtxoCoin';
export * from './address';
export * from './config';
export * from './recovery';
export * from './replayProtection';
Expand Down
Loading