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
21 changes: 0 additions & 21 deletions modules/abstract-utxo/src/abstractUtxoCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,27 +814,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
return explainTx(this.decodeTransactionFromPrebuild(params), params, this.network);
}

/**
* Create a multisig address of a given type from a list of keychains and a signing threshold
* @param addressType
* @param signatureThreshold
* @param keys
*/
createMultiSigAddress(addressType: ScriptType2Of3, signatureThreshold: number, keys: Buffer[]): MultiSigAddress {
const {
scriptPubKey: outputScript,
redeemScript,
witnessScript,
} = utxolib.bitgo.outputScripts.createOutputScript2of3(keys, addressType);

return {
outputScript,
redeemScript,
witnessScript,
address: utxolib.address.fromOutputScript(outputScript, this.network),
};
}

/**
* @deprecated - use {@see backupKeyRecovery}
* Builds a funds recovery transaction without BitGo
Expand Down
36 changes: 21 additions & 15 deletions modules/abstract-utxo/src/address/fixedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
P2trUnsupportedError,
P2wshUnsupportedError,
UnsupportedAddressTypeError,
sanitizeLegacyPath,
isTriple,
Triple,
} from '@bitgo/sdk-core';
import * as utxolib from '@bitgo/utxo-lib';
import { bitgo } from '@bitgo/utxo-lib';
Expand Down Expand Up @@ -52,7 +53,7 @@ function supportsAddressType(network: utxolib.Network, addressType: ScriptType2O

export function generateAddressWithChainAndIndex(
network: utxolib.Network,
keychains: { pub: string }[],
keychains: bitgo.RootWalletKeys | Triple<string>,
chain: bitgo.ChainCode,
index: number,
format: CreateAddressFormat | undefined
Expand All @@ -61,24 +62,19 @@ export function generateAddressWithChainAndIndex(
// Convert CreateAddressFormat to AddressFormat for wasm-utxo
// 'base58' -> 'default', 'cashaddr' -> 'cashaddr'
const wasmFormat = format === 'base58' ? 'default' : format;
return wasmUtxo.fixedScriptWallet.address(
keychains.map((k) => k.pub) as [string, string, string],
chain,
index,
network,
wasmFormat
);
return wasmUtxo.fixedScriptWallet.address(keychains, chain, index, network, wasmFormat);
}

if (!(keychains instanceof bitgo.RootWalletKeys)) {
const hdNodes = keychains.map((pub) => bip32.fromBase58(pub));
keychains = new bitgo.RootWalletKeys(hdNodes as Triple<utxolib.BIP32Interface>);
}

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 derivedKeys = keychains.deriveForChainAndIndex(chain, index).publicKeys;
const { scriptPubKey: outputScript } = utxolib.bitgo.outputScripts.createOutputScript2of3(derivedKeys, addressType);

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

return canonicalAddress(network, address, format);
}

Expand Down Expand Up @@ -143,7 +139,17 @@ export function generateAddress(network: utxolib.Network, params: GenerateFixedS
}
}

return generateAddressWithChainAndIndex(network, keychains, derivationChain, derivationIndex, params.format);
if (!isTriple(keychains)) {
throw new Error('keychains must be a triple');
}

return generateAddressWithChainAndIndex(
network,
keychains.map((k) => k.pub) as Triple<string>,
derivationChain,
derivationIndex,
params.format
);
}

type Keychain = {
Expand Down
41 changes: 27 additions & 14 deletions modules/abstract-utxo/src/recovery/backupKeyRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
} from '@bitgo/sdk-core';
import { getMainnet, networks } from '@bitgo/utxo-lib';

import { AbstractUtxoCoin, MultiSigAddress } from '../abstractUtxoCoin';
import { AbstractUtxoCoin } from '../abstractUtxoCoin';
import { signAndVerifyPsbt } from '../sign';
import { generateAddressWithChainAndIndex } from '../address';

import { forCoin, RecoveryProvider } from './RecoveryProvider';
import { MempoolApi } from './mempoolApi';
Expand Down Expand Up @@ -125,12 +126,27 @@ export interface RecoverParams {
feeRate?: number;
}

function getFormattedAddress(coin: AbstractUtxoCoin, address: MultiSigAddress) {
// Blockchair uses cashaddr format when querying the API for address information. Convert legacy addresses to cashaddr
// before querying the API.
return coin.getChain() === 'bch' || coin.getChain() === 'bcha'
? coin.canonicalAddress(address.address, 'cashaddr').split(':')[1]
: address.address;
/**
* Generate an address and format it for API queries
* @param coin - The coin instance
* @param network - The network to use
* @param walletKeys - The wallet keys
* @param chain - The chain code
* @param addrIndex - The address index
* @returns The formatted address (with cashaddr prefix stripped for BCH/BCHA)
*/
function getFormattedAddress(
coin: AbstractUtxoCoin,
network: utxolib.Network,
walletKeys: RootWalletKeys,
chain: ChainCode,
addrIndex: number
): string {
const format = coin.getChain() === 'bch' || coin.getChain() === 'bcha' ? 'cashaddr' : undefined;
const address = generateAddressWithChainAndIndex(network, walletKeys, chain, addrIndex, format);

// Blockchair uses cashaddr format when querying the API for address information. Strip the prefix for BCH/BCHA.
return format === 'cashaddr' ? address.split(':')[1] : address;
}

async function queryBlockchainUnspentsPath(
Expand All @@ -157,10 +173,7 @@ async function queryBlockchainUnspentsPath(
}

async function gatherUnspents(addrIndex: number) {
const walletKeysForUnspent = walletKeys.deriveForChainAndIndex(chain, addrIndex);
const address = coin.createMultiSigAddress(scriptType, 2, walletKeysForUnspent.publicKeys);

const formattedAddress = getFormattedAddress(coin, address);
const formattedAddress = getFormattedAddress(coin, coin.network, walletKeys, chain, addrIndex);
const addrInfo = await recoveryProvider.getAddressInfo(formattedAddress);
// we use txCount here because it implies usage - having tx'es means the addr was generated and used
if (addrInfo.txCount === 0) {
Expand All @@ -169,7 +182,7 @@ async function queryBlockchainUnspentsPath(
numSequentialAddressesWithoutTxs = 0;

if (addrInfo.balance > 0) {
console.log(`Found an address with balance: ${address.address} with balance ${addrInfo.balance}`);
console.log(`Found an address with balance: ${formattedAddress} with balance ${addrInfo.balance}`);
const addressUnspents = await recoveryProvider.getUnspentsForAddresses([formattedAddress]);
const processedUnspents = await Promise.all(
addressUnspents.map(async (u): Promise<WalletUnspent<bigint>> => {
Expand Down Expand Up @@ -375,9 +388,9 @@ export async function backupKeyRecovery(
const recoveryAmount = totalInputAmount - approximateFee - krsFee;

if (recoveryAmount < BigInt(0)) {
throw new Error(`this wallet\'s balance is too low to pay the fees specified by the KRS provider.
throw new Error(`this wallet\'s balance is too low to pay the fees specified by the KRS provider.
Existing balance on wallet: ${totalInputAmount.toString()}. Estimated network fee for the recovery transaction
: ${approximateFee.toString()}, KRS fee to pay: ${krsFee.toString()}. After deducting fees, your total
: ${approximateFee.toString()}, KRS fee to pay: ${krsFee.toString()}. After deducting fees, your total
recoverable balance is ${recoveryAmount.toString()}`);
}

Expand Down