Skip to content

Commit 14bcb12

Browse files
Merge pull request #7559 from BitGo/BTC-2668.backupkeyrecovery-utxo-wasm
feat(abstract-utxo): update backup key recovery to use standard address generation
2 parents 3db7b2a + e4a6b38 commit 14bcb12

File tree

3 files changed

+48
-50
lines changed

3 files changed

+48
-50
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -815,27 +815,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
815815
return explainTx(this.decodeTransactionFromPrebuild(params), params, this.network);
816816
}
817817

818-
/**
819-
* Create a multisig address of a given type from a list of keychains and a signing threshold
820-
* @param addressType
821-
* @param signatureThreshold
822-
* @param keys
823-
*/
824-
createMultiSigAddress(addressType: ScriptType2Of3, signatureThreshold: number, keys: Buffer[]): MultiSigAddress {
825-
const {
826-
scriptPubKey: outputScript,
827-
redeemScript,
828-
witnessScript,
829-
} = utxolib.bitgo.outputScripts.createOutputScript2of3(keys, addressType);
830-
831-
return {
832-
outputScript,
833-
redeemScript,
834-
witnessScript,
835-
address: utxolib.address.fromOutputScript(outputScript, this.network),
836-
};
837-
}
838-
839818
/**
840819
* @deprecated - use {@see backupKeyRecovery}
841820
* Builds a funds recovery transaction without BitGo

modules/abstract-utxo/src/address/fixedScript.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
P2trUnsupportedError,
1010
P2wshUnsupportedError,
1111
UnsupportedAddressTypeError,
12-
sanitizeLegacyPath,
12+
isTriple,
13+
Triple,
1314
} from '@bitgo/sdk-core';
1415
import * as utxolib from '@bitgo/utxo-lib';
1516
import { bitgo } from '@bitgo/utxo-lib';
@@ -52,7 +53,7 @@ function supportsAddressType(network: utxolib.Network, addressType: ScriptType2O
5253

5354
export function generateAddressWithChainAndIndex(
5455
network: utxolib.Network,
55-
keychains: { pub: string }[],
56+
keychains: bitgo.RootWalletKeys | Triple<string>,
5657
chain: bitgo.ChainCode,
5758
index: number,
5859
format: CreateAddressFormat | undefined
@@ -61,24 +62,19 @@ export function generateAddressWithChainAndIndex(
6162
// Convert CreateAddressFormat to AddressFormat for wasm-utxo
6263
// 'base58' -> 'default', 'cashaddr' -> 'cashaddr'
6364
const wasmFormat = format === 'base58' ? 'default' : format;
64-
return wasmUtxo.fixedScriptWallet.address(
65-
keychains.map((k) => k.pub) as [string, string, string],
66-
chain,
67-
index,
68-
network,
69-
wasmFormat
70-
);
65+
return wasmUtxo.fixedScriptWallet.address(keychains, chain, index, network, wasmFormat);
66+
}
67+
68+
if (!(keychains instanceof bitgo.RootWalletKeys)) {
69+
const hdNodes = keychains.map((pub) => bip32.fromBase58(pub));
70+
keychains = new bitgo.RootWalletKeys(hdNodes as Triple<utxolib.BIP32Interface>);
7171
}
7272

73-
const path = '0/0/' + chain + '/' + index;
74-
const hdNodes = keychains.map(({ pub }) => bip32.fromBase58(pub));
75-
const derivedKeys = hdNodes.map((hdNode) => hdNode.derivePath(sanitizeLegacyPath(path)).publicKey);
7673
const addressType = bitgo.scriptTypeForChain(chain);
7774

75+
const derivedKeys = keychains.deriveForChainAndIndex(chain, index).publicKeys;
7876
const { scriptPubKey: outputScript } = utxolib.bitgo.outputScripts.createOutputScript2of3(derivedKeys, addressType);
79-
8077
const address = utxolib.address.fromOutputScript(outputScript, network);
81-
8278
return canonicalAddress(network, address, format);
8379
}
8480

@@ -143,7 +139,17 @@ export function generateAddress(network: utxolib.Network, params: GenerateFixedS
143139
}
144140
}
145141

146-
return generateAddressWithChainAndIndex(network, keychains, derivationChain, derivationIndex, params.format);
142+
if (!isTriple(keychains)) {
143+
throw new Error('keychains must be a triple');
144+
}
145+
146+
return generateAddressWithChainAndIndex(
147+
network,
148+
keychains.map((k) => k.pub) as Triple<string>,
149+
derivationChain,
150+
derivationIndex,
151+
params.format
152+
);
147153
}
148154

149155
type Keychain = {

modules/abstract-utxo/src/recovery/backupKeyRecovery.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
} from '@bitgo/sdk-core';
1616
import { getMainnet, networks } from '@bitgo/utxo-lib';
1717

18-
import { AbstractUtxoCoin, MultiSigAddress } from '../abstractUtxoCoin';
18+
import { AbstractUtxoCoin } from '../abstractUtxoCoin';
1919
import { signAndVerifyPsbt } from '../sign';
20+
import { generateAddressWithChainAndIndex } from '../address';
2021

2122
import { forCoin, RecoveryProvider } from './RecoveryProvider';
2223
import { MempoolApi } from './mempoolApi';
@@ -125,12 +126,27 @@ export interface RecoverParams {
125126
feeRate?: number;
126127
}
127128

128-
function getFormattedAddress(coin: AbstractUtxoCoin, address: MultiSigAddress) {
129-
// Blockchair uses cashaddr format when querying the API for address information. Convert legacy addresses to cashaddr
130-
// before querying the API.
131-
return coin.getChain() === 'bch' || coin.getChain() === 'bcha'
132-
? coin.canonicalAddress(address.address, 'cashaddr').split(':')[1]
133-
: address.address;
129+
/**
130+
* Generate an address and format it for API queries
131+
* @param coin - The coin instance
132+
* @param network - The network to use
133+
* @param walletKeys - The wallet keys
134+
* @param chain - The chain code
135+
* @param addrIndex - The address index
136+
* @returns The formatted address (with cashaddr prefix stripped for BCH/BCHA)
137+
*/
138+
function getFormattedAddress(
139+
coin: AbstractUtxoCoin,
140+
network: utxolib.Network,
141+
walletKeys: RootWalletKeys,
142+
chain: ChainCode,
143+
addrIndex: number
144+
): string {
145+
const format = coin.getChain() === 'bch' || coin.getChain() === 'bcha' ? 'cashaddr' : undefined;
146+
const address = generateAddressWithChainAndIndex(network, walletKeys, chain, addrIndex, format);
147+
148+
// Blockchair uses cashaddr format when querying the API for address information. Strip the prefix for BCH/BCHA.
149+
return format === 'cashaddr' ? address.split(':')[1] : address;
134150
}
135151

136152
async function queryBlockchainUnspentsPath(
@@ -157,10 +173,7 @@ async function queryBlockchainUnspentsPath(
157173
}
158174

159175
async function gatherUnspents(addrIndex: number) {
160-
const walletKeysForUnspent = walletKeys.deriveForChainAndIndex(chain, addrIndex);
161-
const address = coin.createMultiSigAddress(scriptType, 2, walletKeysForUnspent.publicKeys);
162-
163-
const formattedAddress = getFormattedAddress(coin, address);
176+
const formattedAddress = getFormattedAddress(coin, coin.network, walletKeys, chain, addrIndex);
164177
const addrInfo = await recoveryProvider.getAddressInfo(formattedAddress);
165178
// we use txCount here because it implies usage - having tx'es means the addr was generated and used
166179
if (addrInfo.txCount === 0) {
@@ -169,7 +182,7 @@ async function queryBlockchainUnspentsPath(
169182
numSequentialAddressesWithoutTxs = 0;
170183

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

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

0 commit comments

Comments
 (0)