From 7e84fc850db6d086a4aa159ac6367e068c66f861 Mon Sep 17 00:00:00 2001 From: Kenny Joseph Date: Wed, 18 Mar 2026 10:45:08 -0400 Subject: [PATCH 1/3] serverAssistedImportV2 --- packages/bitcore-wallet-client/src/index.ts | 1 + packages/bitcore-wallet-client/src/lib/api.ts | 622 +++++++++++++++++- .../src/lib/common/utils.ts | 2 +- packages/bitcore-wallet-client/src/lib/key.ts | 56 +- .../src/types/serverAssistedImportEvents.d.ts | 150 +++++ .../bitcore-wallet-client/test/api.test.ts | 105 +++ .../bitcore-wallet-client/test/key.test.ts | 8 + .../src/lib/expressapp.ts | 36 +- .../bitcore-wallet-service/src/lib/server.ts | 6 +- 9 files changed, 945 insertions(+), 41 deletions(-) create mode 100644 packages/bitcore-wallet-client/src/types/serverAssistedImportEvents.d.ts diff --git a/packages/bitcore-wallet-client/src/index.ts b/packages/bitcore-wallet-client/src/index.ts index 780ed63710a..b6f88a54e76 100644 --- a/packages/bitcore-wallet-client/src/index.ts +++ b/packages/bitcore-wallet-client/src/index.ts @@ -20,6 +20,7 @@ export type * as EncryptionTypes from './lib/common/encryption'; export { Utils } from './lib/common/utils'; export type * as UtilsTypes from './lib/common/utils'; export { Errors } from './lib/errors'; +export type { ServerAssistedImportEvents } from './types/serverAssistedImportEvents'; export * as TssKey from './lib/tsskey'; export * as TssSign from './lib/tsssign'; \ No newline at end of file diff --git a/packages/bitcore-wallet-client/src/lib/api.ts b/packages/bitcore-wallet-client/src/lib/api.ts index fffe6a33ad6..fa49e6d0eae 100644 --- a/packages/bitcore-wallet-client/src/lib/api.ts +++ b/packages/bitcore-wallet-client/src/lib/api.ts @@ -18,6 +18,7 @@ import { PayPro } from './paypro'; import { PayProV2 } from './payproV2'; import { Request } from './request'; import { Verifier } from './verifier'; +import type { ServerAssistedImportEvents } from 'src/types/serverAssistedImportEvents'; const $ = singleton(); @@ -3365,7 +3366,6 @@ export class API extends EventEmitter { /** * Imports existing wallets against BWS and return key & clients[] for each account / coin - * @returns {key, clients[]} Returns key, clients[] */ static serverAssistedImport( opts: { @@ -3383,15 +3383,14 @@ export class API extends EventEmitter { use0forBCH?: boolean; }, /** BWS connection options (see ClientAPI constructor) */ - clientOpts, + clientOpts: API | any, /** Callback function in the standard form (err, key, clients) */ callback: (err?: Error, key?: Key, clients?: API[]) => void ) { - $.checkArgument(opts.words || opts.xPrivKey, 'Missing argument: words or xPrivKey at '); + const { words, xPrivKey, passphrase, includeTestnetWallets, includeLegacyWallets, use0forBCH } = opts || {}; + $.checkArgument(words || xPrivKey, 'Missing argument: words or xPrivKey at '); const client = clientOpts instanceof API ? API.clone(clientOpts) : new API(clientOpts); - const includeTestnetWallets = opts.includeTestnetWallets; - const includeLegacyWallets = opts.includeLegacyWallets; const credentials = []; const copayerIdAlreadyTested = {}; const keyCredentialIndex: { credentials: Credentials; key: Key; opts: any; status?: string }[] = []; @@ -3617,7 +3616,7 @@ export class API extends EventEmitter { account: clonedSettings.account, m: clonedSettings.m, n: clonedSettings.n, - use0forBCH: opts.use0forBCH // only used for server assisted import + use0forBCH: use0forBCH // only used for server assisted import }); accountKeyCredentialIndex.push({ @@ -3786,16 +3785,16 @@ export class API extends EventEmitter { const id = Uuid.v4(); for (const set of sets) { try { - if (opts.words) { - if (opts.passphrase) { - set.passphrase = opts.passphrase; + if (words) { + if (passphrase) { + set.passphrase = passphrase; } - k = new Key({ id, seedData: opts.words, seedType: 'mnemonic', ...set }); + k = new Key({ id, seedData: words, seedType: 'mnemonic', ...set }); } else { k = new Key({ id, - seedData: opts.xPrivKey, + seedData: xPrivKey, seedType: 'extendedPrivateKey', ...set }); @@ -3820,6 +3819,475 @@ export class API extends EventEmitter { ); } + /** + * Derives keys from words/xPrivKey and checks for existing wallets against BWS. + * If wallets exist, generates credentials and returns a single `key` and an array of `clients` as found wallets. + */ + static async serverAssistedImportV2( + opts: { + /** Mnemonic words */ + words?: string; + /** Extended Private Key */ + xPrivKey?: string; + /** Mnemonic's passphrase */ + passphrase?: string; + /** Include testnet wallets */ + includeTestnetWallets?: boolean; + /** Search legacy wallets */ + includeLegacyWallets?: boolean; + }, + /** BWS connection options (see ClientAPI constructor) */ + clientOpts: API | any, + /** + * Event emitter to consume status updates, errors, and the end result + * Events: + * - `keyConfig.count` => int - total number of key configurations to be checked (not all configurations will necessarily be processed, as the process may exit early if wallets are found) + * - `keyConfig.start` => int - index of the current key configuration being processed + * - `keyConfig.keyCreated` => void - status event saying a key was successfully created from the provided backup data + * - `chainPermutations.count` => int - total number of permutaions of [chain/coin, network, and derivation strategy] to be checked for each key configuration + * - `chainPermutations.getKey` => int - index of the current permutation being processed, and the key is created along the permuation to be sent to BWS for existence check + * - `findingCopayers` => int - number of copayers being sent to BWS to check for existence. Note, this is called inside a loop and may be called more than once. The loop goes as long as there are copayers to check for + * - `foundCopayers` => int - number of copayers found in BWS. Note, this is called inside a loop and may be called more than once. Each event is the number of copayers found for that loop iteration, not the total sum of copayers found. + * - `foundCopayers.count` => int - total number of copayers in BWS. This is the sum of all `foundCopayers` events and is emitted when the process of checking copayers against BWS is complete + * - `keyConfig.noCopayersFound` => void - emitted when no copayers are found for a given key configuration, and the process is moving on to the next key configuration (if any) + * - `creatingCredentials` => void - status event saying client credentials are being created for a found copayers + * - `gettingStatuses` => void - status event saying wallet statuses are being fetched from BWS for the found copayers + * - `gatheringWalletsInfos` => int - number of wallets being processed to gather wallet info + * - `walletInfo.gatheringTokens` => { chain: string, network: string } - status event saying token info is being gathered for a wallet of a chain:network + * - `walletInfo.gatheringTokens.error` => { chain: string, network: string, error: Error } - emitted with error info if gathering token info fails for a wallet of a chain:network + * - `walletInfo.importingToken` => { chain: string, network: string, tokenName: string, tokenAddress: string } - token wallet is being imported for a wallet of a chain:network + * - `walletInfo.gatheringMultisig` => { chain: string, network: string } - status event saying multisig info is being gathered for a wallet of a chain:network + * - `walletInfo.multisig.creatingCredentials` => { chain: string, network: string, walletName: string, multisigContractAddress: string, m: int, n: int } - status event saying multisig wallet credentials are being created for a wallet of a chain:network + * - `walletInfo.multisig.importingToken` => { chain: string, network: string, walletName: string, multisigContractAddress: string, tokenName: string, tokenAddress: string } - token wallet is being imported for a multisig wallet of a chain:network + * - `error` => Error - emitted with any terminating error that throws during the process + * - `done` => { key: Key, clients: API[] } - emitted with the final result object containing the key and clients when the process is complete + */ + events?: ServerAssistedImportEvents + ) { + $.checkArgument(!events || events instanceof EventEmitter, 'Invalid argument: events must be an EventEmitter instance'); + try { + const { words, xPrivKey, passphrase, includeTestnetWallets, includeLegacyWallets } = opts || {}; + events.on('findingCopayers', (num) => { + log.info(`Checking existence of ${num} copayers in BWS...`); + }); + $.checkArgument(words || xPrivKey, 'Missing argument: words or xPrivKey at '); + const client = clientOpts instanceof API ? API.clone(clientOpts) : new API(clientOpts); + const keyConfigs: Array<{ + chains?: Set; + nonCompliantDerivation: boolean; + useLegacyCoinType?: boolean; + useLegacyPurpose: boolean; + passphrase?: string; + }> = [ + { + // current wallets: /[44,48]/[0,145]'/ + nonCompliantDerivation: false, + useLegacyCoinType: false, + useLegacyPurpose: false, + passphrase: undefined // is set later + } + ]; + + if (includeLegacyWallets) { + const legacyConfigs = [ + { + // old bch wallets: /[44,48]/[0,0]'/ + nonCompliantDerivation: false, + useLegacyCoinType: true, + useLegacyPurpose: false + }, + { + // old BTC/BCH multisig wallets: /[44]/[0,145]'/ + nonCompliantDerivation: false, + useLegacyCoinType: false, + useLegacyPurpose: true + }, + { + // old multisig BCH wallets: /[44]/[0]'/ + nonCompliantDerivation: false, + useLegacyCoinType: true, + useLegacyPurpose: true + }, + { + // old BTC non-comp wallets: /44'/[0]'/ + nonCompliantDerivation: true, + useLegacyCoinType: false, + useLegacyPurpose: true + } + ]; + + keyConfigs.push(...legacyConfigs); + } + + events?.emit('keyConfig.count', keyConfigs.length); + + const allCopayerIds = new Set(); + const getKeyToCheck = (params: { + key: Key; + requestPrivKey: any; + perm: { coin: string; chain: string; network: string; multisig?: boolean; preForkBchCheck?: boolean }; + account: number; + }) => { + const { key, requestPrivKey, perm, account } = params; + const { chain, coin, network, multisig, preForkBchCheck } = perm; + const algo = key.getAlgorithm(chain); + const path = key.getBaseAddressDerivationPath({ + n: multisig ? 2 : 1, + chain, + coin, + network, + account, + use0forBCH: preForkBchCheck, + }); + const xPrivKey = key.derive(passphrase, path, algo, network); + const xPubKey = xPrivKey.xpubkey; + const copayerId = Utils.xPubToCopayerId(chain, xPubKey); + const signature = Utils.signMessage(copayerId, requestPrivKey); + return { + key, + requestPrivKey, + perm, + account, + copayerId, + path, + signature, + }; + }; + + + type KeyData = { + copayerId: string; + key: Key; + perms: Array<{ coin: string; chain: string; network: string; multisig?: boolean; preForkBchCheck?: boolean }>; + path: string; + account: number; + signature: string; + requestPrivKey: any; + }; + + let key: Key; + const clients: API[] = []; + + const id = Uuid.v4(); + for (const i in keyConfigs) { + events?.emit('keyConfig.start', Number(i)); + const keyConfig = keyConfigs[i]; + let k: Key; + try { + if (words) { + if (passphrase) { + keyConfig.passphrase = passphrase; + } + + k = new Key({ id, seedData: words, seedType: 'mnemonic', ...keyConfig }); + } else { + k = new Key({ + id, + seedData: xPrivKey, + seedType: 'extendedPrivateKey', + ...keyConfig + }); + } + } catch (e) { + log.info('Backup error:', e); + throw new Errors.INVALID_BACKUP(); + } + events?.emit('keyConfig.keyCreated'); + + let chainPermutations: { coin: string; chain: string; network: string; multisig?: boolean; preForkBchCheck?: boolean }[] = [ + { coin: 'btc', chain: 'btc', network: 'livenet' }, + { coin: 'bch', chain: 'bch', network: 'livenet' }, + { coin: 'bch', chain: 'bch', network: 'livenet', preForkBchCheck: true }, // check for prefork bch wallet + { coin: 'eth', chain: 'eth', network: 'livenet' }, + { coin: 'matic', chain: 'matic', network: 'livenet' }, + { coin: 'eth', chain: 'arb', network: 'livenet' }, + { coin: 'eth', chain: 'base', network: 'livenet' }, + { coin: 'eth', chain: 'op', network: 'livenet' }, + { coin: 'xrp', chain: 'xrp', network: 'livenet' }, + { coin: 'sol', chain: 'sol', network: 'livenet' }, + { coin: 'doge', chain: 'doge', network: 'livenet' }, + { coin: 'ltc', chain: 'ltc', network: 'livenet' }, + { coin: 'btc', chain: 'btc', network: 'livenet', multisig: true }, + { coin: 'bch', chain: 'bch', network: 'livenet', multisig: true }, + { coin: 'doge', chain: 'doge', network: 'livenet', multisig: true }, + { coin: 'ltc', chain: 'ltc', network: 'livenet', multisig: true } + ]; + + if (k.use44forMultisig) { + // testing old multisig + chainPermutations = chainPermutations.filter(x => x.multisig); + } + + if (k.use0forBCH) { + // testing BCH, old coin=0 wallets + chainPermutations = chainPermutations.filter(x => x.chain == 'bch'); + } + + if (!k.compliantDerivation) { + // only BTC, and no testnet + chainPermutations = chainPermutations.filter(x => x.chain == 'btc'); + } else if (includeTestnetWallets) { + const testnetPermutations = JSON.parse(JSON.stringify(chainPermutations)); + for (const perm of testnetPermutations) { + perm.network = 'testnet'; + } + // no need to check testnet for pre-fork BCH wallets + chainPermutations = chainPermutations.concat(testnetPermutations.filter(x => !x.preForkBchCheck)); + } + + events?.emit('chainPermutations.count', chainPermutations.length); + + // deterministic request key + const requestPrivKey = k.derive(passphrase, Constants.PATHS.REQUEST_KEY).privateKey; + const keysToCheck: { [copayerId: string]: KeyData } = {}; + + for (const j in chainPermutations) { + events?.emit('chainPermutations.getKey', Number(j)); + const perm = chainPermutations[j]; + const keyData = getKeyToCheck({ key: k, requestPrivKey, perm, account: 0 }); + // assert(!allCopayerIds.has(keyData.copayerId) || !!keysToCheck[keyData.copayerId].perm.multisig == !!keyData.perm.multisig, 'Duplicate copayerId generated, this should not happen'); + if (!keysToCheck[keyData.copayerId]) { + keysToCheck[keyData.copayerId] = { ...keyData, perms: [keyData.perm] }; + } else { + keysToCheck[keyData.copayerId].perms.push(keyData.perm); + } + allCopayerIds.add(keyData.copayerId); + } + + const existingKeyDatas: { [copayerId: string]: KeyData & { credentials?: Credentials[] } } = {}; + let existingCopayerIds: Set; + do { + events?.emit('findingCopayers', Object.keys(keysToCheck).length); + const { body } = await client.request.post('/v1/wallets/exist', { + copayers: Object.values(keysToCheck).map(x => ({ copayerId: x.copayerId, signature: x.signature })) + }); + + existingCopayerIds = new Set(body); + const checkedCopayerIds = Object.keys(keysToCheck); + for (const copayerId of checkedCopayerIds) { + if (existingCopayerIds.has(copayerId)) { + const foundKeyData = keysToCheck[copayerId]; + existingKeyDatas[copayerId] = foundKeyData; + for (const perm of foundKeyData.perms) { + const nextKeyData = getKeyToCheck({ ...foundKeyData, account: foundKeyData.account + 1, perm }); + $.checkState(!allCopayerIds.has(nextKeyData.copayerId), 'Duplicate copayerId generated, this should not happen'); + if (!keysToCheck[nextKeyData.copayerId]) { + keysToCheck[nextKeyData.copayerId] = { ...nextKeyData, perms: [nextKeyData.perm] }; + } else { + keysToCheck[nextKeyData.copayerId].perms.push(nextKeyData.perm); + } + } + } + delete keysToCheck[copayerId]; + } + events?.emit('foundCopayers', existingCopayerIds.size); + } while (existingCopayerIds.size > 0); + + events?.emit('foundCopayers.count', Object.keys(existingKeyDatas).length); + + + // if no copayers were found for this keyConfig, continue to the next one + if (Object.keys(existingKeyDatas).length === 0) { + events?.emit('keyConfig.noCopayersFound'); + continue; + } + + // Build clients for found copayers + events?.emit('creatingCredentials'); + key = Object.values(existingKeyDatas)[0]?.key; // all found keys should be the same, so just take the first one to generate clients + const credentials: Array = []; + for (const copayerId in existingKeyDatas) { + const keyData = existingKeyDatas[copayerId]; + for (const perm of keyData.perms) { + const creds = keyData.key.createCredentials(null, { + coin: perm.coin, + chain: perm.chain, + network: perm.network, + account: keyData.account, + m: 1, + n: perm.multisig ? 2 : 1, + use0forBCH: perm.preForkBchCheck, // only used for server assisted import + }); + keyData.credentials = keyData.credentials || []; + keyData.credentials.push(creds); + credentials.push(creds); + } + } + events?.emit('gettingStatuses'); + const walletsInfos = await client.bulkClient.getStatusAll( + credentials, + { + silentFailure: true, + twoStep: true, + includeExtendedInfo: true, + ignoreIncomplete: true + } + ); + + events?.emit('gatheringWalletsInfos', walletsInfos.length); + for (const walletInfo of walletsInfos) { + if (!walletInfo.success) { + continue; + } + const { status } = walletInfo; + let found = false; + for (const me of status.wallet.copayers) { + const keyData = existingKeyDatas[me.id]; + if (keyData) { + found = true; + const credIndex = keyData.credentials.findIndex(c => c.copayerId === me.id && (c.n > 1) == (status.wallet.n > 1)); + const credentials = keyData.credentials[credIndex]; + const perm = keyData.perms[credIndex]; + const { network } = perm; + const { wallet } = status; + const newClient = API.clone(client); + newClient.fromString(credentials); + newClient._processStatus(status); + if (!credentials.hasWalletInfo()) { + try { + credentials.addWalletInfo( + wallet.id, + wallet.name, + wallet.m, + wallet.n, + me.name, + { + allowOverwrite: !!wallet.tssKeyId + } + ); + } catch (e) { + if (e.message) { + log.info('Trying credentials...', e.message); + } + if (e.message && e.message.match(/Bad\snr/)) { + log.warn(new Errors.WALLET_DOES_NOT_EXIST()); + break; + } + } + } + if (wallet.status != 'complete') continue; + + if (status.customData?.walletPrivKey) { + credentials.addWalletPrivateKey(status.customData.walletPrivKey); + } + + if (credentials.walletPrivKey) { + if (!Verifier.checkCopayers(credentials, wallet.copayers)) { + log.warn(new Errors.SERVER_COMPROMISED()); + continue; + } + } else { + // this should only happen in AIR-GAPPED flows + log.warn('Could not verify copayers key (missing wallet Private Key)'); + } + + credentials.addPublicKeyRing( + newClient._extractPublicKeyRing(wallet.copayers) + ); + client.emit('walletCompleted', wallet); + + if (perm.coin == 'btc') { + if (['P2WPKH', 'P2WSH'].includes(wallet.addressType)) { + newClient.credentials.addressType = wallet.n == 1 ? Constants.SCRIPT_TYPES.P2WPKH : Constants.SCRIPT_TYPES.P2WSH; + } else if (wallet.addressType === 'P2TR') { + newClient.credentials.addressType = Constants.SCRIPT_TYPES.P2TR; + } + } + + clients.push(newClient); + + // Handle importing of tokens and multisig wallets for EVM chains + { + const chainConfigurations = [ + { chain: 'eth', tokenAddresses: status.preferences.tokenAddresses, multisigInfo: status.preferences.multisigEthInfo, tokenOpts: Constants.ETH_TOKEN_OPTS }, + { chain: 'matic', tokenAddresses: status.preferences.maticTokenAddresses, multisigInfo: status.preferences.multisigMaticInfo, tokenOpts: Constants.MATIC_TOKEN_OPTS }, + { chain: 'arb', tokenAddresses: status.preferences.arbTokenAddresses, multisigInfo: status.preferences.multisigArbInfo, tokenOpts: Constants.ARB_TOKEN_OPTS }, + { chain: 'op', tokenAddresses: status.preferences.opTokenAddresses, multisigInfo: status.preferences.multisigOpInfo, tokenOpts: Constants.OP_TOKEN_OPTS }, + { chain: 'base', tokenAddresses: status.preferences.baseTokenAddresses, multisigInfo: status.preferences.multisigBaseInfo, tokenOpts: Constants.BASE_TOKEN_OPTS }, + { chain: 'sol', tokenAddresses: status.preferences.solTokenAddresses, multisigInfo: status.preferences.multisigSolInfo, tokenOpts: Constants.SOL_TOKEN_OPTS }, + ]; + + for (const config of chainConfigurations) { + const { chain, tokenAddresses, multisigInfo, tokenOpts } = config; + if (chain !== perm.chain) continue; + events?.emit('walletInfo.gatheringTokens', { chain, network }); + // Handle importing of tokens + if (tokenAddresses?.length) { + const { body: customTokensData } = await newClient.request.get(`/v1/service/oneInch/getTokens/${chain}`).catch(err => { + log.warn(`getNetworkTokensData err for ${chain}:${network}`, err); + events?.emit('walletInfo.gatheringTokens.error', { chain, network, error: err }); + return { body: null }; + }); + + for (const t of tokenAddresses) { + const token = tokenOpts[t] || (customTokensData && customTokensData[t]); + if (!token) { + log.warn(`Token ${t} unknown on ${chain}`); + continue; + } + log.info(`Importing token: ${token.name} on ${chain}`); + events?.emit('walletInfo.importingToken', { chain, network, tokenName: token.name, tokenAddress: t }); + const tokenCredentials = newClient.credentials.getTokenCredentials(token, chain); + const tokenClient = newClient.toClone(); + tokenClient.credentials = tokenCredentials; + clients.push(tokenClient); + } + } + + events?.emit('walletInfo.gatheringMultisig', { chain, network }); + + // Handle importing of multisig wallets (I don't believe this contract-based multisig feature was ever used, so this might be dead code) + for (const info of (multisigInfo || [])) { + log.info(`Importing multisig wallet on ${chain}. Address: ${info.multisigContractAddress} - m: ${info.m} - n: ${info.n}`); + events?.emit('walletInfo.multisig.creatingCredentials', { ...info, chain, network }); + const multisigCredentials = newClient.credentials.getMultisigEthCredentials({ + walletName: info.walletName, + multisigContractAddress: info.multisigContractAddress, + n: info.n, + m: info.m + }); + const multisigClient = newClient.toClone(); + multisigClient.credentials = multisigCredentials; + clients.push(multisigClient); + + const multisigTokenAddresses = info.tokenAddresses || []; + for (const t of multisigTokenAddresses) { + const token = tokenOpts[t]; + if (!token) { + log.warn(`Token ${t} unknown in multisig on ${chain}`); + continue; + } + log.info(`Importing multisig token: ${token.name} on ${chain}`); + events?.emit('walletInfo.multisig.importingToken', { ...info, chain, network, tokenName: token.name, tokenAddress: t }); + const tokenCredentials = multisigClient.credentials.getTokenCredentials(token, chain); + const tokenClient = multisigClient.toClone(); + tokenClient.credentials = tokenCredentials; + clients.push(tokenClient); + } + } + } + } + } + } + if (!found) { + log.warn(new Error('Copayer not in wallet')); + } + } + // If we've gotten this far, return the results which short-circuits the rest of the keyConfigs. + events?.emit('done', { key, clients }); + return { key, clients }; + } + + throw new Errors.WALLET_DOES_NOT_EXIST(); + } catch (err) { + if (events) { + events.emit('error', err); + } else { + throw err; + } + } + } + async banxaGetQuote(data) { return this.request.post('/v1/service/banxa/quote', data); } @@ -3978,6 +4446,15 @@ export interface CreateWalletOpts { tssKeyId?: string; }; + +interface MultisigInfo { + walletName: string; + multisigContractAddress: string; + n: number; + m: number; + tokenAddresses?: string[]; +}; + export interface Status { balance: { availableAmount: number; @@ -3996,7 +4473,20 @@ export interface Status { walletPrivKey?: string; // used for multisig join secret }; pendingTxps: Array; // TOOD - preferences: object; // TODO + preferences: { + tokenAddresses?: string[]; // for Ethereum + maticTokenAddresses?: string[]; // for Polygon + arbTokenAddresses?: string[]; // for Arbitrum + opTokenAddresses?: string[]; // for Optimism + baseTokenAddresses?: string[]; // for Base + solTokenAddresses?: string[]; // for Solana + multisigEthInfo?: Array; + multisigMaticInfo?: Array; + multisigArbInfo?: Array; + multisigOpInfo?: Array; + multisigBaseInfo?: Array; + multisigSolInfo?: Array; + }; wallet: { addressType: string; beAuthPrivateKey2?: string; @@ -4153,3 +4643,111 @@ export interface Address { path: string; isChange?: boolean; }; + +// export class ServerAssistedImportEvents extends EventEmitter { +// on(event: 'keyConfig.count', listener: (count: number) => void): this; +// on(event: 'keyConfig.start', listener: (index: number) => void): this; +// on(event: 'keyConfig.keyCreated', listener: () => void): this; +// on(event: 'chainPermutations.count', listener: (count: number) => void): this; +// on(event: 'chainPermutations.getKey', listener: (index: number) => void): this; +// on(event: 'findingCopayers', listener: (num: number) => void): this; +// on(event: 'foundCopayers', listener: (num: number) => void): this; +// on(event: 'foundCopayers.count', listener: (count: number) => void): this; +// on(event: 'keyConfig.noCopayersFound', listener: () => void): this; +// on(event: 'creatingCredentials', listener: () => void): this; +// on(event: 'gettingStatuses', listener: () => void): this; +// on(event: 'gatheringWalletsInfos', listener: (num: number) => void): this; +// on(event: 'walletInfo.gatheringTokens', listener: (data: { chain: string; network: string }) => void): this; +// on(event: 'walletInfo.gatheringTokens.error', listener: (data: { chain: string; network: string; error: Error }) => void): this; +// on(event: 'walletInfo.importingToken', listener: (data: { chain: string; network: string; tokenName: string; tokenAddress: string }) => void): this; +// on(event: 'walletInfo.gatheringMultisig', listener: (data: { chain: string; network: string }) => void): this; +// on(event: 'walletInfo.multisig.creatingCredentials', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; m: number; n: number }) => void): this; +// on(event: 'walletInfo.multisig.importingToken', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; tokenName: string; tokenAddress: string }) => void): this; +// on(event: 'error', listener: (error: Error) => void): this; +// on(event: 'done', listener: (data: { key: Key; clients: API[] }) => void): this; + +// once(...args: [event: 'keyConfig.count', listener: (count: number) => void] | [event: 'keyConfig.start', listener: (index: number) => void] | [event: 'keyConfig.keyCreated', listener: () => void] | [event: 'chainPermutations.count', listener: (count: number) => void] | [event: 'chainPermutations.getKey', listener: (index: number) => void] | [event: 'findingCopayers', listener: (num: number) => void] | [event: 'foundCopayers', listener: (num: number) => void] | [event: 'foundCopayers.count', listener: (count: number) => void] | [event: 'keyConfig.noCopayersFound', listener: () => void] | [event: 'creatingCredentials', listener: () => void] | [event: 'gettingStatuses', listener: () => void] | [event: 'gatheringWalletsInfos', listener: (num: number) => void] | [event: 'walletInfo.gatheringTokens', listener: (data: { chain: string; network: string; }) => void] | [event: 'walletInfo.gatheringTokens.error', listener: (data: { chain: string; network: string; error: Error; }) => void] | [event: 'walletInfo.importingToken', listener: (data: { chain: string; network: string; tokenName: string; tokenAddress: string; }) => void] | [event: 'walletInfo.gatheringMultisig', listener: (data: { chain: string; network: string; }) => void] | [event: 'walletInfo.multisig.creatingCredentials', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; m: number; n: number; }) => void] | [event: 'walletInfo.multisig.importingToken', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; tokenName: string; tokenAddress: string; }) => void] | [event: 'error', listener: (error: Error) => void] | [event: 'done', listener: (data: { key: Key; clients: API[]; }) => void]): this; + +// addListener(event: 'keyConfig.count', listener: (count: number) => void): this; +// addListener(event: 'keyConfig.start', listener: (index: number) => void): this; +// addListener(event: 'keyConfig.keyCreated', listener: () => void): this; +// addListener(event: 'chainPermutations.count', listener: (count: number) => void): this; +// addListener(event: 'chainPermutations.getKey', listener: (index: number) => void): this; +// addListener(event: 'findingCopayers', listener: (num: number) => void): this; +// addListener(event: 'foundCopayers', listener: (num: number) => void): this; +// addListener(event: 'foundCopayers.count', listener: (count: number) => void): this; +// addListener(event: 'keyConfig.noCopayersFound', listener: () => void): this; +// addListener(event: 'creatingCredentials', listener: () => void): this; +// addListener(event: 'gettingStatuses', listener: () => void): this; +// addListener(event: 'gatheringWalletsInfos', listener: (num: number) => void): this; +// addListener(event: 'walletInfo.gatheringTokens', listener: (data: { chain: string; network: string }) => void): this; +// addListener(event: 'walletInfo.gatheringTokens.error', listener: (data: { chain: string; network: string; error: Error }) => void): this; +// addListener(event: 'walletInfo.importingToken', listener: (data: { chain: string; network: string; tokenName: string; tokenAddress: string }) => void): this; +// addListener(event: 'walletInfo.gatheringMultisig', listener: (data: { chain: string; network: string }) => void): this; +// addListener(event: 'walletInfo.multisig.creatingCredentials', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; m: number; n: number }) => void): this; +// addListener(event: 'walletInfo.multisig.importingToken', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; tokenName: string; tokenAddress: string }) => void): this; +// addListener(event: 'error', listener: (error: Error) => void): this; +// addListener(event: 'done', listener: (data: { key: Key; clients: API[] }) => void): this; + +// removeListener(event: 'keyConfig.count', listener: (count: number) => void): this; +// removeListener(event: 'keyConfig.start', listener: (index: number) => void): this; +// removeListener(event: 'keyConfig.keyCreated', listener: () => void): this; +// removeListener(event: 'chainPermutations.count', listener: (count: number) => void): this; +// removeListener(event: 'chainPermutations.getKey', listener: (index: number) => void): this; +// removeListener(event: 'findingCopayers', listener: (num: number) => void): this; +// removeListener(event: 'foundCopayers', listener: (num: number) => void): this; +// removeListener(event: 'foundCopayers.count', listener: (count: number) => void): this; +// removeListener(event: 'keyConfig.noCopayersFound', listener: () => void): this; +// removeListener(event: 'creatingCredentials', listener: () => void): this; +// removeListener(event: 'gettingStatuses', listener: () => void): this; +// removeListener(event: 'gatheringWalletsInfos', listener: (num: number) => void): this; +// removeListener(event: 'walletInfo.gatheringTokens', listener: (data: { chain: string; network: string }) => void): this; +// removeListener(event: 'walletInfo.gatheringTokens.error', listener: (data: { chain: string; network: string; error: Error }) => void): this; +// removeListener(event: 'walletInfo.importingToken', listener: (data: { chain: string; network: string; tokenName: string; tokenAddress: string }) => void): this; +// removeListener(event: 'walletInfo.gatheringMultisig', listener: (data: { chain: string; network: string }) => void): this; +// removeListener(event: 'walletInfo.multisig.creatingCredentials', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; m: number; n: number }) => void): this; +// removeListener(event: 'walletInfo.multisig.importingToken', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; tokenName: string; tokenAddress: string }) => void): this; +// removeListener(event: 'error', listener: (error: Error) => void): this; +// removeListener(event: 'done', listener: (data: { key: Key; clients: API[] }) => void): this; +// } +// /** Total number of key configurations to be checked (not all configurations will necessarily be processed, as the process may exit early if wallets are found) */ +// 'keyConfig.count': [number]; +// /** Index of the current key configuration being processed */ +// 'keyConfig.start': [number]; +// /** A key was successfully created from the provided backup data */ +// 'keyConfig.keyCreated': []; +// /** Total number of permutations of [chain/coin, network, and derivation strategy] to be checked for each key configuration */ +// 'chainPermutations.count': [number]; +// /** Index of the current permutation being processed; the key is derived along the permutation to be sent to BWS for existence check */ +// 'chainPermutations.getKey': [number]; +// /** Number of copayers being sent to BWS to check for existence. Called inside a loop and may fire more than once */ +// 'findingCopayers': [number]; +// /** Number of copayers found in BWS for a single loop iteration (not the running total) */ +// 'foundCopayers': [number]; +// /** Total number of copayers found in BWS — sum of all `foundCopayers` events, emitted when the copayer-check loop is complete */ +// 'foundCopayers.count': [number]; +// /** No copayers were found for the current key configuration; moving on to the next one (if any) */ +// 'keyConfig.noCopayersFound': []; +// /** Client credentials are being created for the found copayers */ +// 'creatingCredentials': []; +// /** Wallet statuses are being fetched from BWS for the found copayers */ +// 'gettingStatuses': []; +// /** Number of wallets being processed to gather wallet info */ +// 'gatheringWalletsInfos': [number]; +// /** Token info is being gathered for a wallet of the given chain/network */ +// 'walletInfo.gatheringTokens': [{ chain: string; network: string }]; +// /** Gathering token info failed for a wallet of the given chain/network */ +// 'walletInfo.gatheringTokens.error': [{ chain: string; network: string; error: Error }]; +// /** A token wallet is being imported for a wallet of the given chain/network */ +// 'walletInfo.importingToken': [{ chain: string; network: string; tokenName: string; tokenAddress: string }]; +// /** Multisig info is being gathered for a wallet of the given chain/network */ +// 'walletInfo.gatheringMultisig': [{ chain: string; network: string }]; +// /** Multisig wallet credentials are being created for a wallet of the given chain/network */ +// 'walletInfo.multisig.creatingCredentials': [{ chain: string; network: string; walletName: string; multisigContractAddress: string; m: number; n: number }]; +// /** A token wallet is being imported for a multisig wallet of the given chain/network */ +// 'walletInfo.multisig.importingToken': [{ chain: string; network: string; walletName: string; multisigContractAddress: string; tokenName: string; tokenAddress: string }]; +// /** Terminating error that was thrown during the process */ +// 'error': [Error]; +// /** Final result containing the recovered key and all imported wallet clients */ +// 'done': [{ key: Key; clients: API[] }]; +// }>; diff --git a/packages/bitcore-wallet-client/src/lib/common/utils.ts b/packages/bitcore-wallet-client/src/lib/common/utils.ts index b63b5890f32..25482ba797f 100644 --- a/packages/bitcore-wallet-client/src/lib/common/utils.ts +++ b/packages/bitcore-wallet-client/src/lib/common/utils.ts @@ -275,7 +275,7 @@ export class Utils { // no matter WHICH chain static xPubToCopayerId(chain: string, xpub: string): string { // this was introduced because we allowed coinType = 0' wallets for BCH - // for the "wallet duplication" feature + // for the "wallet duplication" feature // now it is effective for all coins. chain = chain.toLowerCase(); diff --git a/packages/bitcore-wallet-client/src/lib/key.ts b/packages/bitcore-wallet-client/src/lib/key.ts index 00f9d6f3b78..3fef779dc81 100644 --- a/packages/bitcore-wallet-client/src/lib/key.ts +++ b/packages/bitcore-wallet-client/src/lib/key.ts @@ -226,11 +226,6 @@ export class Key { private setFromMnemonic(m, opts: SetFromMnemonicOptions) { const algos = opts.algo ? [opts.algo] : SUPPORTED_ALGOS; for (const algo of algos) { - // private setFromMnemonic( - // m, - // opts: { passphrase?: string; password?: PasswordMaybe; encryptionOpts?: KeyOptions['encryptionOpts'], algo?: KeyAlgorithm } - // ) { - // for (const algo of SUPPORTED_ALGOS) { const xpriv = m.toHDPrivateKey(opts.passphrase, NETWORK, ALGO_TO_KEY_TYPE[algo]); this.#setFingerprint({ value: xpriv.fingerPrint.toString('hex'), algo }); @@ -475,11 +470,20 @@ export class Key { } }; - derive(password: PasswordMaybe, path: string, algo?: KeyAlgorithm): Bitcore.HDPrivateKey { + /** + * Derive a child key + * @param {string} password + * @param {string} path + * @param {KeyAlgorithm} [algo] "ECDSA" (default) or "EDDSA" + * @param {string} [network] Used to converts a livenet xPrivkey to testnet and vice versa + * @returns + */ + derive(password: PasswordMaybe, path: string, algo?: KeyAlgorithm, network?: string): Bitcore.HDPrivateKey { $.checkArgument(path, 'no path at derive()'); + let xPrivKey; if (algo?.toUpperCase?.() === Constants.ALGOS.EDDSA) { const key = this.#getChildKeyEDDSA(password, path); - return new Bitcore.HDPrivateKey({ + xPrivKey = new Bitcore.HDPrivateKey({ network: NETWORK, depth: 1, parentFingerPrint: Buffer.from(this.#getFingerprint({ algo }), 'hex'), @@ -488,17 +492,31 @@ export class Key { privateKey: Bitcore.encoding.Base58.decode(key.privKey), }); } else { - const xPrivKey = new Bitcore.HDPrivateKey( + xPrivKey = new Bitcore.HDPrivateKey( this.get(password, algo).xPrivKey, NETWORK ); const deriveFn = this.compliantDerivation ? xPrivKey.deriveChild.bind(xPrivKey) : xPrivKey.deriveNonCompliantChild.bind(xPrivKey); - return deriveFn(path); + xPrivKey = deriveFn(path); } + if (network && network !== NETWORK) { + xPrivKey = this.convertXPrivKeyToNetwork(xPrivKey, network); + } + return xPrivKey; }; + convertXPrivKeyToNetwork(xPrivKey: Bitcore.HDPrivateKey, network: string): Bitcore.HDPrivateKey { + if (!['testnet', 'regtest'].includes(network)) return xPrivKey; + const x = xPrivKey.toObject(); + x.network = network; + delete x.xprivkey; + delete x.checksum; + x.privateKey = x.privateKey.padStart(64, '0'); + return new Bitcore.HDPrivateKey(x); + } + _checkChain(chain: string) { if (!Constants.CHAINS.includes(chain)) throw new Error('Invalid chain'); @@ -532,7 +550,7 @@ export class Key { // checking in chains for simplicity if ( - ['testnet', 'regtest]'].includes(opts.network) && + ['testnet', 'regtest'].includes(opts.network) && Constants.UTXO_CHAINS.includes(chain) ) { coinCode = '1'; @@ -587,11 +605,13 @@ export class Key { algo?: KeyAlgorithm; tssXPubKey?: string; copayerName?: string; + /** Only used when trying to find keys from a seed phrase import */ + use0forBCH?: boolean; } ) { opts = opts || {} as any; opts.chain = (opts.chain || Utils.getChain(opts.coin)).toLowerCase(); - const algo = opts.algo || ALGOS_BY_CHAIN[opts.chain.toLowerCase()] || ALGOS_BY_CHAIN.default; + const algo = opts.algo || this.getAlgorithm(opts.chain); if (password) $.shouldBeString(password, 'provide password'); @@ -605,21 +625,15 @@ export class Key { $.shouldBeUndefined(opts['useLegacyPurpose'], 'useLegacyPurpose is deprecated'); const path = this.getBaseAddressDerivationPath(opts); - let xPrivKey = this.derive(password, path, algo); + let xPrivKey = this.derive(password, path, algo, opts.network); const requestPrivKey = this.derive( password, Constants.PATHS.REQUEST_KEY, ).privateKey.toString(); if (['testnet', 'regtest'].includes(opts.network)) { - // Hacky: BTC/BCH xPriv depends on network: This code is to // convert a livenet xPriv to a testnet/regtest xPriv - const x = xPrivKey.toObject(); - x.network = opts.network; - delete x.xprivkey; - delete x.checksum; - x.privateKey = x.privateKey.padStart(64, '0'); - xPrivKey = new Bitcore.HDPrivateKey(x); + xPrivKey = this.convertXPrivKeyToNetwork(xPrivKey, opts.network); } return Credentials.fromDerivedKey({ @@ -761,6 +775,10 @@ export class Key { } }; + getAlgorithm(chain: string) { + return ALGOS_BY_CHAIN[chain.toLowerCase()] || ALGOS_BY_CHAIN.default; + } + #setPrivKey(params: { value: any; algo?: KeyAlgorithm }) { const { value, algo } = params; switch (algo?.toUpperCase?.()) { diff --git a/packages/bitcore-wallet-client/src/types/serverAssistedImportEvents.d.ts b/packages/bitcore-wallet-client/src/types/serverAssistedImportEvents.d.ts new file mode 100644 index 00000000000..c2bbf9a7b07 --- /dev/null +++ b/packages/bitcore-wallet-client/src/types/serverAssistedImportEvents.d.ts @@ -0,0 +1,150 @@ +import type { API } from '../lib/api'; +import type { Key } from '../lib/key'; +import type { EventEmitter } from 'events'; + + +export class ServerAssistedImportEvents extends EventEmitter { + /** Total number of key configurations to be checked (not all configurations will necessarily be processed, as the process may exit early if wallets are found) */ + on(event: 'keyConfig.count', listener: (count: number) => void): this; + /** Index of the current key configuration being processed */ + on(event: 'keyConfig.start', listener: (index: number) => void): this; + /** A key was successfully created from the provided backup data */ + on(event: 'keyConfig.keyCreated', listener: () => void): this; + /** Total number of permutations of [chain/coin, network, and derivation strategy] to be checked for each key configuration */ + on(event: 'chainPermutations.count', listener: (count: number) => void): this; + /** Index of the current permutation being processed; the key is derived along the permutation to be sent to BWS for existence check */ + on(event: 'chainPermutations.getKey', listener: (index: number) => void): this; + /** Number of copayers being sent to BWS to check for existence. Called inside a loop and may fire more than once */ + on(event: 'findingCopayers', listener: (num: number) => void): this; + /** Number of copayers found in BWS for a single loop iteration (not the running total) */ + on(event: 'foundCopayers', listener: (num: number) => void): this; + /** Total number of copayers found in BWS — sum of all `foundCopayers` events, emitted when the copayer-check loop is complete */ + on(event: 'foundCopayers.count', listener: (count: number) => void): this; + /** No copayers were found for the current key configuration; moving on to the next one (if any) */ + on(event: 'keyConfig.noCopayersFound', listener: () => void): this; + /** Client credentials are being created for the found copayers */ + on(event: 'creatingCredentials', listener: () => void): this; + /** Wallet statuses are being fetched from BWS for the found copayers */ + on(event: 'gettingStatuses', listener: () => void): this; + /** Number of wallets being processed to gather wallet info */ + on(event: 'gatheringWalletsInfos', listener: (num: number) => void): this; + /** Token info is being gathered for a wallet of the given chain/network */ + on(event: 'walletInfo.gatheringTokens', listener: (data: { chain: string; network: string }) => void): this; + /** Gathering token info failed for a wallet of the given chain/network */ + on(event: 'walletInfo.gatheringTokens.error', listener: (data: { chain: string; network: string; error: Error }) => void): this; + /** A token wallet is being imported for a wallet of the given chain/network */ + on(event: 'walletInfo.importingToken', listener: (data: { chain: string; network: string; tokenName: string; tokenAddress: string }) => void): this; + /** Multisig info is being gathered for a wallet of the given chain/network */ + on(event: 'walletInfo.gatheringMultisig', listener: (data: { chain: string; network: string }) => void): this; + /** Multisig wallet credentials are being created for a wallet of the given chain/network */ + on(event: 'walletInfo.multisig.creatingCredentials', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; m: number; n: number }) => void): this; + /** A token wallet is being imported for a multisig wallet of the given chain/network */ + on(event: 'walletInfo.multisig.importingToken', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; tokenName: string; tokenAddress: string }) => void): this; + /** Terminating error that was thrown during the process */ + on(event: 'error', listener: (error: Error) => void): this; + /** Final result containing the recovered key and all imported wallet clients */ + on(event: 'done', listener: (data: { key: Key; clients: API[] }) => void): this; + + /** Total number of key configurations to be checked (not all configurations will necessarily be processed, as the process may exit early if wallets are found) */ + once(event: 'keyConfig.count', listener: (count: number) => void): this; + /** Index of the current key configuration being processed */ + once(event: 'keyConfig.start', listener: (index: number) => void): this; + /** A key was successfully created from the provided backup data */ + once(event: 'keyConfig.keyCreated', listener: () => void): this; + /** Total number of permutations of [chain/coin, network, and derivation strategy] to be checked for each key configuration */ + once(event: 'chainPermutations.count', listener: (count: number) => void): this; + /** Index of the current permutation being processed; the key is derived along the permutation to be sent to BWS for existence check */ + once(event: 'chainPermutations.getKey', listener: (index: number) => void): this; + /** Number of copayers being sent to BWS to check for existence. Called inside a loop and may fire more than once */ + once(event: 'findingCopayers', listener: (num: number) => void): this; + /** Number of copayers found in BWS for a single loop iteration (not the running total) */ + once(event: 'foundCopayers', listener: (num: number) => void): this; + /** Total number of copayers found in BWS — sum of all `foundCopayers` events, emitted when the copayer-check loop is complete */ + once(event: 'foundCopayers.count', listener: (count: number) => void): this; + /** No copayers were found for the current key configuration; moving on to the next one (if any) */ + once(event: 'keyConfig.noCopayersFound', listener: () => void): this; + /** Client credentials are being created for the found copayers */ + once(event: 'creatingCredentials', listener: () => void): this; + /** Wallet statuses are being fetched from BWS for the found copayers */ + once(event: 'gettingStatuses', listener: () => void): this; + /** Number of wallets being processed to gather wallet info */ + once(event: 'gatheringWalletsInfos', listener: (num: number) => void): this; + /** Token info is being gathered for a wallet of the given chain/network */ + once(event: 'walletInfo.gatheringTokens', listener: (data: { chain: string; network: string }) => void): this; + /** Gathering token info failed for a wallet of the given chain/network */ + once(event: 'walletInfo.gatheringTokens.error', listener: (data: { chain: string; network: string; error: Error }) => void): this; + /** A token wallet is being imported for a wallet of the given chain/network */ + once(event: 'walletInfo.importingToken', listener: (data: { chain: string; network: string; tokenName: string; tokenAddress: string }) => void): this; + /** Multisig info is being gathered for a wallet of the given chain/network */ + once(event: 'walletInfo.gatheringMultisig', listener: (data: { chain: string; network: string }) => void): this; + /** Multisig wallet credentials are being created for a wallet of the given chain/network */ + once(event: 'walletInfo.multisig.creatingCredentials', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; m: number; n: number }) => void): this; + /** A token wallet is being imported for a multisig wallet of the given chain/network */ + once(event: 'walletInfo.multisig.importingToken', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; tokenName: string; tokenAddress: string }) => void): this; + /** Terminating error that was thrown during the process */ + once(event: 'error', listener: (error: Error) => void): this; + /** Final result containing the recovered key and all imported wallet clients */ + once(event: 'done', listener: (data: { key: Key; clients: API[] }) => void): this; + + /** Total number of key configurations to be checked (not all configurations will necessarily be processed, as the process may exit early if wallets are found) */ + addListener(event: 'keyConfig.count', listener: (count: number) => void): this; + /** Index of the current key configuration being processed */ + addListener(event: 'keyConfig.start', listener: (index: number) => void): this; + /** A key was successfully created from the provided backup data */ + addListener(event: 'keyConfig.keyCreated', listener: () => void): this; + /** Total number of permutations of [chain/coin, network, and derivation strategy] to be checked for each key configuration */ + addListener(event: 'chainPermutations.count', listener: (count: number) => void): this; + /** Index of the current permutation being processed; the key is derived along the permutation to be sent to BWS for existence check */ + addListener(event: 'chainPermutations.getKey', listener: (index: number) => void): this; + /** Number of copayers being sent to BWS to check for existence. Called inside a loop and may fire more than once */ + addListener(event: 'findingCopayers', listener: (num: number) => void): this; + /** Number of copayers found in BWS for a single loop iteration (not the running total) */ + addListener(event: 'foundCopayers', listener: (num: number) => void): this; + /** Total number of copayers found in BWS — sum of all `foundCopayers` events, emitted when the copayer-check loop is complete */ + addListener(event: 'foundCopayers.count', listener: (count: number) => void): this; + /** No copayers were found for the current key configuration; moving on to the next one (if any) */ + addListener(event: 'keyConfig.noCopayersFound', listener: () => void): this; + /** Client credentials are being created for the found copayers */ + addListener(event: 'creatingCredentials', listener: () => void): this; + /** Wallet statuses are being fetched from BWS for the found copayers */ + addListener(event: 'gettingStatuses', listener: () => void): this; + /** Number of wallets being processed to gather wallet info */ + addListener(event: 'gatheringWalletsInfos', listener: (num: number) => void): this; + /** Token info is being gathered for a wallet of the given chain/network */ + addListener(event: 'walletInfo.gatheringTokens', listener: (data: { chain: string; network: string }) => void): this; + /** Gathering token info failed for a wallet of the given chain/network */ + addListener(event: 'walletInfo.gatheringTokens.error', listener: (data: { chain: string; network: string; error: Error }) => void): this; + /** A token wallet is being imported for a wallet of the given chain/network */ + addListener(event: 'walletInfo.importingToken', listener: (data: { chain: string; network: string; tokenName: string; tokenAddress: string }) => void): this; + /** Multisig info is being gathered for a wallet of the given chain/network */ + addListener(event: 'walletInfo.gatheringMultisig', listener: (data: { chain: string; network: string }) => void): this; + /** Multisig wallet credentials are being created for a wallet of the given chain/network */ + addListener(event: 'walletInfo.multisig.creatingCredentials', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; m: number; n: number }) => void): this; + /** A token wallet is being imported for a multisig wallet of the given chain/network */ + addListener(event: 'walletInfo.multisig.importingToken', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; tokenName: string; tokenAddress: string }) => void): this; + /** Terminating error that was thrown during the process */ + addListener(event: 'error', listener: (error: Error) => void): this; + /** Final result containing the recovered key and all imported wallet clients */ + addListener(event: 'done', listener: (data: { key: Key; clients: API[] }) => void): this; + + removeListener(event: 'keyConfig.count', listener: (count: number) => void): this; + removeListener(event: 'keyConfig.start', listener: (index: number) => void): this; + removeListener(event: 'keyConfig.keyCreated', listener: () => void): this; + removeListener(event: 'chainPermutations.count', listener: (count: number) => void): this; + removeListener(event: 'chainPermutations.getKey', listener: (index: number) => void): this; + removeListener(event: 'findingCopayers', listener: (num: number) => void): this; + removeListener(event: 'foundCopayers', listener: (num: number) => void): this; + removeListener(event: 'foundCopayers.count', listener: (count: number) => void): this; + removeListener(event: 'keyConfig.noCopayersFound', listener: () => void): this; + removeListener(event: 'creatingCredentials', listener: () => void): this; + removeListener(event: 'gettingStatuses', listener: () => void): this; + removeListener(event: 'gatheringWalletsInfos', listener: (num: number) => void): this; + removeListener(event: 'walletInfo.gatheringTokens', listener: (data: { chain: string; network: string }) => void): this; + removeListener(event: 'walletInfo.gatheringTokens.error', listener: (data: { chain: string; network: string; error: Error }) => void): this; + removeListener(event: 'walletInfo.importingToken', listener: (data: { chain: string; network: string; tokenName: string; tokenAddress: string }) => void): this; + removeListener(event: 'walletInfo.gatheringMultisig', listener: (data: { chain: string; network: string }) => void): this; + removeListener(event: 'walletInfo.multisig.creatingCredentials', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; m: number; n: number }) => void): this; + removeListener(event: 'walletInfo.multisig.importingToken', listener: (data: { chain: string; network: string; walletName: string; multisigContractAddress: string; tokenName: string; tokenAddress: string }) => void): this; + removeListener(event: 'error', listener: (error: Error) => void): this; + removeListener(event: 'done', listener: (data: { key: Key; clients: API[] }) => void): this; +} diff --git a/packages/bitcore-wallet-client/test/api.test.ts b/packages/bitcore-wallet-client/test/api.test.ts index 13286598f07..e43b77bc82d 100644 --- a/packages/bitcore-wallet-client/test/api.test.ts +++ b/packages/bitcore-wallet-client/test/api.test.ts @@ -6551,6 +6551,111 @@ describe('client API', function() { }); }); + it('should be able to gain access to multiple 1-1 wallets from mnemonic using V2 import function', async function() { + let walletCnt = 0; + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'btc', chain: 'btc' }); + walletCnt++; + const key = keys[0]; + const words = key.get(null, true).mnemonic; + const walletName = clients[0].credentials.walletName; + const copayerName = clients[0].credentials.copayerName; + const addrs = new Set(); + let addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'bch', chain: 'bch', key }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'doge', chain: 'doge', key }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'ltc', chain: 'ltc', key }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'eth', chain: 'eth', key }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'eth', chain: 'arb', key }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'eth', chain: 'op', key }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'eth', chain: 'base', key }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + { + const account = 1; + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'btc', chain: 'btc', key, account }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'eth', chain: 'eth', key, account }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'eth', chain: 'arb', key, account }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'eth', chain: 'op', key, account }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'eth', chain: 'base', key, account }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + } + { + const account = 2; + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'btc', chain: 'btc', key, account }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + } + { + const account = 3; + await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'btc', chain: 'btc', key, account }); + walletCnt++; + addr = await clients[0].createAddress(); + should.exist(addr); + addrs.add(addr.address); + } + const retVal = await Client.serverAssistedImportV2({ words, includeTestnetWallets: true, includeLegacyWallets: true }, helpers.newClient(app)); + const { key: k, clients: c } = retVal; + c.length.should.equal(walletCnt); + const recoveryClient = c[0]; + await recoveryClient.openWallet(); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + const list = await recoveryClient.getMainAddresses({}); + should.exist(list); + list.length.should.equal(1); + addrs.has(list[0].address).should.be.true; + }); + + it('should be able to gain access to eth tokens wallets from mnemonic', function(done) { helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'eth' }, () => { const words = keys[0].get(null, true).mnemonic; diff --git a/packages/bitcore-wallet-client/test/key.test.ts b/packages/bitcore-wallet-client/test/key.test.ts index 3c84236d245..6c2cf735c2f 100644 --- a/packages/bitcore-wallet-client/test/key.test.ts +++ b/packages/bitcore-wallet-client/test/key.test.ts @@ -358,6 +358,14 @@ describe('Key', function() { const xpk = k.derive(null, "m/44'/1'/0'").toString(); xpk.should.equal('tprv8gSy16H5hQ1MKNHzZDzsktr4aaGQSHg4XYVEbfsEiGSBcgw4J8dEm8uf19FH4L9h6W47VBKtc3bbYyjb6HAm6QdyRLpB6fsA7bW19RZnby2'); }); + it('should derive child to a different network', function() { + const k = new Key({ seedType: 'mnemonic', seedData: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' }); + const xpk1 = k.derive(null, "m/44'/0'/0'", null); + xpk1.toString().should.equal('xprv9xpXFhFpqdQK3TmytPBqXtGSwS3DLjojFhTGht8gwAAii8py5X6pxeBnQ6ehJiyJ6nDjWGJfZ95WxByFXVkDxHXrqu53WCRGypk2ttuqncb'); + const xpk2 = k.derive(null, "m/44'/0'/0'", null, 'testnet'); + xpk2.toString().should.equal('tprv8fVU32aAEuEPeH1WYx3LhXtSFZTRaFqjbFNPaJZ9R8fCVja44tSaUPZEKGpMK6McUDkWWMvRiVfKR3Wzei6AmLoTNYHMAZ9KtvVTLZZdhvA'); + xpk1.privateKey.toString().should.equal(xpk2.privateKey.toString()); + }); }); describe('#createCredentials', function() { diff --git a/packages/bitcore-wallet-service/src/lib/expressapp.ts b/packages/bitcore-wallet-service/src/lib/expressapp.ts index 349ca34ef9c..5c94d0dc9dd 100644 --- a/packages/bitcore-wallet-service/src/lib/expressapp.ts +++ b/packages/bitcore-wallet-service/src/lib/expressapp.ts @@ -432,9 +432,9 @@ export class ExpressApp { twoStep, silentFailure, includeServerMessages, - tokenAddresses: getParam('tokenAddress', true), - multisigContractAddress: getParam('multisigContractAddress'), - network: getParam('network') + tokenAddresses: getParam('tokenAddress', true) as string[] | null, + multisigContractAddress: getParam('multisigContractAddress') as string | null, + network: getParam('network') as string | null }; return opts; }; @@ -445,7 +445,7 @@ export class ExpressApp { promise.then( (server: WalletService) => new Promise(resolve => { - const options: any = buildOpts(req, server.copayerId); + const options = buildOpts(req, server.copayerId); if (options.tokenAddresses) { // add a null entry to array so we can get the chain balance options.tokenAddresses.unshift(null); @@ -456,7 +456,7 @@ export class ExpressApp { optsClone.tokenAddresses = null; optsClone.tokenAddress = tokenAddress; return server.getStatus(optsClone, (err, status) => { - const result: any = { + const result = { walletId: server.walletId, tokenAddress: optsClone.tokenAddress, success: true, @@ -467,7 +467,7 @@ export class ExpressApp { logger.error( `An error occurred retrieving wallet status - id: ${server.walletId} - token address: ${optsClone.tokenAddress} - err: ${err.message}` ); - cb(null, result); // do not throw error, continue with next wallets + cb(null, [result]); // do not throw error, continue with next wallets }); }, (err, result) => { @@ -504,6 +504,30 @@ export class ExpressApp { return res.json(_.flatten(responses)); }); + router.post('/v1/wallets/exist', async (req, res) => { + try { + const copayers = req.body.copayers; + if (!copayers || !Array.isArray(copayers)) { + logger.info('Invalid request to /v1/wallets/exist - copayers should be an array'); + return res.json([]); + } + + const storage = WalletService.getStorage(); + const existing = await Promise.all<{ copayerId: string; verified: boolean }>(copayers.map(c => new Promise((resolve) => { + storage.fetchCopayerLookup(c.copayerId, (err, copayer) => { + if (err || !copayer) { + return resolve({ copayerId: c.copayerId, verified: false }); + } + const verified = copayer.requestPubKeys.some(pubkey => Utils.verifyMessage(c.copayerId, c.signature, pubkey.key)); + return resolve({ copayerId: c.copayerId, verified }); + }); + }))); + return res.json(existing.filter(e => e.verified).map(e => e.copayerId)); + } catch (err) { + return returnError(err, res, req); + } + }); + router.get('/v1/wallets/:identifier/', (req, res) => { getServerWithAuth( req, diff --git a/packages/bitcore-wallet-service/src/lib/server.ts b/packages/bitcore-wallet-service/src/lib/server.ts index a53a98ada93..3aaf5ff4f29 100644 --- a/packages/bitcore-wallet-service/src/lib/server.ts +++ b/packages/bitcore-wallet-service/src/lib/server.ts @@ -944,7 +944,7 @@ export class WalletService implements IWalletService { ); } - /* + /** * Verifies a signature * @param text * @param signature @@ -954,7 +954,7 @@ export class WalletService implements IWalletService { return Utils.verifyMessage(text, signature, pubkey); } - /* + /** * Verifies a request public key * @param requestPubKey * @param signature @@ -965,7 +965,7 @@ export class WalletService implements IWalletService { return Utils.verifyMessage(requestPubKey, signature, pub.toString()); } - /* + /** * Verifies signature againt a collection of pubkeys * @param text * @param signature From b6d888a7a1b72283bb1ffbbf014049a32f2ee953 Mon Sep 17 00:00:00 2001 From: Kenny Joseph Date: Thu, 19 Mar 2026 14:50:49 -0400 Subject: [PATCH 2/3] replace old with new function; make tweaks per test cases; add test case --- packages/bitcore-wallet-client/src/lib/api.ts | 910 ++++---------- packages/bitcore-wallet-client/src/lib/key.ts | 18 +- .../bitcore-wallet-client/test/api.test.ts | 1073 ++++++++--------- 3 files changed, 758 insertions(+), 1243 deletions(-) diff --git a/packages/bitcore-wallet-client/src/lib/api.ts b/packages/bitcore-wallet-client/src/lib/api.ts index fa49e6d0eae..9f69d1cb157 100644 --- a/packages/bitcore-wallet-client/src/lib/api.ts +++ b/packages/bitcore-wallet-client/src/lib/api.ts @@ -4,7 +4,6 @@ import { EventEmitter } from 'events'; import querystring from 'querystring'; import Mnemonic from '@bitpay-labs/bitcore-mnemonic'; import * as CWC from '@bitpay-labs/crypto-wallet-core'; -import async from 'async'; import Bip38 from 'bip38'; import { singleton } from 'preconditions'; import * as Uuid from 'uuid'; @@ -3364,466 +3363,11 @@ export class API extends EventEmitter { }; } - /** - * Imports existing wallets against BWS and return key & clients[] for each account / coin - */ - static serverAssistedImport( - opts: { - /** Mnemonic words */ - words?: string; - /** Extended Private Key */ - xPrivKey?: string; - /** Mnemonic's passphrase */ - passphrase?: string; - /** Include testnet wallets */ - includeTestnetWallets?: boolean; - /** Search legacy wallets */ - includeLegacyWallets?: boolean; - /** Use 0 for BCH */ - use0forBCH?: boolean; - }, - /** BWS connection options (see ClientAPI constructor) */ - clientOpts: API | any, - /** Callback function in the standard form (err, key, clients) */ - callback: (err?: Error, key?: Key, clients?: API[]) => void - ) { - const { words, xPrivKey, passphrase, includeTestnetWallets, includeLegacyWallets, use0forBCH } = opts || {}; - $.checkArgument(words || xPrivKey, 'Missing argument: words or xPrivKey at '); - - const client = clientOpts instanceof API ? API.clone(clientOpts) : new API(clientOpts); - const credentials = []; - const copayerIdAlreadyTested = {}; - const keyCredentialIndex: { credentials: Credentials; key: Key; opts: any; status?: string }[] = []; - const clients = []; - let k: Key; - const sets: Array<{ nonCompliantDerivation: boolean; useLegacyCoinType?: boolean; useLegacyPurpose: boolean; passphrase?: any }> = [ - { - // current wallets: /[44,48]/[0,145]'/ - nonCompliantDerivation: false, - useLegacyCoinType: false, - useLegacyPurpose: false, - passphrase: undefined // is set later - } - ]; - - if (includeLegacyWallets) { - const legacyOpts = [ - { - // old bch wallets: /[44,48]/[0,0]'/ - nonCompliantDerivation: false, - useLegacyCoinType: true, - useLegacyPurpose: false - }, - { - // old BTC/BCH multisig wallets: /[44]/[0,145]'/ - nonCompliantDerivation: false, - useLegacyCoinType: false, - useLegacyPurpose: true - }, - { - // old multisig BCH wallets: /[44]/[0]'/ - nonCompliantDerivation: false, - useLegacyCoinType: true, - useLegacyPurpose: true - }, - { - // old BTC no-comp wallets: /44'/[0]'/ - nonCompliantDerivation: true, - useLegacyPurpose: true - } - ]; - - sets.push(...legacyOpts); - } - - const generateCredentials = (key, opts) => { - const c = key.createCredentials(null, { - coin: opts.coin, - chain: opts.chain?.toLowerCase() || opts.coin, // chain === coin IS NO LONGER TRUE for Arbitrum, Base, Optimisim - network: opts.network, - account: opts.account, - m: opts.m, - n: opts.n, - use0forBCH: opts.use0forBCH, // only used for server assisted import - algo: opts.algo - }); - - if (copayerIdAlreadyTested[c.copayerId + ':' + opts.n]) { - return; - } else { - copayerIdAlreadyTested[c.copayerId + ':' + opts.n] = true; - } - - keyCredentialIndex.push({ credentials: c, key, opts }); - credentials.push(c); - }; - - const checkKey = (key: Key) => { - let opts = [ - // [coin, chain, network, multisig, preForkBchCheck] - ['btc', 'btc', 'livenet'], - ['bch', 'bch', 'livenet'], - ['bch', 'bch', 'livenet', false, true], // check for prefork bch wallet - ['eth', 'eth', 'livenet'], - ['matic', 'matic', 'livenet'], - ['eth', 'arb', 'livenet'], - ['eth', 'base', 'livenet'], - ['eth', 'op', 'livenet'], - ['xrp', 'xrp', 'livenet'], - ['sol', 'sol', 'livenet'], - ['doge', 'doge', 'livenet'], - ['ltc', 'ltc', 'livenet'], - ['btc', 'btc', 'livenet', true], - ['bch', 'bch', 'livenet', true], - ['doge', 'doge', 'livenet', true], - ['ltc', 'ltc', 'livenet', true] - ]; - if (key.use44forMultisig) { - // testing old multi sig - opts = opts.filter(x => x[3]); - } - - if (key.use0forBCH) { - // testing BCH, old coin=0 wallets - opts = opts.filter(x => x[0] == 'bch'); - } - - if (key.compliantDerivation && includeTestnetWallets) { - const testnet = JSON.parse(JSON.stringify(opts)); - for (const x of testnet) { - x[2] = 'testnet'; - } - opts = opts.concat(testnet); - } - if (!key.compliantDerivation) { - // leave only BTC, and no testnet - opts = opts.filter(x => x[0] == 'btc'); - } - - for (let i = 0; i < opts.length; i++) { - const opt = opts[i]; - const optsObj = { - coin: opt[0], - chain: opt[1], - network: opt[2], - account: 0, - // If opt[3] == true then check for multisig address type. - // The values of m & n don't actually matter (other than n being >1 and m being <= n) - m: 1, - n: opt[3] ? 2 : 1, - use0forBCH: opt[4], - algo: opt[5], - }; - generateCredentials(key, optsObj); - } - }; - - const addWalletInfo = (combined, foundWallets, cb) => { - async.each( - combined, - (item, cb2) => { - const credentials = item.credentials; - const wallet = item.status.wallet; - client.fromString(credentials); - client._processStatus(item.status); - - if (!credentials.hasWalletInfo()) { - const me = (wallet.copayers || []).find(c => c.id === credentials.copayerId); - if (!me) return cb2(null, new Error('Copayer not in wallet')); - - try { - credentials.addWalletInfo( - wallet.id, - wallet.name, - wallet.m, - wallet.n, - me.name, - { - allowOverwrite: !!wallet.tssKeyId - } - ); - } catch (e) { - if (e.message) { - log.info('Trying credentials...', e.message); - } - if (e.message && e.message.match(/Bad\snr/)) { - return cb2(null, new Errors.WALLET_DOES_NOT_EXIST()); - } - } - } - if (wallet.status != 'complete') return cb2(null, item); - - if (item.status.customData?.walletPrivKey) { - credentials.addWalletPrivateKey(item.status.customData.walletPrivKey); - } - - if (credentials.walletPrivKey) { - if (!Verifier.checkCopayers(credentials, wallet.copayers)) { - return cb2(null, new Errors.SERVER_COMPROMISED()); - } - } else { - // this should only happen in AIR-GAPPED flows - log.warn('Could not verify copayers key (missing wallet Private Key)'); - } - - credentials.addPublicKeyRing( - client._extractPublicKeyRing(wallet.copayers) - ); - client.emit('walletCompleted', wallet); - - foundWallets.push(item); - cb2(); - }, - err => { - cb(err); - } - ); - }; - - const getClientsFromWallets = (err, res) => { - if (err) { - return callback(err); - } - - // marry all found wallets and keyCredentialIndex entries for simplicity - const combined = keyCredentialIndex - .map((x, i) => { - if (res[i].success) { - x.status = res[i].status; - return x; - } - }) - .filter(x => x); - - const foundWallets = []; - addWalletInfo(combined, foundWallets, err => { - if (err) return callback(err); - checkForOtherAccounts(foundWallets); - }); - }; - - const getNextBatch = (key, settings) => { - const accountKeyCredentialIndex = []; - const credBatch = []; - // add potential wallet account credentials - for (let i = 0; i < 5; i++) { - settings.account++; - const clonedSettings = JSON.parse(JSON.stringify(settings)); - const c = key.createCredentials(null, { - coin: clonedSettings.coin, // base currency used for fees. Helpful for UI - chain: clonedSettings.chain || clonedSettings.coin, - network: clonedSettings.network, - account: clonedSettings.account, - m: clonedSettings.m, - n: clonedSettings.n, - use0forBCH: use0forBCH // only used for server assisted import - }); - - accountKeyCredentialIndex.push({ - credentials: c, - key, - opts: clonedSettings - }); - credBatch.push(c); - } - return { credentials: credBatch, accountKeyCredentialIndex }; - }; - - const checkForOtherAccounts = foundWallets => { - const addtFoundWallets = []; - async.each( - foundWallets, - (wallet, next2) => { - k = wallet.key; - let mostRecentResults = [{ success: true }]; - async.whilst( - () => mostRecentResults.every(x => x.success), - next => { - const { credentials, accountKeyCredentialIndex } = getNextBatch( - k, - wallet.opts - ); - client.bulkClient.getStatusAll( - credentials, - { - silentFailure: true, - twoStep: true, - includeExtendedInfo: true, - ignoreIncomplete: true - }, - (err, response) => { - mostRecentResults = response; - const combined = accountKeyCredentialIndex - .map((x, i) => { - if (response[i].success) { - x.status = response[i].status; - return x; - } - }) - .filter(x => x); - addWalletInfo(combined, addtFoundWallets, next); - } - ); - }, - err => { - next2(err); - } - ); - }, - err => { - if (err) return callback(err); - const allWallets = foundWallets.concat(addtFoundWallets); - // generate clients - async.each( - allWallets, - async (wallet, next) => { - if ( - wallet.opts.coin == 'btc' && - (wallet.status.wallet.addressType == 'P2WPKH' || - wallet.status.wallet.addressType == 'P2WSH') - ) { - client.credentials.addressType = - wallet.status.wallet.n == 1 - ? Constants.SCRIPT_TYPES.P2WPKH - : Constants.SCRIPT_TYPES.P2WSH; - } - if (wallet.opts.coin === 'btc' && wallet.status.wallet.addressType === 'P2TR') { - client.credentials.addressType = Constants.SCRIPT_TYPES.P2TR; - } - // add client to list - const newClient = client.toClone(); - // newClient.credentials = settings.credentials; - newClient.fromString(wallet.credentials); - clients.push(newClient); - - async function handleChainTokensAndMultisig(chain, tokenAddresses, multisigInfo, tokenOpts, tokenUrlPath) { - // Handle importing of tokens - if (tokenAddresses?.length) { - async function getNetworkTokensData() { - return new Promise((resolve, reject) => { - newClient.request.get(`/v1/service/oneInch/getTokens/${tokenUrlPath}`, (err, data) => { - if (err) return reject(err); - return resolve(data); - }); - }); - } - - let customTokensData; - try { - customTokensData = await getNetworkTokensData(); - } catch (error) { - log.warn(`getNetworkTokensData err for ${chain}`, error); - customTokensData = null; - } - - for (const t of tokenAddresses) { - const token = tokenOpts[t] || (customTokensData && customTokensData[t]); - if (!token) { - log.warn(`Token ${t} unknown on ${chain}`); - continue; - } - log.info(`Importing token: ${token.name} on ${chain}`); - const tokenCredentials = newClient.credentials.getTokenCredentials(token, chain); - const tokenClient = newClient.toClone(); - tokenClient.credentials = tokenCredentials; - clients.push(tokenClient); - } - } - - // Handle importing of multisig wallets - for (const info of (multisigInfo || [])) { - log.info(`Importing multisig wallet on ${chain}. Address: ${info.multisigContractAddress} - m: ${info.m} - n: ${info.n}`); - const multisigCredentials = newClient.credentials.getMultisigEthCredentials({ - walletName: info.walletName, - multisigContractAddress: info.multisigContractAddress, - n: info.n, - m: info.m - }); - const multisigClient = newClient.toClone(); - multisigClient.credentials = multisigCredentials; - clients.push(multisigClient); - - const multisigTokenAddresses = info.tokenAddresses || []; - for (const t of multisigTokenAddresses) { - const token = tokenOpts[t]; - if (!token) { - log.warn(`Token ${t} unknown in multisig on ${chain}`); - continue; - } - log.info(`Importing multisig token: ${token.name} on ${chain}`); - const tokenCredentials = multisigClient.credentials.getTokenCredentials(token, chain); - const tokenClient = multisigClient.toClone(); - tokenClient.credentials = tokenCredentials; - clients.push(tokenClient); - } - } - } - - const chainConfigurations = [ - { chain: 'eth', tokenAddresses: wallet.status.preferences.tokenAddresses, multisigInfo: wallet.status.preferences.multisigEthInfo, tokenOpts: Constants.ETH_TOKEN_OPTS, tokenUrlPath: 'eth' }, - { chain: 'matic', tokenAddresses: wallet.status.preferences.maticTokenAddresses, multisigInfo: wallet.status.preferences.multisigMaticInfo, tokenOpts: Constants.MATIC_TOKEN_OPTS, tokenUrlPath: 'matic' }, - { chain: 'arb', tokenAddresses: wallet.status.preferences.arbTokenAddresses, multisigInfo: wallet.status.preferences.multisigArbInfo, tokenOpts: Constants.ARB_TOKEN_OPTS, tokenUrlPath: 'arb' }, - { chain: 'op', tokenAddresses: wallet.status.preferences.opTokenAddresses, multisigInfo: wallet.status.preferences.multisigOpInfo, tokenOpts: Constants.OP_TOKEN_OPTS, tokenUrlPath: 'op' }, - { chain: 'base', tokenAddresses: wallet.status.preferences.baseTokenAddresses, multisigInfo: wallet.status.preferences.multisigBaseInfo, tokenOpts: Constants.BASE_TOKEN_OPTS, tokenUrlPath: 'base' }, - { chain: 'sol', tokenAddresses: wallet.status.preferences.solTokenAddresses, multisigInfo: wallet.status.preferences.multisigSolInfo, tokenOpts: Constants.SOL_TOKEN_OPTS, tokenUrlPath: 'sol' }, - ]; - - for (const config of chainConfigurations) { - await handleChainTokensAndMultisig(config.chain, config.tokenAddresses, config.multisigInfo, config.tokenOpts, config.tokenUrlPath); - } - next(); - }, - err => { - if (err) return callback(err); - return callback(null, k, clients); - } - ); - } - ); - }; - - const id = Uuid.v4(); - for (const set of sets) { - try { - if (words) { - if (passphrase) { - set.passphrase = passphrase; - } - - k = new Key({ id, seedData: words, seedType: 'mnemonic', ...set }); - } else { - k = new Key({ - id, - seedData: xPrivKey, - seedType: 'extendedPrivateKey', - ...set - }); - } - } catch (e) { - log.info('Backup error:', e); - return callback(new Errors.INVALID_BACKUP()); - } - checkKey(k); - } - - // send batched calls to server - client.bulkClient.getStatusAll( - credentials, - { - silentFailure: true, - twoStep: true, - includeExtendedInfo: true, - ignoreIncomplete: true - }, - getClientsFromWallets - ); - } - /** * Derives keys from words/xPrivKey and checks for existing wallets against BWS. - * If wallets exist, generates credentials and returns a single `key` and an array of `clients` as found wallets. + * If wallets exist, generates credentials and returns a single `key` and an array of found wallets as `clients` */ - static async serverAssistedImportV2( + static async serverAssistedImport( opts: { /** Mnemonic words */ words?: string; @@ -3833,43 +3377,17 @@ export class API extends EventEmitter { passphrase?: string; /** Include testnet wallets */ includeTestnetWallets?: boolean; - /** Search legacy wallets */ + /** Search legacy wallets (pre-fork BCH & old multisig wallets) */ includeLegacyWallets?: boolean; }, /** BWS connection options (see ClientAPI constructor) */ clientOpts: API | any, - /** - * Event emitter to consume status updates, errors, and the end result - * Events: - * - `keyConfig.count` => int - total number of key configurations to be checked (not all configurations will necessarily be processed, as the process may exit early if wallets are found) - * - `keyConfig.start` => int - index of the current key configuration being processed - * - `keyConfig.keyCreated` => void - status event saying a key was successfully created from the provided backup data - * - `chainPermutations.count` => int - total number of permutaions of [chain/coin, network, and derivation strategy] to be checked for each key configuration - * - `chainPermutations.getKey` => int - index of the current permutation being processed, and the key is created along the permuation to be sent to BWS for existence check - * - `findingCopayers` => int - number of copayers being sent to BWS to check for existence. Note, this is called inside a loop and may be called more than once. The loop goes as long as there are copayers to check for - * - `foundCopayers` => int - number of copayers found in BWS. Note, this is called inside a loop and may be called more than once. Each event is the number of copayers found for that loop iteration, not the total sum of copayers found. - * - `foundCopayers.count` => int - total number of copayers in BWS. This is the sum of all `foundCopayers` events and is emitted when the process of checking copayers against BWS is complete - * - `keyConfig.noCopayersFound` => void - emitted when no copayers are found for a given key configuration, and the process is moving on to the next key configuration (if any) - * - `creatingCredentials` => void - status event saying client credentials are being created for a found copayers - * - `gettingStatuses` => void - status event saying wallet statuses are being fetched from BWS for the found copayers - * - `gatheringWalletsInfos` => int - number of wallets being processed to gather wallet info - * - `walletInfo.gatheringTokens` => { chain: string, network: string } - status event saying token info is being gathered for a wallet of a chain:network - * - `walletInfo.gatheringTokens.error` => { chain: string, network: string, error: Error } - emitted with error info if gathering token info fails for a wallet of a chain:network - * - `walletInfo.importingToken` => { chain: string, network: string, tokenName: string, tokenAddress: string } - token wallet is being imported for a wallet of a chain:network - * - `walletInfo.gatheringMultisig` => { chain: string, network: string } - status event saying multisig info is being gathered for a wallet of a chain:network - * - `walletInfo.multisig.creatingCredentials` => { chain: string, network: string, walletName: string, multisigContractAddress: string, m: int, n: int } - status event saying multisig wallet credentials are being created for a wallet of a chain:network - * - `walletInfo.multisig.importingToken` => { chain: string, network: string, walletName: string, multisigContractAddress: string, tokenName: string, tokenAddress: string } - token wallet is being imported for a multisig wallet of a chain:network - * - `error` => Error - emitted with any terminating error that throws during the process - * - `done` => { key: Key, clients: API[] } - emitted with the final result object containing the key and clients when the process is complete - */ + /** Event emitter to consume status updates, errors, and the end result */ events?: ServerAssistedImportEvents ) { $.checkArgument(!events || events instanceof EventEmitter, 'Invalid argument: events must be an EventEmitter instance'); try { const { words, xPrivKey, passphrase, includeTestnetWallets, includeLegacyWallets } = opts || {}; - events.on('findingCopayers', (num) => { - log.info(`Checking existence of ${num} copayers in BWS...`); - }); $.checkArgument(words || xPrivKey, 'Missing argument: words or xPrivKey at '); const client = clientOpts instanceof API ? API.clone(clientOpts) : new API(clientOpts); const keyConfigs: Array<{ @@ -3891,26 +3409,26 @@ export class API extends EventEmitter { if (includeLegacyWallets) { const legacyConfigs = [ { - // old bch wallets: /[44,48]/[0,0]'/ + // old BCH wallets: m/48'/0'/ nonCompliantDerivation: false, useLegacyCoinType: true, useLegacyPurpose: false }, { - // old BTC/BCH multisig wallets: /[44]/[0,145]'/ + // old BTC/BCH multisig wallets: m/44'/145'/ nonCompliantDerivation: false, useLegacyCoinType: false, useLegacyPurpose: true }, { - // old multisig BCH wallets: /[44]/[0]'/ + // old multisig BCH wallets: m/44'/0'/ nonCompliantDerivation: false, useLegacyCoinType: true, useLegacyPurpose: true }, { - // old BTC non-comp wallets: /44'/[0]'/ - nonCompliantDerivation: true, + // old BTC non-comp wallets: m/44'/0'/ + nonCompliantDerivation: true, // Non-padded private keys useLegacyCoinType: false, useLegacyPurpose: true } @@ -3958,16 +3476,16 @@ export class API extends EventEmitter { type KeyData = { copayerId: string; key: Key; - perms: Array<{ coin: string; chain: string; network: string; multisig?: boolean; preForkBchCheck?: boolean }>; + perm: { coin: string; chain: string; network: string; multisig?: boolean; preForkBchCheck?: boolean }; path: string; account: number; signature: string; requestPrivKey: any; }; - let key: Key; const clients: API[] = []; - + const keysToCheck: { [copayerId: string]: KeyData[] } = {}; + const id = Uuid.v4(); for (const i in keyConfigs) { events?.emit('keyConfig.start', Number(i)); @@ -3997,7 +3515,7 @@ export class API extends EventEmitter { let chainPermutations: { coin: string; chain: string; network: string; multisig?: boolean; preForkBchCheck?: boolean }[] = [ { coin: 'btc', chain: 'btc', network: 'livenet' }, { coin: 'bch', chain: 'bch', network: 'livenet' }, - { coin: 'bch', chain: 'bch', network: 'livenet', preForkBchCheck: true }, // check for prefork bch wallet + { coin: 'bch', chain: 'bch', network: 'livenet', preForkBchCheck: true }, // check for pre-fork BCH wallet { coin: 'eth', chain: 'eth', network: 'livenet' }, { coin: 'matic', chain: 'matic', network: 'livenet' }, { coin: 'eth', chain: 'arb', network: 'livenet' }, @@ -4039,246 +3557,254 @@ export class API extends EventEmitter { // deterministic request key const requestPrivKey = k.derive(passphrase, Constants.PATHS.REQUEST_KEY).privateKey; - const keysToCheck: { [copayerId: string]: KeyData } = {}; for (const j in chainPermutations) { events?.emit('chainPermutations.getKey', Number(j)); const perm = chainPermutations[j]; const keyData = getKeyToCheck({ key: k, requestPrivKey, perm, account: 0 }); - // assert(!allCopayerIds.has(keyData.copayerId) || !!keysToCheck[keyData.copayerId].perm.multisig == !!keyData.perm.multisig, 'Duplicate copayerId generated, this should not happen'); if (!keysToCheck[keyData.copayerId]) { - keysToCheck[keyData.copayerId] = { ...keyData, perms: [keyData.perm] }; - } else { - keysToCheck[keyData.copayerId].perms.push(keyData.perm); + keysToCheck[keyData.copayerId] = []; } + keysToCheck[keyData.copayerId].push(keyData); allCopayerIds.add(keyData.copayerId); } + } - const existingKeyDatas: { [copayerId: string]: KeyData & { credentials?: Credentials[] } } = {}; - let existingCopayerIds: Set; - do { - events?.emit('findingCopayers', Object.keys(keysToCheck).length); - const { body } = await client.request.post('/v1/wallets/exist', { - copayers: Object.values(keysToCheck).map(x => ({ copayerId: x.copayerId, signature: x.signature })) - }); + const existingKeyDatas: { [copayerId: string]: Array } = {}; + let existingCopayerIds: Set; + do { + events?.emit('findingCopayers', Object.keys(keysToCheck).length); + const { body } = await client.request.post('/v1/wallets/exist', { + copayers: Object.values(keysToCheck).map(x => ({ copayerId: x[0].copayerId, signature: x[0].signature })) + }); - existingCopayerIds = new Set(body); - const checkedCopayerIds = Object.keys(keysToCheck); - for (const copayerId of checkedCopayerIds) { - if (existingCopayerIds.has(copayerId)) { - const foundKeyData = keysToCheck[copayerId]; - existingKeyDatas[copayerId] = foundKeyData; - for (const perm of foundKeyData.perms) { - const nextKeyData = getKeyToCheck({ ...foundKeyData, account: foundKeyData.account + 1, perm }); - $.checkState(!allCopayerIds.has(nextKeyData.copayerId), 'Duplicate copayerId generated, this should not happen'); - if (!keysToCheck[nextKeyData.copayerId]) { - keysToCheck[nextKeyData.copayerId] = { ...nextKeyData, perms: [nextKeyData.perm] }; - } else { - keysToCheck[nextKeyData.copayerId].perms.push(nextKeyData.perm); - } + existingCopayerIds = new Set(body); + const checkedCopayerIds = Object.keys(keysToCheck); + for (const copayerId of checkedCopayerIds) { + if (existingCopayerIds.has(copayerId)) { + const foundKeyData = keysToCheck[copayerId]; + existingKeyDatas[copayerId] = foundKeyData; + for (const keyData of foundKeyData) { + const nextKeyData = getKeyToCheck({ ...keyData, account: keyData.account + 1 }); + $.checkState(!allCopayerIds.has(nextKeyData.copayerId), 'Duplicate copayerId generated, this should not happen'); + if (!keysToCheck[nextKeyData.copayerId]) { + keysToCheck[nextKeyData.copayerId] = []; } + keysToCheck[nextKeyData.copayerId].push(nextKeyData); } - delete keysToCheck[copayerId]; } - events?.emit('foundCopayers', existingCopayerIds.size); - } while (existingCopayerIds.size > 0); - - events?.emit('foundCopayers.count', Object.keys(existingKeyDatas).length); + delete keysToCheck[copayerId]; + } + events?.emit('foundCopayers', existingCopayerIds.size); + } while (existingCopayerIds.size > 0); + events?.emit('foundCopayers.count', Object.keys(existingKeyDatas).length); - // if no copayers were found for this keyConfig, continue to the next one - if (Object.keys(existingKeyDatas).length === 0) { - events?.emit('keyConfig.noCopayersFound'); - continue; - } + if (Object.keys(existingKeyDatas).length === 0) { + throw new Errors.WALLET_DOES_NOT_EXIST(); + } - // Build clients for found copayers - events?.emit('creatingCredentials'); - key = Object.values(existingKeyDatas)[0]?.key; // all found keys should be the same, so just take the first one to generate clients - const credentials: Array = []; - for (const copayerId in existingKeyDatas) { - const keyData = existingKeyDatas[copayerId]; - for (const perm of keyData.perms) { + // Build clients for found copayers + events?.emit('creatingCredentials'); + const credentialsToSubmit: { [copayerId: string]: Credentials } = {}; + // const credentialsTracker = new Set(); + for (const copayerId in existingKeyDatas) { + const keyDatas = existingKeyDatas[copayerId]; + for (const keyData of keyDatas) { + if (credentialsToSubmit[keyData.copayerId]) { + // Key derivation is computationally expensive in createCredentials, so re-use keys here + keyData.credentials = Credentials.fromDerivedKey({ + ...credentialsToSubmit[keyData.copayerId], + n: keyData.perm.multisig ? 2 : 1, + addressType: undefined // different n can lead to different address type + }); + } else { const creds = keyData.key.createCredentials(null, { - coin: perm.coin, - chain: perm.chain, - network: perm.network, + coin: keyData.perm.coin, + chain: keyData.perm.chain, + network: keyData.perm.network, account: keyData.account, m: 1, - n: perm.multisig ? 2 : 1, - use0forBCH: perm.preForkBchCheck, // only used for server assisted import + n: keyData.perm.multisig ? 2 : 1, + use0forBCH: keyData.perm.preForkBchCheck, // only used for server assisted import }); - keyData.credentials = keyData.credentials || []; - keyData.credentials.push(creds); - credentials.push(creds); + keyData.credentials = creds; + // No need to submit duplicate copayerId credentials, even though the credentials may differ slightly based on multisig, etc. + // The relevant info about which permutation to use for the copayer will come back from the status call + credentialsToSubmit[keyData.copayerId] = creds; } } - events?.emit('gettingStatuses'); - const walletsInfos = await client.bulkClient.getStatusAll( - credentials, - { - silentFailure: true, - twoStep: true, - includeExtendedInfo: true, - ignoreIncomplete: true - } - ); + } + events?.emit('gettingStatuses'); + const walletsInfos = await client.bulkClient.getStatusAll( + Object.values(credentialsToSubmit), + { + silentFailure: true, + twoStep: true, + includeExtendedInfo: true, + ignoreIncomplete: true + } + ); - events?.emit('gatheringWalletsInfos', walletsInfos.length); - for (const walletInfo of walletsInfos) { - if (!walletInfo.success) { + events?.emit('gatheringWalletsInfos', walletsInfos.length); + let key: Key; + for (const walletInfo of walletsInfos) { + if (!walletInfo.success) { + continue; + } + const { status } = walletInfo; + let found = false; + for (const copayer of status.wallet.copayers) { + const keyDatas = existingKeyDatas[copayer.id]; + const myKeyData = keyDatas?.find(({ credentials: c }) => c.copayerId === copayer.id && (c.n > 1) == (status.wallet.n > 1)); + if (!myKeyData) { continue; } - const { status } = walletInfo; - let found = false; - for (const me of status.wallet.copayers) { - const keyData = existingKeyDatas[me.id]; - if (keyData) { - found = true; - const credIndex = keyData.credentials.findIndex(c => c.copayerId === me.id && (c.n > 1) == (status.wallet.n > 1)); - const credentials = keyData.credentials[credIndex]; - const perm = keyData.perms[credIndex]; - const { network } = perm; - const { wallet } = status; - const newClient = API.clone(client); - newClient.fromString(credentials); - newClient._processStatus(status); - if (!credentials.hasWalletInfo()) { - try { - credentials.addWalletInfo( - wallet.id, - wallet.name, - wallet.m, - wallet.n, - me.name, - { - allowOverwrite: !!wallet.tssKeyId - } - ); - } catch (e) { - if (e.message) { - log.info('Trying credentials...', e.message); - } - if (e.message && e.message.match(/Bad\snr/)) { - log.warn(new Errors.WALLET_DOES_NOT_EXIST()); - break; - } + found = true; + key = myKeyData.key; + const myCredentials = myKeyData.credentials; + const perm = myKeyData.perm; + const { network } = perm; + const { wallet } = status; + const newClient = API.clone(client); + newClient.fromObj(myCredentials); + newClient._processStatus(status); + const credentials = newClient.credentials; + if (!credentials.hasWalletInfo()) { + try { + credentials.addWalletInfo( + wallet.id, + wallet.name, + wallet.m, + wallet.n, + copayer.name, + { + allowOverwrite: !!wallet.tssKeyId } + ); + } catch (e) { + if (e.message) { + log.info('Trying credentials...', e.message); } - if (wallet.status != 'complete') continue; - - if (status.customData?.walletPrivKey) { - credentials.addWalletPrivateKey(status.customData.walletPrivKey); + if (e.message && e.message.match(/Bad\snr/)) { + log.warn(new Errors.WALLET_DOES_NOT_EXIST()); + break; } + } + } + if (wallet.status != 'complete') continue; - if (credentials.walletPrivKey) { - if (!Verifier.checkCopayers(credentials, wallet.copayers)) { - log.warn(new Errors.SERVER_COMPROMISED()); - continue; - } - } else { - // this should only happen in AIR-GAPPED flows - log.warn('Could not verify copayers key (missing wallet Private Key)'); - } + if (status.customData?.walletPrivKey) { + credentials.addWalletPrivateKey(status.customData.walletPrivKey); + } - credentials.addPublicKeyRing( - newClient._extractPublicKeyRing(wallet.copayers) - ); - client.emit('walletCompleted', wallet); + if (credentials.walletPrivKey) { + if (!Verifier.checkCopayers(credentials, wallet.copayers)) { + log.warn(new Errors.SERVER_COMPROMISED()); + continue; + } + } else { + // this should only happen in AIR-GAPPED flows + log.warn('Could not verify copayers key (missing wallet Private Key)'); + } - if (perm.coin == 'btc') { - if (['P2WPKH', 'P2WSH'].includes(wallet.addressType)) { - newClient.credentials.addressType = wallet.n == 1 ? Constants.SCRIPT_TYPES.P2WPKH : Constants.SCRIPT_TYPES.P2WSH; - } else if (wallet.addressType === 'P2TR') { - newClient.credentials.addressType = Constants.SCRIPT_TYPES.P2TR; - } - } + credentials.addPublicKeyRing( + newClient._extractPublicKeyRing(wallet.copayers) + ); + client.emit('walletCompleted', wallet); - clients.push(newClient); - - // Handle importing of tokens and multisig wallets for EVM chains - { - const chainConfigurations = [ - { chain: 'eth', tokenAddresses: status.preferences.tokenAddresses, multisigInfo: status.preferences.multisigEthInfo, tokenOpts: Constants.ETH_TOKEN_OPTS }, - { chain: 'matic', tokenAddresses: status.preferences.maticTokenAddresses, multisigInfo: status.preferences.multisigMaticInfo, tokenOpts: Constants.MATIC_TOKEN_OPTS }, - { chain: 'arb', tokenAddresses: status.preferences.arbTokenAddresses, multisigInfo: status.preferences.multisigArbInfo, tokenOpts: Constants.ARB_TOKEN_OPTS }, - { chain: 'op', tokenAddresses: status.preferences.opTokenAddresses, multisigInfo: status.preferences.multisigOpInfo, tokenOpts: Constants.OP_TOKEN_OPTS }, - { chain: 'base', tokenAddresses: status.preferences.baseTokenAddresses, multisigInfo: status.preferences.multisigBaseInfo, tokenOpts: Constants.BASE_TOKEN_OPTS }, - { chain: 'sol', tokenAddresses: status.preferences.solTokenAddresses, multisigInfo: status.preferences.multisigSolInfo, tokenOpts: Constants.SOL_TOKEN_OPTS }, - ]; - - for (const config of chainConfigurations) { - const { chain, tokenAddresses, multisigInfo, tokenOpts } = config; - if (chain !== perm.chain) continue; - events?.emit('walletInfo.gatheringTokens', { chain, network }); - // Handle importing of tokens - if (tokenAddresses?.length) { - const { body: customTokensData } = await newClient.request.get(`/v1/service/oneInch/getTokens/${chain}`).catch(err => { - log.warn(`getNetworkTokensData err for ${chain}:${network}`, err); - events?.emit('walletInfo.gatheringTokens.error', { chain, network, error: err }); - return { body: null }; - }); - - for (const t of tokenAddresses) { - const token = tokenOpts[t] || (customTokensData && customTokensData[t]); - if (!token) { - log.warn(`Token ${t} unknown on ${chain}`); - continue; - } - log.info(`Importing token: ${token.name} on ${chain}`); - events?.emit('walletInfo.importingToken', { chain, network, tokenName: token.name, tokenAddress: t }); - const tokenCredentials = newClient.credentials.getTokenCredentials(token, chain); - const tokenClient = newClient.toClone(); - tokenClient.credentials = tokenCredentials; - clients.push(tokenClient); - } + if (perm.coin == 'btc') { + if (['P2WPKH', 'P2WSH'].includes(wallet.addressType)) { + newClient.credentials.addressType = wallet.n == 1 ? Constants.SCRIPT_TYPES.P2WPKH : Constants.SCRIPT_TYPES.P2WSH; + } else if (wallet.addressType === 'P2TR') { + newClient.credentials.addressType = Constants.SCRIPT_TYPES.P2TR; + } + } + + clients.push(newClient); + + // Handle importing of tokens and multisig wallets for EVM chains + { + const chainConfigurations = [ + { chain: 'eth', tokenAddresses: status.preferences.tokenAddresses, multisigInfo: status.preferences.multisigEthInfo, tokenOpts: Constants.ETH_TOKEN_OPTS }, + { chain: 'matic', tokenAddresses: status.preferences.maticTokenAddresses, multisigInfo: status.preferences.multisigMaticInfo, tokenOpts: Constants.MATIC_TOKEN_OPTS }, + { chain: 'arb', tokenAddresses: status.preferences.arbTokenAddresses, multisigInfo: status.preferences.multisigArbInfo, tokenOpts: Constants.ARB_TOKEN_OPTS }, + { chain: 'op', tokenAddresses: status.preferences.opTokenAddresses, multisigInfo: status.preferences.multisigOpInfo, tokenOpts: Constants.OP_TOKEN_OPTS }, + { chain: 'base', tokenAddresses: status.preferences.baseTokenAddresses, multisigInfo: status.preferences.multisigBaseInfo, tokenOpts: Constants.BASE_TOKEN_OPTS }, + { chain: 'sol', tokenAddresses: status.preferences.solTokenAddresses, multisigInfo: status.preferences.multisigSolInfo, tokenOpts: Constants.SOL_TOKEN_OPTS }, + ]; + + for (const config of chainConfigurations) { + const { chain, tokenAddresses, multisigInfo, tokenOpts } = config; + if (chain !== perm.chain) continue; + events?.emit('walletInfo.gatheringTokens', { chain, network }); + // Handle importing of tokens + if (tokenAddresses?.length) { + const { body: customTokensData } = await newClient.request.get(`/v1/service/oneInch/getTokens/${chain}`).catch(err => { + log.warn(`getNetworkTokensData err for ${chain}:${network}`, err); + events?.emit('walletInfo.gatheringTokens.error', { chain, network, error: err }); + return { body: null }; + }); + + for (const t of tokenAddresses) { + const token = tokenOpts[t] || (customTokensData && customTokensData[t]); + if (!token) { + log.warn(`Token ${t} unknown on ${chain}`); + continue; } + log.info(`Importing token: ${token.name} on ${chain}`); + events?.emit('walletInfo.importingToken', { chain, network, tokenName: token.name, tokenAddress: t }); + const tokenCredentials = newClient.credentials.getTokenCredentials(token, chain); + const tokenClient = newClient.toClone(); + tokenClient.credentials = tokenCredentials; + clients.push(tokenClient); + } + } - events?.emit('walletInfo.gatheringMultisig', { chain, network }); - - // Handle importing of multisig wallets (I don't believe this contract-based multisig feature was ever used, so this might be dead code) - for (const info of (multisigInfo || [])) { - log.info(`Importing multisig wallet on ${chain}. Address: ${info.multisigContractAddress} - m: ${info.m} - n: ${info.n}`); - events?.emit('walletInfo.multisig.creatingCredentials', { ...info, chain, network }); - const multisigCredentials = newClient.credentials.getMultisigEthCredentials({ - walletName: info.walletName, - multisigContractAddress: info.multisigContractAddress, - n: info.n, - m: info.m - }); - const multisigClient = newClient.toClone(); - multisigClient.credentials = multisigCredentials; - clients.push(multisigClient); - - const multisigTokenAddresses = info.tokenAddresses || []; - for (const t of multisigTokenAddresses) { - const token = tokenOpts[t]; - if (!token) { - log.warn(`Token ${t} unknown in multisig on ${chain}`); - continue; - } - log.info(`Importing multisig token: ${token.name} on ${chain}`); - events?.emit('walletInfo.multisig.importingToken', { ...info, chain, network, tokenName: token.name, tokenAddress: t }); - const tokenCredentials = multisigClient.credentials.getTokenCredentials(token, chain); - const tokenClient = multisigClient.toClone(); - tokenClient.credentials = tokenCredentials; - clients.push(tokenClient); - } + events?.emit('walletInfo.gatheringMultisig', { chain, network }); + + // Handle importing of multisig wallets (I don't believe this contract-based multisig feature was ever used, so this might be dead code) + for (const info of (multisigInfo || [])) { + log.info(`Importing multisig wallet on ${chain}. Address: ${info.multisigContractAddress} - m: ${info.m} - n: ${info.n}`); + events?.emit('walletInfo.multisig.creatingCredentials', { ...info, chain, network }); + const multisigCredentials = newClient.credentials.getMultisigEthCredentials({ + walletName: info.walletName, + multisigContractAddress: info.multisigContractAddress, + n: info.n, + m: info.m + }); + const multisigClient = newClient.toClone(); + multisigClient.credentials = multisigCredentials; + clients.push(multisigClient); + + const multisigTokenAddresses = info.tokenAddresses || []; + for (const t of multisigTokenAddresses) { + const token = tokenOpts[t]; + if (!token) { + log.warn(`Token ${t} unknown in multisig on ${chain}`); + continue; } + log.info(`Importing multisig token: ${token.name} on ${chain}`); + events?.emit('walletInfo.multisig.importingToken', { ...info, chain, network, tokenName: token.name, tokenAddress: t }); + const tokenCredentials = multisigClient.credentials.getTokenCredentials(token, chain); + const tokenClient = multisigClient.toClone(); + tokenClient.credentials = tokenCredentials; + clients.push(tokenClient); } } } } - if (!found) { - log.warn(new Error('Copayer not in wallet')); - } } - // If we've gotten this far, return the results which short-circuits the rest of the keyConfigs. - events?.emit('done', { key, clients }); - return { key, clients }; + if (!found) { + log.warn(new Error('Copayer not in wallet')); + } } - throw new Errors.WALLET_DOES_NOT_EXIST(); + if (!clients.length) { + throw new Errors.WALLET_DOES_NOT_EXIST(); + } + events?.emit('done', { key, clients }); + return { key, clients }; } catch (err) { if (events) { events.emit('error', err); diff --git a/packages/bitcore-wallet-client/src/lib/key.ts b/packages/bitcore-wallet-client/src/lib/key.ts index 3fef779dc81..2c17592ba35 100644 --- a/packages/bitcore-wallet-client/src/lib/key.ts +++ b/packages/bitcore-wallet-client/src/lib/key.ts @@ -45,15 +45,21 @@ export interface KeyOptions { id?: string; seedType: 'new' | 'extendedPrivateKey' | 'object' | 'mnemonic' | 'objectV1'; seedData?: any; - passphrase?: string; // seed passphrase - password?: string; // encrypting password - encryptionOpts?: { iter?: number }; // options for encryption - use0forBCH?: boolean; + /** Seed passphrase (legacy) - essentially an extra, custom word in a mnemonic (no longer used) */ + passphrase?: string; + /** Encrypting password */ + password?: string; + /** Options for encryption */ + encryptionOpts?: { iter?: number }; + /** Use legacy 'purpose' in derivation path for multisig (44' => m/44'/coin_type'/account'/change/address_index */ useLegacyPurpose?: boolean; + /** Use legacy 'coin_type' in derivation path for BCH (0' => m/purpose'/0'/account'/change/address_index */ useLegacyCoinType?: boolean; nonCompliantDerivation?: boolean; + /** Language for mnemonic (default: en) */ language?: Language; - algo?: KeyAlgorithm; // eddsa or ecdsa (Bitcoin) by default + /** "ECDSA" (default - Bitcoin, etc.) or "EDDSA" (Solana) */ + algo?: KeyAlgorithm; }; export interface ExportedKey { @@ -110,7 +116,7 @@ export class Key { * * // data for derived credentials. * 'use0forBCH': 'use0forBCH', // use the 0 coin' path element in BCH (legacy) - * 'use44forMultisig': 'use44forMultisig', // use the purpose 44' for multisig wallts (legacy) + * 'use44forMultisig': 'use44forMultisig', // use the purpose 44' for multisig wallets (legacy) * 'id': 'id', * }; */ diff --git a/packages/bitcore-wallet-client/test/api.test.ts b/packages/bitcore-wallet-client/test/api.test.ts index e43b77bc82d..e2eb2d9527e 100644 --- a/packages/bitcore-wallet-client/test/api.test.ts +++ b/packages/bitcore-wallet-client/test/api.test.ts @@ -6484,22 +6484,26 @@ describe('client API', function() { describe('#import FromMnemonic', () => { it('should handle importing an invalid mnemonic', function(done) { const mnemonicWords = 'this is an invalid mnemonic'; - Client.serverAssistedImport({ words: mnemonicWords }, {}, err => { - should.exist(err); - err.should.be.an.instanceOf(Errors.INVALID_BACKUP); - done(); - }); + Client.serverAssistedImport({ words: mnemonicWords }, {}) + .then(() => { throw new Error('Should have thrown'); }) + .catch(err => { + should.exist(err); + err.should.be.an.instanceOf(Errors.INVALID_BACKUP); + done(); + }); }); }); describe('#import FromExtendedPrivateKey', () => { it('should handle importing an invalid extended private key', function(done) { const xPrivKey = 'this is an invalid key'; - Client.serverAssistedImport({ xPrivKey }, {}, err => { - should.exist(err); - err.should.be.an.instanceOf(Errors.INVALID_BACKUP); - done(); - }); + Client.serverAssistedImport({ xPrivKey }, {}) + .then(() => { throw new Error('Should have thrown'); }) + .catch(err => { + should.exist(err); + err.should.be.an.instanceOf(Errors.INVALID_BACKUP); + done(); + }); }); }); @@ -6530,28 +6534,26 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true, includeLegacyWallets: true }, helpers.newClient(app), - (err, k, c) => { + ).then(({ key: k, clients: c }) => { + c.length.should.equal(1); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { should.not.exist(err); - c.length.should.equal(1); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.getMainAddresses({}, (err, list) => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.getMainAddresses({}, (err, list) => { - should.not.exist(err); - should.exist(list); - list[0].address.should.equal(addr.address); - done(); - }); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); }); - } - ); + }); + }).catch(done); }); }); }); - it('should be able to gain access to multiple 1-1 wallets from mnemonic using V2 import function', async function() { + it('should be able to gain access to multiple 1-1 wallets from mnemonic', async function() { let walletCnt = 0; await helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'btc', chain: 'btc' }); walletCnt++; @@ -6642,7 +6644,7 @@ describe('client API', function() { should.exist(addr); addrs.add(addr.address); } - const retVal = await Client.serverAssistedImportV2({ words, includeTestnetWallets: true, includeLegacyWallets: true }, helpers.newClient(app)); + const retVal = await Client.serverAssistedImport({ words, includeTestnetWallets: true, includeLegacyWallets: true }, helpers.newClient(app)); const { key: k, clients: c } = retVal; c.length.should.equal(walletCnt); const recoveryClient = c[0]; @@ -6676,29 +6678,28 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the eth wallet + 2 tokens. - c.length.should.equal(3); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + ).then(({ key: k, clients: c }) => { + // the eth wallet + 2 tokens. + c.length.should.equal(3); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('eth'); + recoveryClient.credentials.chain.should.equal('eth'); + const recoveryClient2 = c[2]; + recoveryClient2.openWallet(null, err => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('eth'); - recoveryClient.credentials.chain.should.equal('eth'); - const recoveryClient2 = c[2]; - recoveryClient2.openWallet(null, err => { - should.not.exist(err); - recoveryClient2.credentials.coin.should.equal('gusd'); - should.exist(recoveryClient2.credentials.chain); - recoveryClient2.credentials.chain.should.equal('eth'); - recoveryClient2.credentials.walletId.should.equal(`${walletId}-${tokenAddresses[1]}`); - done(); - }); + recoveryClient2.credentials.coin.should.equal('gusd'); + should.exist(recoveryClient2.credentials.chain); + recoveryClient2.credentials.chain.should.equal('eth'); + recoveryClient2.credentials.walletId.should.equal(`${walletId}-${tokenAddresses[1]}`); + done(); }); - } - ); + }); + }).catch(done); } ); }); @@ -6719,29 +6720,28 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the eth wallet + 1 token. - c.length.should.equal(2); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + ).then(({ key: k, clients: c }) => { + // the eth wallet + 1 token. + c.length.should.equal(2); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('eth'); + recoveryClient.credentials.chain.should.equal('eth'); + const recoveryClient2 = c[1]; + recoveryClient2.openWallet(null, err => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('eth'); - recoveryClient.credentials.chain.should.equal('eth'); - const recoveryClient2 = c[1]; - recoveryClient2.openWallet(null, err => { - should.not.exist(err); - recoveryClient2.credentials.coin.should.equal('usdc'); - should.exist(recoveryClient2.credentials.chain); - recoveryClient2.credentials.chain.should.equal('eth'); - recoveryClient2.credentials.walletId.should.equal(`${walletId}-${tokenAddresses[0]}`); - done(); - }); + recoveryClient2.credentials.coin.should.equal('usdc'); + should.exist(recoveryClient2.credentials.chain); + recoveryClient2.credentials.chain.should.equal('eth'); + recoveryClient2.credentials.walletId.should.equal(`${walletId}-${tokenAddresses[0]}`); + done(); }); - } - ); + }); + }).catch(done); }); }); }); @@ -6761,20 +6761,20 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the eth wallet + 1 unknown token addresses on preferences. - c.length.should.equal(1); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { - should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('eth'); - recoveryClient.credentials.chain.should.equal('eth'); - done(); - }); + ).then(({ key: k, clients: c }) => { + // the eth wallet + 1 unknown token addresses on preferences. + c.length.should.equal(1); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('eth'); + recoveryClient.credentials.chain.should.equal('eth'); + done(); }); + }).catch(done); }); }); }); @@ -6798,29 +6798,28 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the matic wallet + 2 tokens. - c.length.should.equal(3); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + ).then(({ key: k, clients: c }) => { + // the matic wallet + 2 tokens. + c.length.should.equal(3); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('matic'); + recoveryClient.credentials.chain.should.equal('matic'); + const recoveryClient2 = c[2]; + recoveryClient2.openWallet(null, err => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('matic'); - recoveryClient.credentials.chain.should.equal('matic'); - const recoveryClient2 = c[2]; - recoveryClient2.openWallet(null, err => { - should.not.exist(err); - recoveryClient2.credentials.coin.should.equal('dai'); - should.exist(recoveryClient2.credentials.chain); - recoveryClient2.credentials.chain.should.equal('matic'); - recoveryClient2.credentials.walletId.should.equal(`${walletId}-${maticTokenAddresses[1]}`); - done(); - }); + recoveryClient2.credentials.coin.should.equal('dai'); + should.exist(recoveryClient2.credentials.chain); + recoveryClient2.credentials.chain.should.equal('matic'); + recoveryClient2.credentials.walletId.should.equal(`${walletId}-${maticTokenAddresses[1]}`); + done(); }); - } - ); + }); + }).catch(done); } ); }); @@ -6841,29 +6840,28 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the matic wallet + 1 token. - c.length.should.equal(2); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + ).then(({ key: k, clients: c }) => { + // the matic wallet + 1 token. + c.length.should.equal(2); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('matic'); + recoveryClient.credentials.chain.should.equal('matic'); + const recoveryClient2 = c[1]; + recoveryClient2.openWallet(null, err => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('matic'); - recoveryClient.credentials.chain.should.equal('matic'); - const recoveryClient2 = c[1]; - recoveryClient2.openWallet(null, err => { - should.not.exist(err); - recoveryClient2.credentials.coin.should.equal('usdc.e'); - should.exist(recoveryClient2.credentials.chain); - recoveryClient2.credentials.chain.should.equal('matic'); - recoveryClient2.credentials.walletId.should.equal(`${walletId}-${maticTokenAddresses[0]}`); - done(); - }); + recoveryClient2.credentials.coin.should.equal('usdc.e'); + should.exist(recoveryClient2.credentials.chain); + recoveryClient2.credentials.chain.should.equal('matic'); + recoveryClient2.credentials.walletId.should.equal(`${walletId}-${maticTokenAddresses[0]}`); + done(); }); - } - ); + }); + }).catch(done); }); }); }); @@ -6883,20 +6881,20 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the matic wallet + 1 unknown token addresses on preferences. - c.length.should.equal(1); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { - should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('matic'); - recoveryClient.credentials.chain.should.equal('matic'); - done(); - }); + ).then(({ key: k, clients: c }) => { + // the matic wallet + 1 unknown token addresses on preferences. + c.length.should.equal(1); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('matic'); + recoveryClient.credentials.chain.should.equal('matic'); + done(); }); + }).catch(done); }); }); }); @@ -6920,29 +6918,28 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the op wallet + 2 tokens. - c.length.should.equal(3); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + ).then(({ key: k, clients: c }) => { + // the op wallet + 2 tokens. + c.length.should.equal(3); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('eth'); + recoveryClient.credentials.chain.should.equal('op'); + const recoveryClient2 = c[2]; + recoveryClient2.openWallet(null, err => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('eth'); - recoveryClient.credentials.chain.should.equal('op'); - const recoveryClient2 = c[2]; - recoveryClient2.openWallet(null, err => { - should.not.exist(err); - recoveryClient2.credentials.coin.should.equal('wbtc'); - should.exist(recoveryClient2.credentials.chain); - recoveryClient2.credentials.chain.should.equal('op'); - recoveryClient2.credentials.walletId.should.equal(`${walletId}-${opTokenAddresses[1]}`); - done(); - }); + recoveryClient2.credentials.coin.should.equal('wbtc'); + should.exist(recoveryClient2.credentials.chain); + recoveryClient2.credentials.chain.should.equal('op'); + recoveryClient2.credentials.walletId.should.equal(`${walletId}-${opTokenAddresses[1]}`); + done(); }); - } - ); + }); + }).catch(done); } ); }); @@ -6963,29 +6960,28 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the op wallet + 1 token. - c.length.should.equal(2); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + ).then(({ key: k, clients: c }) => { + // the op wallet + 1 token. + c.length.should.equal(2); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('eth'); + recoveryClient.credentials.chain.should.equal('op'); + const recoveryClient2 = c[1]; + recoveryClient2.openWallet(null, err => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('eth'); - recoveryClient.credentials.chain.should.equal('op'); - const recoveryClient2 = c[1]; - recoveryClient2.openWallet(null, err => { - should.not.exist(err); - recoveryClient2.credentials.coin.should.equal('usdc'); - should.exist(recoveryClient2.credentials.chain); - recoveryClient2.credentials.chain.should.equal('op'); - recoveryClient2.credentials.walletId.should.equal(`${walletId}-${opTokenAddresses[0]}`); - done(); - }); + recoveryClient2.credentials.coin.should.equal('usdc'); + should.exist(recoveryClient2.credentials.chain); + recoveryClient2.credentials.chain.should.equal('op'); + recoveryClient2.credentials.walletId.should.equal(`${walletId}-${opTokenAddresses[0]}`); + done(); }); - } - ); + }); + }).catch(done); }); }); }); @@ -7005,20 +7001,20 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the op wallet + 1 unknown token addresses on preferences. - c.length.should.equal(1); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { - should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('eth'); - recoveryClient.credentials.chain.should.equal('op'); - done(); - }); + ).then(({ key: k, clients: c }) => { + // the op wallet + 1 unknown token addresses on preferences. + c.length.should.equal(1); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('eth'); + recoveryClient.credentials.chain.should.equal('op'); + done(); }); + }).catch(done); }); }); }); @@ -7042,29 +7038,28 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the base wallet + 2 tokens. - c.length.should.equal(3); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + ).then(({ key: k, clients: c }) => { + // the base wallet + 2 tokens. + c.length.should.equal(3); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('eth'); + recoveryClient.credentials.chain.should.equal('base'); + const recoveryClient2 = c[2]; + recoveryClient2.openWallet(null, err => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('eth'); - recoveryClient.credentials.chain.should.equal('base'); - const recoveryClient2 = c[2]; - recoveryClient2.openWallet(null, err => { - should.not.exist(err); - recoveryClient2.credentials.coin.should.equal('weth'); - should.exist(recoveryClient2.credentials.chain); - recoveryClient2.credentials.chain.should.equal('base'); - recoveryClient2.credentials.walletId.should.equal(`${walletId}-${baseTokenAddresses[1]}`); - done(); - }); + recoveryClient2.credentials.coin.should.equal('weth'); + should.exist(recoveryClient2.credentials.chain); + recoveryClient2.credentials.chain.should.equal('base'); + recoveryClient2.credentials.walletId.should.equal(`${walletId}-${baseTokenAddresses[1]}`); + done(); }); - } - ); + }); + }).catch(done); } ); }); @@ -7085,29 +7080,28 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the base wallet + 1 token. - c.length.should.equal(2); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + ).then(({ key: k, clients: c }) => { + // the base wallet + 1 token. + c.length.should.equal(2); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('eth'); + recoveryClient.credentials.chain.should.equal('base'); + const recoveryClient2 = c[1]; + recoveryClient2.openWallet(null, err => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('eth'); - recoveryClient.credentials.chain.should.equal('base'); - const recoveryClient2 = c[1]; - recoveryClient2.openWallet(null, err => { - should.not.exist(err); - recoveryClient2.credentials.coin.should.equal('usdc'); - should.exist(recoveryClient2.credentials.chain); - recoveryClient2.credentials.chain.should.equal('base'); - recoveryClient2.credentials.walletId.should.equal(`${walletId}-${baseTokenAddresses[0]}`); - done(); - }); + recoveryClient2.credentials.coin.should.equal('usdc'); + should.exist(recoveryClient2.credentials.chain); + recoveryClient2.credentials.chain.should.equal('base'); + recoveryClient2.credentials.walletId.should.equal(`${walletId}-${baseTokenAddresses[0]}`); + done(); }); - } - ); + }); + }).catch(done); }); }); }); @@ -7127,20 +7121,20 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the base wallet + unknown token addresses should be ignored. - c.length.should.equal(1); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { - should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('eth'); - recoveryClient.credentials.chain.should.equal('base'); - done(); - }); + ).then(({ key: k, clients: c }) => { + // the base wallet + unknown token addresses should be ignored. + c.length.should.equal(1); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('eth'); + recoveryClient.credentials.chain.should.equal('base'); + done(); }); + }).catch(done); }); }); }); @@ -7164,29 +7158,28 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the arb wallet + 2 tokens. - c.length.should.equal(3); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + ).then(({ key: k, clients: c }) => { + // the arb wallet + 2 tokens. + c.length.should.equal(3); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('eth'); + recoveryClient.credentials.chain.should.equal('arb'); + const recoveryClient2 = c[2]; + recoveryClient2.openWallet(null, err => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('eth'); - recoveryClient.credentials.chain.should.equal('arb'); - const recoveryClient2 = c[2]; - recoveryClient2.openWallet(null, err => { - should.not.exist(err); - recoveryClient2.credentials.coin.should.equal('wbtc'); - should.exist(recoveryClient2.credentials.chain); - recoveryClient2.credentials.chain.should.equal('arb'); - recoveryClient2.credentials.walletId.should.equal(`${walletId}-${arbTokenAddresses[1]}`); - done(); - }); + recoveryClient2.credentials.coin.should.equal('wbtc'); + should.exist(recoveryClient2.credentials.chain); + recoveryClient2.credentials.chain.should.equal('arb'); + recoveryClient2.credentials.walletId.should.equal(`${walletId}-${arbTokenAddresses[1]}`); + done(); }); - } - ); + }); + }).catch(done); } ); }); @@ -7207,29 +7200,28 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the arb wallet + 1 token. - c.length.should.equal(2); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + ).then(({ key: k, clients: c }) => { + // the arb wallet + 1 token. + c.length.should.equal(2); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('eth'); + recoveryClient.credentials.chain.should.equal('arb'); + const recoveryClient2 = c[1]; + recoveryClient2.openWallet(null, err => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('eth'); - recoveryClient.credentials.chain.should.equal('arb'); - const recoveryClient2 = c[1]; - recoveryClient2.openWallet(null, err => { - should.not.exist(err); - recoveryClient2.credentials.coin.should.equal('usdc'); - should.exist(recoveryClient2.credentials.chain); - recoveryClient2.credentials.chain.should.equal('arb'); - recoveryClient2.credentials.walletId.should.equal(`${walletId}-${arbTokenAddresses[0]}`); - done(); - }); + recoveryClient2.credentials.coin.should.equal('usdc'); + should.exist(recoveryClient2.credentials.chain); + recoveryClient2.credentials.chain.should.equal('arb'); + recoveryClient2.credentials.walletId.should.equal(`${walletId}-${arbTokenAddresses[0]}`); + done(); }); - } - ); + }); + }).catch(done); }); }); }); @@ -7249,20 +7241,20 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - // the arb wallet + unknown token addresses should be ignored. - c.length.should.equal(1); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { - should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.walletId.should.equal(walletId); - recoveryClient.credentials.coin.should.equal('eth'); - recoveryClient.credentials.chain.should.equal('arb'); - done(); - }); + ).then(({ key: k, clients: c }) => { + // the arb wallet + unknown token addresses should be ignored. + c.length.should.equal(1); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.walletId.should.equal(walletId); + recoveryClient.credentials.coin.should.equal('eth'); + recoveryClient.credentials.chain.should.equal('arb'); + done(); }); + }).catch(done); }); }); }); @@ -7281,27 +7273,26 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - should.not.exist(err); - c.length.should.equal(2); - c[0].credentials.coin.should.equal('btc'); - c[1].credentials.coin.should.equal('bch'); - c[0].credentials.copayerId.should.not.equal(c[1].credentials.copayerId); + ).then(({ key: k, clients: c }) => { + should.not.exist(err); + c.length.should.equal(2); + c[0].credentials.coin.should.equal('btc'); + c[1].credentials.coin.should.equal('bch'); + c[0].credentials.copayerId.should.not.equal(c[1].credentials.copayerId); - const recoveryClient = c[1]; - recoveryClient.openWallet(null, err => { + const recoveryClient = c[1]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.getMainAddresses({}, (err, list) => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.getMainAddresses({}, (err, list) => { - should.not.exist(err); - should.exist(list); - list[0].address.should.equal(addr.address); - done(); - }); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); }); - } - ); + }); + }); }); }); }); @@ -7320,26 +7311,25 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { + ).then(({ key: k, clients: c }) => { + should.not.exist(err); + c.length.should.equal(2); + c[0].credentials.coin.should.equal('btc'); + c[1].credentials.coin.should.equal('bch'); + c[0].credentials.copayerId.should.not.equal(c[1].credentials.copayerId); + const recoveryClient = c[1]; + recoveryClient.openWallet(null, err => { should.not.exist(err); - c.length.should.equal(2); - c[0].credentials.coin.should.equal('btc'); - c[1].credentials.coin.should.equal('bch'); - c[0].credentials.copayerId.should.not.equal(c[1].credentials.copayerId); - const recoveryClient = c[1]; - recoveryClient.openWallet(null, err => { + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.getMainAddresses({}, (err, list) => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.getMainAddresses({}, (err, list) => { - should.not.exist(err); - should.exist(list); - list[0].address.should.equal(addr.address); - done(); - }); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); }); - } - ); + }); + }).catch(done); }); }); }); @@ -7359,35 +7349,34 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { + ).then(({ key: k, clients: c }) => { + should.not.exist(err); + c.length.should.equal(3); + c[0].credentials.coin.should.equal('btc'); + c[1].credentials.coin.should.equal('btc'); + c[2].credentials.coin.should.equal('btc'); + c[0].credentials.account.should.equal(0); + c[1].credentials.account.should.equal(1); + c[2].credentials.account.should.equal(2); + c[0].credentials.copayerId.should.not.equal(c[1].credentials.copayerId); + c[0].credentials.copayerId.should.not.equal(c[2].credentials.copayerId); + c[1].credentials.copayerId.should.not.equal(c[2].credentials.copayerId); + should.exist(c[0].credentials.walletId); + should.exist(c[1].credentials.walletId); + should.exist(c[2].credentials.walletId); + const recoveryClient = c[2]; + recoveryClient.openWallet(null, err => { should.not.exist(err); - c.length.should.equal(3); - c[0].credentials.coin.should.equal('btc'); - c[1].credentials.coin.should.equal('btc'); - c[2].credentials.coin.should.equal('btc'); - c[0].credentials.account.should.equal(0); - c[1].credentials.account.should.equal(1); - c[2].credentials.account.should.equal(2); - c[0].credentials.copayerId.should.not.equal(c[1].credentials.copayerId); - c[0].credentials.copayerId.should.not.equal(c[2].credentials.copayerId); - c[1].credentials.copayerId.should.not.equal(c[2].credentials.copayerId); - should.exist(c[0].credentials.walletId); - should.exist(c[1].credentials.walletId); - should.exist(c[2].credentials.walletId); - const recoveryClient = c[2]; - recoveryClient.openWallet(null, err => { + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.getMainAddresses({}, (err, list) => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.getMainAddresses({}, (err, list) => { - should.not.exist(err); - should.exist(list); - list[0].address.should.equal(addr.address); - done(); - }); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); }); - } - ); + }); + }).catch(done); }); }); }); @@ -7412,35 +7401,34 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { + ).then(({ key: k, clients: c }) => { + should.not.exist(err); + c.length.should.equal(7); + // check the following data on each of the clients + for (let i = 0; i < c.length; i++) { + c[i].credentials.coin.should.equal('btc'); + c[i].credentials.account.should.equal(i); + } + // make sure only one client has each copayerId + c.every(client => { + const copayerId = client.credentials.copayerId; + return c.filter(x => x.credentials.copayerId === copayerId).length === 1; + }).should.equal(true); + + + const recoveryClient = c[6]; + recoveryClient.openWallet(null, err => { should.not.exist(err); - c.length.should.equal(7); - // check the following data on each of the clients - for (let i = 0; i < c.length; i++) { - c[i].credentials.coin.should.equal('btc'); - c[i].credentials.account.should.equal(i); - } - // make sure only one client has each copayerId - c.every(client => { - const copayerId = client.credentials.copayerId; - return c.filter(x => x.credentials.copayerId === copayerId).length === 1; - }).should.equal(true); - - - const recoveryClient = c[6]; - recoveryClient.openWallet(null, err => { + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.getMainAddresses({}, (err, list) => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.getMainAddresses({}, (err, list) => { - should.not.exist(err); - should.exist(list); - list[0].address.should.equal(addr.address); - done(); - }); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); }); - } - ); + }); + }).catch(done); }); }); }); @@ -7465,38 +7453,37 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { + ).then(({ key: k, clients: c }) => { + should.not.exist(err); + c.length.should.equal(3); + c[0].credentials.coin.should.equal('eth'); + c[1].credentials.coin.should.equal('eth'); + c[2].credentials.coin.should.equal('eth'); + c[0].credentials.chain.should.equal('arb'); + c[1].credentials.chain.should.equal('arb'); + c[2].credentials.chain.should.equal('arb'); + c[0].credentials.account.should.equal(0); + c[1].credentials.account.should.equal(1); + c[2].credentials.account.should.equal(2); + c[0].credentials.copayerId.should.not.equal(c[1].credentials.copayerId); + c[0].credentials.copayerId.should.not.equal(c[2].credentials.copayerId); + c[1].credentials.copayerId.should.not.equal(c[2].credentials.copayerId); + should.exist(c[0].credentials.walletId); + should.exist(c[1].credentials.walletId); + should.exist(c[2].credentials.walletId); + const recoveryClient = c[2]; + recoveryClient.openWallet(null, err => { should.not.exist(err); - c.length.should.equal(3); - c[0].credentials.coin.should.equal('eth'); - c[1].credentials.coin.should.equal('eth'); - c[2].credentials.coin.should.equal('eth'); - c[0].credentials.chain.should.equal('arb'); - c[1].credentials.chain.should.equal('arb'); - c[2].credentials.chain.should.equal('arb'); - c[0].credentials.account.should.equal(0); - c[1].credentials.account.should.equal(1); - c[2].credentials.account.should.equal(2); - c[0].credentials.copayerId.should.not.equal(c[1].credentials.copayerId); - c[0].credentials.copayerId.should.not.equal(c[2].credentials.copayerId); - c[1].credentials.copayerId.should.not.equal(c[2].credentials.copayerId); - should.exist(c[0].credentials.walletId); - should.exist(c[1].credentials.walletId); - should.exist(c[2].credentials.walletId); - const recoveryClient = c[2]; - recoveryClient.openWallet(null, err => { + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.getMainAddresses({}, (err, list) => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.getMainAddresses({}, (err, list) => { - should.not.exist(err); - should.exist(list); - list[0].address.should.equal(addr.address); - done(); - }); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); }); - } - ); + }); + }).catch(done); }); }); }); @@ -7515,24 +7502,23 @@ describe('client API', function() { Client.serverAssistedImport( { words, passphrase, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - should.not.exist(err); - c.length.should.equal(1); + ).then(({ key: k, clients: c }) => { + should.not.exist(err); + c.length.should.equal(1); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { + should.not.exist(err); + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.getMainAddresses({}, (err, list) => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.getMainAddresses({}, (err, list) => { - should.not.exist(err); - should.exist(list); - list[0].address.should.equal(addr.address); - done(); - }); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); }); - } - ); + }); + }).catch(done); }); }); }); @@ -7548,28 +7534,27 @@ describe('client API', function() { Client.serverAssistedImport( { xPrivKey, includeTestnetWallets: true }, helpers.newClient(app), - (err, key, c) => { - const k = key.toObj(); - k.xPrivKey.should.equal(xPrivKey); - k.compliantDerivation.should.equal(true); - k.use0forBCH.should.equal(false); - k.use44forMultisig.should.equal(false); + ).then(({ key, clients: c }) => { + const k = key.toObj(); + k.xPrivKey.should.equal(xPrivKey); + k.compliantDerivation.should.equal(true); + k.use0forBCH.should.equal(false); + k.use44forMultisig.should.equal(false); + should.not.exist(err); + c.length.should.equal(1); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { should.not.exist(err); - c.length.should.equal(1); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.getMainAddresses({}, (err, list) => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.getMainAddresses({}, (err, list) => { - should.not.exist(err); - should.exist(list); - list[0].address.should.equal(addr.address); - done(); - }); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); }); - } - ); + }); + }).catch(done); }); }); }); @@ -7585,28 +7570,27 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - k.compliantDerivation.should.equal(true); - k.use0forBCH.should.equal(false); - k.use44forMultisig.should.equal(false); + ).then(({ key, clients: c }) => { + key.compliantDerivation.should.equal(true); + key.use0forBCH.should.equal(false); + key.use44forMultisig.should.equal(false); + should.not.exist(err); + c.length.should.equal(1); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { should.not.exist(err); - c.length.should.equal(1); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.m.should.equal(2); + recoveryClient.credentials.n.should.equal(2); + recoveryClient.getMainAddresses({}, (err, list) => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.m.should.equal(2); - recoveryClient.credentials.n.should.equal(2); - recoveryClient.getMainAddresses({}, (err, list) => { - should.not.exist(err); - should.exist(list); - list[0].address.should.equal(addr.address); - done(); - }); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); }); - } - ); + }); + }).catch(done); }); }); }); @@ -7618,12 +7602,13 @@ describe('client API', function() { Client.serverAssistedImport( { words }, helpers.newClient(app), - (err, k, c) => { - should.not.exist(err); - c.length.should.equal(0); - done(); - } - ); + ).then(({ key, clients: c }) => { + throw new Error('Should have thrown wallet not found error'); + }).catch((err) => { + should.exist(err); + err.should.be.an.instanceOf(Errors.WALLET_DOES_NOT_EXIST); + done(); + }); }); }); @@ -7646,31 +7631,30 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeLegacyWallets: true, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - should.exist(k); - should.exist(c[0]); - k.compliantDerivation.should.equal(true); - k.use0forBCH.should.equal(false); - k.use44forMultisig.should.equal(true); + ).then(({ key, clients: c }) => { + should.exist(key); + should.exist(c[0]); + key.compliantDerivation.should.equal(true); + key.use0forBCH.should.equal(false); + key.use44forMultisig.should.equal(true); + should.not.exist(err); + c.length.should.equal(1); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { should.not.exist(err); - c.length.should.equal(1); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.m.should.equal(2); + recoveryClient.credentials.n.should.equal(2); + recoveryClient.getMainAddresses({}, (err, list) => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.m.should.equal(2); - recoveryClient.credentials.n.should.equal(2); - recoveryClient.getMainAddresses({}, (err, list) => { - should.not.exist(err); - should.exist(list); - list[0].address.should.equal(addr.address); - done(); - }); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); }); - } - ); + }); + }).catch(done); }); } ); @@ -7696,31 +7680,30 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeLegacyWallets: true, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { - should.exist(k); - should.exist(c[0]); - k.compliantDerivation.should.equal(true); - k.use0forBCH.should.equal(false); - k.use44forMultisig.should.equal(true); + ).then(({ key, clients: c }) => { + should.exist(key); + should.exist(c[0]); + key.compliantDerivation.should.equal(true); + key.use0forBCH.should.equal(false); + key.use44forMultisig.should.equal(true); + should.not.exist(err); + c.length.should.equal(1); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { should.not.exist(err); - c.length.should.equal(1); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.credentials.copayerName.should.equal(copayerName); + recoveryClient.credentials.m.should.equal(2); + recoveryClient.credentials.n.should.equal(3); + recoveryClient.getMainAddresses({}, (err, list) => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.credentials.copayerName.should.equal(copayerName); - recoveryClient.credentials.m.should.equal(2); - recoveryClient.credentials.n.should.equal(3); - recoveryClient.getMainAddresses({}, (err, list) => { - should.not.exist(err); - should.exist(list); - list[0].address.should.equal(addr.address); - done(); - }); + should.exist(list); + list[0].address.should.equal(addr.address); + done(); }); - } - ); + }); + }).catch(done); }); } ); @@ -7728,12 +7711,13 @@ describe('client API', function() { it('should be able to restore with equal keyid an old bch wallet and an old multisig btc wallet', function(done) { const words = 'famous ship happy oyster retire sponsor disease friend parent wise grunt voyage'; - const k1 = new Key({ seedData: words, seedType: 'mnemonic', useLegacyCoinType: false, useLegacyPurpose: true }); // old bch wallets: /[44,48]/[0,0]'/ - const k2 = new Key({ seedData: words, seedType: 'mnemonic', useLegacyCoinType: true, useLegacyPurpose: false }); // old BTC/BCH multisig wallets: /[44]/[0,145]'/ - helpers.createAndJoinWallet(clients, keys, 2, 2, { key: k1, network: 'livenet' }, () => { - // first create a "old" bch wallet (coin = 0). - clients[1].fromString( - k2.createCredentials(null, { + const keyOldMultisig = new Key({ seedData: words, seedType: 'mnemonic', useLegacyCoinType: false, useLegacyPurpose: true }); // old BTC/BCH multisig wallets: m/44'/[0,145]'/ + const keyPreForkBch = new Key({ seedData: words, seedType: 'mnemonic', useLegacyCoinType: true, useLegacyPurpose: false }); // old BCH wallets: m/[44,48]'/0'/ + // first create old btc multisig wallet + helpers.createAndJoinWallet(clients, keys, 2, 2, { key: keyOldMultisig, network: 'livenet' }, () => { + // then create a pre-fork bch wallet (derivation path's 'coin' index = 0). + clients[1].fromObj( + keyPreForkBch.createCredentials(null, { coin: 'bch', network: 'livenet', account: 0, @@ -7754,15 +7738,15 @@ describe('client API', function() { Client.serverAssistedImport( { words, includeTestnetWallets: false, includeLegacyWallets: true }, helpers.newClient(app), - (err, k, c) => { - should.not.exist(err); - should.exist(k); - should.exist(c[0]); - should.exist(c[1]); - c[0].credentials.keyId.should.equal(c[1].credentials.keyId); - c.length.should.equal(2); - done(); - }); + ).then(({ key, clients: c }) => { + should.not.exist(err); + should.exist(key); + should.exist(c[0]); + should.exist(c[1]); + c[0].credentials.keyId.should.equal(c[1].credentials.keyId); + c.length.should.equal(2); + done(); + }).catch(done); }); }); }); @@ -7786,21 +7770,20 @@ describe('client API', function() { Client.serverAssistedImport( { xPrivKey, includeTestnetWallets: true }, helpers.newClient(app), - (err, k, c) => { + ).then(({ key, clients: c }) => { + should.not.exist(err); + c.length.should.equal(1); + const recoveryClient = c[0]; + recoveryClient.openWallet(null, err => { should.not.exist(err); - c.length.should.equal(1); - const recoveryClient = c[0]; - recoveryClient.openWallet(null, err => { + recoveryClient.credentials.walletName.should.equal(walletName); + recoveryClient.getTx(x.id, (err, x2) => { should.not.exist(err); - recoveryClient.credentials.walletName.should.equal(walletName); - recoveryClient.getTx(x.id, (err, x2) => { - should.not.exist(err); - x2.message.should.equal(opts.message); - done(); - }); + x2.message.should.equal(opts.message); + done(); }); - } - ); + }); + }).catch(done); }); }); }); From 925d9babc29d53cc1c01f87705f3d7a6ef8b0fd3 Mon Sep 17 00:00:00 2001 From: Kenny Joseph Date: Thu, 19 Mar 2026 15:12:21 -0400 Subject: [PATCH 3/3] fix bwc types --- packages/bitcore-wallet-client/src/lib/api.ts | 2 +- ...rAssistedImportEvents.d.ts => serverAssistedImportEvents.ts} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/bitcore-wallet-client/src/types/{serverAssistedImportEvents.d.ts => serverAssistedImportEvents.ts} (99%) diff --git a/packages/bitcore-wallet-client/src/lib/api.ts b/packages/bitcore-wallet-client/src/lib/api.ts index 9f69d1cb157..63406a96b94 100644 --- a/packages/bitcore-wallet-client/src/lib/api.ts +++ b/packages/bitcore-wallet-client/src/lib/api.ts @@ -17,7 +17,7 @@ import { PayPro } from './paypro'; import { PayProV2 } from './payproV2'; import { Request } from './request'; import { Verifier } from './verifier'; -import type { ServerAssistedImportEvents } from 'src/types/serverAssistedImportEvents'; +import type { ServerAssistedImportEvents } from '../types/serverAssistedImportEvents'; const $ = singleton(); diff --git a/packages/bitcore-wallet-client/src/types/serverAssistedImportEvents.d.ts b/packages/bitcore-wallet-client/src/types/serverAssistedImportEvents.ts similarity index 99% rename from packages/bitcore-wallet-client/src/types/serverAssistedImportEvents.d.ts rename to packages/bitcore-wallet-client/src/types/serverAssistedImportEvents.ts index c2bbf9a7b07..60bd4ad3b5c 100644 --- a/packages/bitcore-wallet-client/src/types/serverAssistedImportEvents.d.ts +++ b/packages/bitcore-wallet-client/src/types/serverAssistedImportEvents.ts @@ -3,7 +3,7 @@ import type { Key } from '../lib/key'; import type { EventEmitter } from 'events'; -export class ServerAssistedImportEvents extends EventEmitter { +export declare class ServerAssistedImportEvents extends EventEmitter { /** Total number of key configurations to be checked (not all configurations will necessarily be processed, as the process may exit early if wallets are found) */ on(event: 'keyConfig.count', listener: (count: number) => void): this; /** Index of the current key configuration being processed */