UNPKG

edge-core-js

Version:

Edge account & wallet management library

236 lines (204 loc) 7.22 kB
import { asMaybe } from 'cleaners' import { base64 } from 'rfc4648' import { hmacSha256 } from '../../util/crypto/hashes' import { utf8 } from '../../util/encoding' import { changeWalletStates } from '../account/account-files' import { waitForCurrencyWallet } from '../currency/currency-selectors' import { applyKit } from '../login/login' import { getCurrencyTools, maybeFindCurrencyPluginId } from '../plugins/plugins-selectors' import { makeKeysKit } from './keys' import { asEdgeStorageKeys, wasEdgeStorageKeys } from './storage-keys' export async function listSplittableWalletTypes( ai, accountId, walletId ) { const { allWalletInfosFull } = ai.props.state.accounts[accountId] // Find the wallet we are going to split: const walletInfo = allWalletInfosFull.find( walletInfo => walletInfo.id === walletId ) if (walletInfo == null) throw new Error(`Invalid wallet id ${walletId}`) const pluginId = maybeFindCurrencyPluginId( ai.props.state.plugins.currency, walletInfo.type ) if (pluginId == null) return [] // Get the list of available types: const tools = await getCurrencyTools(ai, pluginId) if (tools.getSplittableTypes == null) return [] const types = await tools.getSplittableTypes(walletInfo) // Filter out wallet types we have already split: return types.filter(type => { const newWalletInfo = makeSplitWalletInfo(walletInfo, type) const existingWalletInfo = allWalletInfosFull.find( walletInfo => walletInfo.id === newWalletInfo.id ) // We can split the wallet if it doesn't exist, or is deleted: return ( existingWalletInfo == null || existingWalletInfo.archived || existingWalletInfo.deleted ) }) } export function makeSplitWalletInfo( walletInfo, newWalletType ) { const { id, type, keys } = walletInfo const cleanKeys = asMaybe(asEdgeStorageKeys)(keys) if (cleanKeys == null) { throw new Error(`Wallet ${id} is not a splittable type`) } const { dataKey, syncKey } = cleanKeys const xorKey = xorData( hmacSha256(utf8.parse(type), dataKey), hmacSha256(utf8.parse(newWalletType), dataKey) ) // Fix the id: const newWalletId = xorData(base64.parse(id), xorKey) const newSyncKey = xorData(syncKey, xorKey.subarray(0, syncKey.length)) // Fix the keys: const networkName = type.replace(/wallet:/, '').replace('-', '') const newNetworkName = newWalletType.replace(/wallet:/, '').replace('-', '') const newKeys = wasEdgeStorageKeys({ dataKey, syncKey: newSyncKey }) for (const key of Object.keys(keys)) { const newKey = key === networkName + 'Key' ? newNetworkName + 'Key' : key if (newKeys[newKey] != null) continue newKeys[newKey] = keys[key] } return { id: base64.stringify(newWalletId), keys: newKeys, type: newWalletType } } export async function splitWalletInfo( ai, accountId, walletId, newWalletType ) { const accountState = ai.props.state.accounts[accountId] const { allWalletInfosFull, sessionKey } = accountState // Find the wallet we are going to split: const walletInfo = allWalletInfosFull.find( walletInfo => walletInfo.id === walletId ) if (walletInfo == null) throw new Error(`Invalid wallet id ${walletId}`) // Handle BCH / BTC+segwit special case: if ( newWalletType === 'wallet:bitcoincash' && walletInfo.type === 'wallet:bitcoin' && walletInfo.keys.format === 'bip49' ) { throw new Error( 'Cannot split segwit-format Bitcoin wallets to Bitcoin Cash' ) } // Handle BitcoinABC/SV replay protection: const needsProtection = newWalletType === 'wallet:bitcoinsv' && walletInfo.type === 'wallet:bitcoincash' if (needsProtection) { const oldWallet = ai.props.output.currency.wallets[walletId].walletApi if (oldWallet == null) throw new Error('Missing Wallet') await protectBchWallet(oldWallet) } // See if the wallet has already been split: const newWalletInfo = makeSplitWalletInfo(walletInfo, newWalletType) const existingWalletInfo = allWalletInfosFull.find( walletInfo => walletInfo.id === newWalletInfo.id ) if (existingWalletInfo != null) { if (existingWalletInfo.archived || existingWalletInfo.deleted) { // Simply undelete the existing wallet: const walletInfos = {} walletInfos[newWalletInfo.id] = { archived: false, deleted: false, migratedFromWalletId: undefined } await changeWalletStates(ai, accountId, walletInfos) return walletInfo.id } if (needsProtection) return newWalletInfo.id throw new Error('This wallet has already been split') } // Add the keys to the login: const kit = makeKeysKit(ai, sessionKey, [newWalletInfo]) await applyKit(ai, sessionKey, kit) // Try to copy metadata on a best-effort basis. // In the future we should clone the repo instead: try { const wallet = await waitForCurrencyWallet(ai, newWalletInfo.id) const oldWallet = ai.props.output.currency.wallets[walletId].walletApi if (oldWallet != null) { if (oldWallet.name != null) await wallet.renameWallet(oldWallet.name) if (oldWallet.fiatCurrencyCode != null) { await wallet.setFiatCurrencyCode(oldWallet.fiatCurrencyCode) } } } catch (error) { ai.props.onError(error) } return newWalletInfo.id } async function protectBchWallet(wallet) { const bchCurrency = { currencyCode: 'BCH', tokenId: null } // Create a UTXO which can be spend only on the ABC network const spendInfoSplit = { ...bchCurrency, spendTargets: [ { nativeAmount: '10000', otherParams: { script: { type: 'replayProtection' } }, publicAddress: '' } ], metadata: {}, networkFeeOption: 'high' } const splitTx = await wallet.makeSpend(spendInfoSplit) const signedSplitTx = await wallet.signTx(splitTx) const broadcastedSplitTx = await wallet.broadcastTx(signedSplitTx) await wallet.saveTx(broadcastedSplitTx) // Taint the rest of the wallet using the UTXO from before const { publicAddress } = await wallet.getReceiveAddress(bchCurrency) const spendInfoTaint = { ...bchCurrency, metadata: { name: 'Replay Protection Tx', notes: 'This transaction is to protect your BCH wallet from unintentionally spending BSV funds. Please wait for the transaction to confirm before making additional transactions using this BCH wallet.' }, networkFeeOption: 'high', spendTargets: [{ publicAddress, nativeAmount: '0' }] } const maxAmount = await wallet.getMaxSpendable(spendInfoTaint) spendInfoTaint.spendTargets[0].nativeAmount = maxAmount const taintTx = await wallet.makeSpend(spendInfoTaint) const signedTaintTx = await wallet.signTx(taintTx) const broadcastedTaintTx = await wallet.broadcastTx(signedTaintTx) await wallet.saveTx(broadcastedTaintTx) } /** * Combines two byte arrays via the XOR operation. */ function xorData(a, b) { if (a.length !== b.length) { throw new Error(`Array lengths do not match: ${a.length}, ${b.length}`) } const out = new Uint8Array(a.length) for (let i = 0; i < a.length; ++i) { out[i] = a[i] ^ b[i] } return out }