UNPKG

scpx-wallet

Version:

Scoop Core Wallet: dual-signature timelock crypto wallet - multi-asset, cross-platform and open-source

822 lines (720 loc) 42.5 kB
// Distributed under AGPLv3 license: see /LICENSE for terms. Copyright 2019-2021 Dominic Morris. const Buffer = require('buffer').Buffer const _ = require('lodash') const bip32 = require('bip32') const bitgoUtxoLib = require('bitgo-utxo-lib') const bitcoinJsLib = require('bitcoinjs-lib') const ethereumJsUtil = require('ethereumjs-util') const bchAddr = require('bchaddrjs') const apiDataContract = require('../api/data-contract') const actionsWallet = require('.') const walletValidate = require('./wallet-validation') const configWallet = require('../config/wallet') const utilsWallet = require('../utils') module.exports = { // // adds (does not persist) a single dynamic (non-standard derivation) address into the singleton "non-standard" account (no HD deriv. path) // specifically, adds a P2SH(P2WPSH(DSIG/CLTV))-derived address // addNonStdAddress_DsigCltv: async (p) => { var { store, apk, h_mpk, assetName, dsigCltvP2sh_addr_txid, // required - browser & server userAccountName, e_email, // required - browser eosActiveWallet } = p // validation if (!store) throw 'store is required' if (!apk) throw 'apk is required' if (!assetName) throw 'assetName is required' if (!h_mpk) throw 'h_mpk is required' if (!dsigCltvP2sh_addr_txid) throw 'dsigCltvP2sh_addr_txid[] required' // { nonStdAddr, protect_op_txid } if (configWallet.WALLET_ENV === "BROWSER") { if (!userAccountName) throw 'userAccountName is required' if (!e_email) throw 'e_email is required' } const storeState = store.getState() if (!storeState || !storeState.wallet || !storeState.wallet.assets || !storeState.wallet.assetsRaw) throw 'Invalid store state' const wallet = storeState.wallet const e_rawAssets = storeState.wallet.assetsRaw const displayableAssets = wallet.assets if (dsigCltvP2sh_addr_txid.length == 0) { utilsWallet.log(`addNonStdAddress_DsigCltv - no unspent non-std addr's supplied; nop.`) return } utilsWallet.logMajor('green','white', `addNonStdAddress_DsigCltv... dsigCltvP2sh_addr_txid=`, dsigCltvP2sh_addr_txid, { logServerConsole: true }) // decrypt raw assets var pt_rawAssets = utilsWallet.aesDecryption(apk, h_mpk, e_rawAssets) if (!pt_rawAssets) { console.warn(`addNonStdAddress_DsigCltv - failed decrypting e_rawAssets; probably using stale store/mpk combination from previous wallet load - aborting.`) return } var rawAssets = JSON.parse(pt_rawAssets) if (!rawAssets) throw 'null rawAssets' var genAsset = rawAssets[assetName.toLowerCase()] try { // get asset if (genAsset === undefined || !genAsset.accounts || genAsset.accounts.length == 0) throw 'Invalid assetName' const meta = configWallet.walletsMeta[assetName.toLowerCase()] const genSymbol = meta.symbol var addedCount = 0 dsigCltvP2sh_addr_txid.forEach(addr_txid => { // validate if (!walletValidate.validateAssetAddress({ testSymbol: genSymbol, testAddressType: meta.addressType, validateAddr: addr_txid.nonStdAddr })) { throw 'invalid nonStdAddr' } }) // make HD account for non-standard addresses, if not already existing var nonStdAccount = genAsset.accounts.find(p => p.nonStd) if (!nonStdAccount) { nonStdAccount = { // new non-std account nonStd: true, name: `Protected ${meta.displayName}`, privKeys: [] // we sign dsigCltv addresses with keys of the csvSpender ("beneficiary"), or of the nonCsvSpender ("benefactor") } genAsset.accounts.push(nonStdAccount) } var rawAssetsJsonUpdated = JSON.stringify(rawAssets, null, 4) const e_rawAssetsUpdated = utilsWallet.aesEncryption(apk, h_mpk, rawAssetsJsonUpdated) store.dispatch({ type: actionsWallet.WCORE_SET_ASSETS_RAW, payload: e_rawAssetsUpdated }) // update local persisted raw assets rawAssetsJsonUpdated = null // add to displayable asset addresses const newDisplayableAssets = _.cloneDeep(displayableAssets) dsigCltvP2sh_addr_txid.forEach(addr_txid => { if (displayableAssets.find(p => p.symbol === genSymbol).addresses.some(p => p.addr === addr_txid.nonStdAddr) === false) { var newDisplayableAsset = newDisplayableAssets.find(p => { return p.symbol === genSymbol }) // no true HD for these non-std (dynamic) addresses (their presence/addition depends on specific scanned TX's) // but for deterministic sorting in the wallet, we allocate a pseudo-HD path ('p_' prefix, for "protected") const chainNdx = 0 // bip44: 0=external chain, 1=internal chain (change addresses) const accountNdx = 0 // only every one '_p' protection account const i = newDisplayableAsset.addresses.filter(p => p.path.startsWith('~p')).length const path = `~p/44'/${meta.bip44_index}'/${accountNdx}'/${chainNdx}/${i}` const newDisplayableAddr = module.exports.newWalletAddressFromPrivKey( { assetName: assetName.toLowerCase(), accountName: nonStdAccount.name, isNonStdAddr: nonStdAccount.nonStd, nonStd_protectOp_txid: addr_txid.protect_op_txid, key: { path }, eosActiveWallet: eosActiveWallet, knownAddr: addr_txid.nonStdAddr, symbol: newDisplayableAsset.symbol }) newDisplayableAsset.addresses.push(newDisplayableAddr) newDisplayableAsset.addresses = sortAddresses(newDisplayableAsset.addresses) addedCount++ utilsWallet.getAppWorker().postMessageWrapped({ msg: 'REFRESH_ASSET_BALANCE', data: { asset: newDisplayableAsset, wallet } }) } else { utilsWallet.log(`addNonStdAddress_DsigCltv - supplied address ${addr_txid.nonStdAddr} already added`, null, { logServerConsole: true }) } }) store.dispatch({ type: actionsWallet.WCORE_SET_ASSETS, payload: { assets: newDisplayableAssets, owner: userAccountName } }) // update addr monitors & refresh balance utilsWallet.getAppWorker().postMessageWrapped({ msg: 'DISCONNECT_ADDRESS_MONITORS', data: { wallet } }) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'CONNECT_ADDRESS_MONITORS', data: { wallet } }) // ret ok return { addedCount, accountName: nonStdAccount.name } } finally { utilsWallet.softNuke(rawAssets) utilsWallet.softNuke(genAsset) pt_rawAssets = null } }, // // add new receive address (in the primary account) // generateNewStandardAddress: async (p) => { const { store, apk, h_mpk, assetName, // required - browser & server userAccountName, e_email, // required - browser eosActiveWallet } = p // validation if (!store) throw 'store is required' if (!apk) throw 'apk is required' if (!store) throw 'store is required' if (!assetName) throw 'assetName is required' if (!h_mpk) throw 'h_mpk is required' if (configWallet.WALLET_ENV === "BROWSER") { if (!userAccountName) throw 'userAccountName is required' if (!e_email) throw 'e_email is required' } const storeState = store.getState() if (!storeState || !storeState.wallet || !storeState.wallet.assets || !storeState.wallet.assetsRaw) throw 'Invalid store state' const wallet = storeState.wallet const e_rawAssets = storeState.wallet.assetsRaw const displayableAssets = wallet.assets utilsWallet.logMajor('green','white', `generateNewStandardAddress...`, null, { logServerConsole: true }) // decrypt raw assets var pt_rawAssets = utilsWallet.aesDecryption(apk, h_mpk, e_rawAssets) var rawAssets = JSON.parse(pt_rawAssets) var genAsset = rawAssets[assetName.toLowerCase()] try { // get asset and account to generate into if (genAsset === undefined || !genAsset.accounts || genAsset.accounts.length == 0) throw 'Invalid assetName' const meta = configWallet.walletsMeta[assetName.toLowerCase()] const genSymbol = meta.symbol const genAccount = genAsset.accounts[0] // default (Scoop) account // generate new address var newPrivKey switch (meta.type) { case configWallet.WALLET_TYPE_UTXO: const leafPathValues = genAccount.privKeys.map(p => Number(p.path.split('/').pop())) var newAddrNdx = -1 for (var i=0; i <= leafPathValues.length; i++) { if (!leafPathValues.includes(i)) { newAddrNdx = i break } } if (newAddrNdx == -1) throw 'Unexpected path values' newPrivKey = module.exports.generateUtxoBip44Wifs({ entropySeed: h_mpk, symbol: genSymbol, addrNdx: newAddrNdx, genCount: 1 })[0] break case configWallet.WALLET_TYPE_ACCOUNT: if (genSymbol === 'EOS') { ; } // nop else if (meta.addressType === configWallet.ADDRESS_TYPE_ETH) { // including erc20 newPrivKey = module.exports.generateEthereumWallet({ entropySeed: h_mpk, addrNdx: genAccount.privKeys.length, genCount: 1 })[0] } break } if (newPrivKey) { // add new priv key (assets raw) genAccount.privKeys.push(newPrivKey) var rawAssetsJsonUpdated = JSON.stringify(rawAssets, null, 4) const e_rawAssetsUpdated = utilsWallet.aesEncryption(apk, h_mpk, rawAssetsJsonUpdated) store.dispatch({ type: actionsWallet.WCORE_SET_ASSETS_RAW, payload: e_rawAssetsUpdated }) rawAssetsJsonUpdated = null // post to server if (userAccountName && configWallet.WALLET_ENV === "BROWSER") { await apiDataContract.updateAssetsJsonApi({ owner: userAccountName, encryptedAssetsJSONRaw: module.exports.encryptPrunedAssets(rawAssets, apk, h_mpk), e_email: e_email, showNotification: true }) } // add new displayable asset address object const newDisplayableAssets = _.cloneDeep(displayableAssets) const newDisplayableAsset = newDisplayableAssets.find(p => { return p.symbol === genSymbol }) const newDisplayableAddr = module.exports.newWalletAddressFromPrivKey( { assetName: assetName.toLowerCase(), accountName: genAccount.name, key: newPrivKey, eosActiveWallet: eosActiveWallet, knownAddr: undefined, symbol: newDisplayableAsset.symbol }) newDisplayableAsset.addresses.push(newDisplayableAddr) newDisplayableAsset.addresses = sortAddresses(newDisplayableAsset.addresses) //_.sortBy(newDisplayableAsset.addresses, ['path']) store.dispatch({ type: actionsWallet.WCORE_SET_ASSETS, payload: { assets: newDisplayableAssets, owner: userAccountName } }) if (configWallet.WALLET_ENV === "BROWSER") { const globalScope = utilsWallet.getMainThreadGlobalScope() const appWorker = globalScope.appWorker } // update addr monitors & refresh balance utilsWallet.getAppWorker().postMessageWrapped({ msg: 'DISCONNECT_ADDRESS_MONITORS', data: { wallet } }) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'CONNECT_ADDRESS_MONITORS', data: { wallet } }) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'REFRESH_ASSET_BALANCE', data: { asset: newDisplayableAsset, wallet } }) // ret ok utilsWallet.logMajor('green','white', `generateNewStandardAddress - complete`, null, { logServerConsole: true }) return { newAddr: newDisplayableAddr, newCount: genAccount.privKeys.length } } else { // ret fail return { err: 'Failed to generate private key', newAddr: undefined } } } finally { utilsWallet.softNuke(rawAssets) utilsWallet.softNuke(genAsset) pt_rawAssets = null } }, deleteUnusedStandardAddress: async (p) => { const { store, apk, h_mpk, assetName, deleteAddr, // required - browser & server userAccountName, e_email, // required - browser eosActiveWallet } = p // validation if (!store) throw 'store is required' if (!apk) throw 'apk is required' if (!store) throw 'store is required' if (!assetName) throw 'assetName is required' if (!deleteAddr) throw 'deleteAddr is required' if (!h_mpk) throw 'h_mpk is required' if (configWallet.WALLET_ENV === "BROWSER") { if (!userAccountName) throw 'userAccountName is required' if (!e_email) throw 'e_email is required' } const storeState = store.getState() if (!storeState || !storeState.wallet || !storeState.wallet.assets || !storeState.wallet.assetsRaw) throw 'Invalid store state' const wallet = storeState.wallet const e_rawAssets = storeState.wallet.assetsRaw const displayableAssets = wallet.assets utilsWallet.logMajor('green','white', `deleteUnusedStandardAddress ${deleteAddr}...`, null, { logServerConsole: true }) // decrypt raw assets var pt_rawAssets = utilsWallet.aesDecryption(apk, h_mpk, e_rawAssets) var rawAssets = JSON.parse(pt_rawAssets) var genAsset = rawAssets[assetName.toLowerCase()] var genAccount try { // get gen-asset if (genAsset === undefined || !genAsset.accounts || genAsset.accounts.length == 0) throw 'Invalid assetName' const meta = configWallet.walletsMeta[assetName.toLowerCase()] const genSymbol = meta.symbol // get store-asset; this has the TX & UTXO appended to it const storeAsset = displayableAssets.find(p => { return p.symbol === genSymbol }) genAccount = genAsset.accounts.find(p => !p.nonStd && !p.imported) const storePrimaryAddresses = storeAsset.addresses.filter(p => p.accountName == genAccount.name) // get prune address(es) // const storePruneAddresses = storePrimaryAddresses.filter(p => // prune all unused addresses // p.lastAddrFetchAt !== undefined && // p.totalTxCount == 0 && p.txs.length == 0 && p.utxos.length == 0 && // p.balance == 0 && p.unconfirmedBalance == 0 // ) const storePruneAddresses = storePrimaryAddresses.filter(p => p.addr == deleteAddr && // refactored to prune a singular addresses; fits UX better p.lastAddrFetchAt !== undefined && p.totalTxCount == 0 && p.txs.length == 0 && p.utxos.length == 0 && p.balance == 0 && p.unconfirmedBalance == 0 ) //utilsWallet.log(`deleteUnusedStandardAddress - storePruneAddresses=`, storePruneAddresses) // raw assets: remove prune-addresses, associated privKeys & update local persisted copy genAccount.privKeys = genAccount.privKeys.filter(p => storePruneAddresses.map(p2 => p2.path).includes(p.path) == false) genAsset.addresses = genAsset.addresses.filter(p => storePruneAddresses.map(p2 => p2.addr).includes(p.addr) == false) var rawAssetsJsonUpdated = JSON.stringify(rawAssets, null, 4) const e_rawAssetsUpdated = utilsWallet.aesEncryption(apk, h_mpk, rawAssetsJsonUpdated) store.dispatch({ type: actionsWallet.WCORE_SET_ASSETS_RAW, payload: e_rawAssetsUpdated }) rawAssetsJsonUpdated = null // displayableAssets: remove specified unused addresses const newDisplayableAssets = _.cloneDeep(displayableAssets) const newDisplayableAsset = newDisplayableAssets.find(p => { return p.symbol === genSymbol }) const countRemoved = newDisplayableAsset.addresses.filter(p => storePruneAddresses.map(p2 => p2.addr).includes(p.addr) == true).length newDisplayableAsset.addresses = newDisplayableAsset.addresses.filter(p => storePruneAddresses.map(p2 => p2.addr).includes(p.addr) == false) store.dispatch({ type: actionsWallet.WCORE_SET_ASSETS, payload: { assets: newDisplayableAssets, owner: userAccountName } }) if (userAccountName && configWallet.WALLET_ENV === "BROWSER") { await apiDataContract.updateAssetsJsonApi({ // raw assets: post encrypted owner: userAccountName, encryptedAssetsJSONRaw: module.exports.encryptPrunedAssets(rawAssets, apk, h_mpk), e_email: e_email, showNotification: true }) } // update addr monitors & refresh balance utilsWallet.getAppWorker().postMessageWrapped({ msg: 'DISCONNECT_ADDRESS_MONITORS', data: { wallet } }) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'CONNECT_ADDRESS_MONITORS', data: { wallet } }) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'REFRESH_ASSET_BALANCE', data: { asset: newDisplayableAsset, wallet } }) // ret ok utilsWallet.logMajor('green','white', `deleteUnusedStandardAddress - complete, countRemoved=`, countRemoved, { logServerConsole: true }) return { countRemoved } } finally { utilsWallet.softNuke(genAccount) utilsWallet.softNuke(rawAssets) utilsWallet.softNuke(genAsset) pt_rawAssets = null } }, // // imports (& persists) external privkeys into a new "import" account (non-std HD deriv path: "i/...") // & remove imported accounts (all addresses in each import) // importPrivKeys: async (p) => { var { store, apk, h_mpk, assetName, addrKeyPairs, // required - browser & server userAccountName, e_email, // required - browser eosActiveWallet } = p // validation if (!store) throw 'store is required' if (!apk) throw 'apk is required' if (!assetName) throw 'assetName is required' if (!h_mpk) throw 'h_mpk is required' if (!addrKeyPairs || addrKeyPairs.length == 0) throw 'addrKeyPairs required' if (configWallet.WALLET_ENV === "BROWSER") { if (!userAccountName) throw 'userAccountName is required' if (!e_email) throw 'e_email is required' } const storeState = store.getState() if (!storeState || !storeState.wallet || !storeState.wallet.assets || !storeState.wallet.assetsRaw) throw 'Invalid store state' const wallet = storeState.wallet const e_rawAssets = storeState.wallet.assetsRaw const displayableAssets = wallet.assets utilsWallet.logMajor('green','white', `importPrivKeys...`, null, { logServerConsole: true }) // decrypt raw assets var pt_rawAssets = utilsWallet.aesDecryption(apk, h_mpk, e_rawAssets) var rawAssets = JSON.parse(pt_rawAssets) var genAsset = rawAssets[assetName.toLowerCase()] try { // get asset if (genAsset === undefined || !genAsset.accounts || genAsset.accounts.length == 0) throw 'Invalid assetName' const meta = configWallet.walletsMeta[assetName.toLowerCase()] const genSymbol = meta.symbol // remove already imported var existingPrivKeys = [] genAsset.accounts.forEach(account => { existingPrivKeys = existingPrivKeys.concat(account.privKeys) }) addrKeyPairs = addrKeyPairs.filter(toImport => !existingPrivKeys.some(existing => existing.privKey === toImport.privKey)) if (addrKeyPairs.length == 0) { utilsWallet.warn(`All supplied keys already imported`, null, { logServerConsole: true }) return { importedAddrCount: 0 } } // make new HD account for import const existingImports = genAsset.importCount || 0 //genAsset.accounts.length - 1 // first account is default Scoop addresses const importAccount = { // new import account imported: true, name: `Import #${existingImports+1} ${meta.displayName}`, privKeys: [] } genAsset.accounts.push(importAccount) const accountNdx = existingImports + 1 // imported accounts start at our HD index 1 (scoop default is 0) genAsset.importCount = accountNdx // map raw suplied priv keys to our internal format; note -- there is no "real" HD path for imported keys (they're not derived keys) // we use custom path prefix '~i' for imported to denote this const privKeys = [] for (var i=0 ; i < addrKeyPairs.length ; i++) { const privKey = addrKeyPairs[i].privKey var chainNdx = 0 // bip44: 0=external chain, 1=internal chain (change addresses) privKeys.push({ privKey, path: `~i/44'/${meta.bip44_index}'/${accountNdx}'/${chainNdx}/${i}` }) } // add new priv keys privKeys.forEach(privKey => { importAccount.privKeys.push(privKey) }) // update local persisted raw assets var rawAssetsJsonUpdated = JSON.stringify(rawAssets, null, 4) const e_rawAssetsUpdated = utilsWallet.aesEncryption(apk, h_mpk, rawAssetsJsonUpdated) store.dispatch({ type: actionsWallet.WCORE_SET_ASSETS_RAW, payload: e_rawAssetsUpdated }) rawAssetsJsonUpdated = null // add to displayable asset addresses - this fails inside .then() below; no idea why const newDisplayableAssets = _.cloneDeep(displayableAssets) const newDisplayableAsset = newDisplayableAssets.find(p => { return p.symbol === genSymbol }) for (var i=0 ; i < addrKeyPairs.length ; i++) { const addr = addrKeyPairs[i].addr var newDisplayableAddr = module.exports.newWalletAddressFromPrivKey( { assetName: assetName.toLowerCase(), accountName: importAccount.name, key: privKeys.find(p => p.privKey == addrKeyPairs[i].privKey), eosActiveWallet: eosActiveWallet, knownAddr: addr, symbol: newDisplayableAsset.symbol }) if (newDisplayableAddr.addr === null) { return { err: "Invalid private key" } } newDisplayableAsset.addresses.push(newDisplayableAddr) newDisplayableAsset.addresses = sortAddresses(newDisplayableAsset.addresses) //_.sortBy(newDisplayableAsset.addresses, ['path']) } store.dispatch({ type: actionsWallet.WCORE_SET_ASSETS, payload: { assets: newDisplayableAssets, owner: userAccountName } }) if (userAccountName && configWallet.WALLET_ENV === "BROWSER") { // raw assets: post encrypted await apiDataContract.updateAssetsJsonApi({ owner: userAccountName, encryptedAssetsJSONRaw: module.exports.encryptPrunedAssets(rawAssets, apk, h_mpk), e_email: e_email, showNotification: true }) } // update addr monitors & refresh balance utilsWallet.getAppWorker().postMessageWrapped({ msg: 'DISCONNECT_ADDRESS_MONITORS', data: { wallet } }) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'CONNECT_ADDRESS_MONITORS', data: { wallet } }) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'REFRESH_ASSET_BALANCE', data: { asset: newDisplayableAsset, wallet } }) // ret ok utilsWallet.logMajor('green','white', `importPrivKeys - complete`, addrKeyPairs.length, { logServerConsole: true }) return { importedAddrCount: privKeys.length, accountName: importAccount.name } } finally { utilsWallet.softNuke(rawAssets) utilsWallet.softNuke(genAsset) pt_rawAssets = null } }, removeImportedAccounts: async (p) => { var { store, apk, h_mpk, assetName, removeAccounts, // required - browser & server userAccountName, e_email, // required - browser eosActiveWallet } = p // validation if (!store) throw 'store is required' if (!apk) throw 'apk is required' if (!assetName) throw 'assetName is required' if (!h_mpk) throw 'h_mpk is required' if (!removeAccounts || removeAccounts.length == 0) throw 'removeAccounts required' if (configWallet.WALLET_ENV === "BROWSER") { if (!userAccountName) throw 'userAccountName is required' if (!e_email) throw 'e_email is required' } const storeState = store.getState() if (!storeState || !storeState.wallet || !storeState.wallet.assets || !storeState.wallet.assetsRaw) throw 'Invalid store state' const wallet = storeState.wallet const e_rawAssets = storeState.wallet.assetsRaw const displayableAssets = wallet.assets utilsWallet.logMajor('green','white', `removeImportedAccounts...`, removeAccounts, { logServerConsole: true }) // decrypt raw assets var pt_rawAssets = utilsWallet.aesDecryption(apk, h_mpk, e_rawAssets) var rawAssets = JSON.parse(pt_rawAssets) var genAsset = rawAssets[assetName.toLowerCase()] try { // get asset if (genAsset === undefined || !genAsset.accounts || genAsset.accounts.length == 0) throw 'Invalid assetName' const meta = configWallet.walletsMeta[assetName.toLowerCase()] const genSymbol = meta.symbol // remove internal scoop accounts - we only remove externally imported accounts const importedAccountNames = genAsset.accounts.filter(p => p.imported == true).map(p => p.name) removeAccounts = removeAccounts.filter(p => importedAccountNames.some(p2 => p2 === p)) if (removeAccounts == 0) { utilsWallet.warn(`No import accounts to remove`, null, { logServerConsole: true }) return { removedAddrCount: 0, removedAccountCount: 0 } } // raw assets: remove specified accounts & addresses const removedAccountCount = genAsset.accounts.filter(p => removeAccounts.some(p2 => p2 === p.name) === true).length genAsset.accounts = genAsset.accounts.filter(p => removeAccounts.some(p2 => p2 === p.name) === false) genAsset.addresses = genAsset.addresses.filter(p => removeAccounts.some(p2 => p2 === p.accountName) === false) // raw assets: update local persisted copy var rawAssetsJsonUpdated = JSON.stringify(rawAssets, null, 4) const e_rawAssetsUpdated = utilsWallet.aesEncryption(apk, h_mpk, rawAssetsJsonUpdated) store.dispatch({ type: actionsWallet.WCORE_SET_ASSETS_RAW, payload: e_rawAssetsUpdated }) rawAssetsJsonUpdated = null // displayableAssets: remove specified accounts & addresses const newDisplayableAssets = _.cloneDeep(displayableAssets) const newDisplayableAsset = newDisplayableAssets.find(p => { return p.symbol === genSymbol }) const removedAddrCount = newDisplayableAsset.addresses.filter(p => removeAccounts.some(p2 => p2 === p.accountName) === true).length newDisplayableAsset.addresses = newDisplayableAsset.addresses.filter(p => removeAccounts.some(p2 => p2 === p.accountName) === false) store.dispatch({ type: actionsWallet.WCORE_SET_ASSETS, payload: { assets: newDisplayableAssets, owner: userAccountName } }) if (userAccountName && configWallet.WALLET_ENV === "BROWSER") { await apiDataContract.updateAssetsJsonApi({ // raw assets: post encrypted owner: userAccountName, encryptedAssetsJSONRaw: module.exports.encryptPrunedAssets(rawAssets, apk, h_mpk), e_email: e_email, showNotification: true }) } // update addr monitors & refresh balance utilsWallet.getAppWorker().postMessageWrapped({ msg: 'DISCONNECT_ADDRESS_MONITORS', data: { wallet } }) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'CONNECT_ADDRESS_MONITORS', data: { wallet } }) utilsWallet.getAppWorker().postMessageWrapped({ msg: 'REFRESH_ASSET_BALANCE', data: { asset: newDisplayableAsset, wallet } }) // ret ok utilsWallet.logMajor('green','white', `removeImportedAccounts - complete`, removedAddrCount, { logServerConsole: true }) return { removedAddrCount, removedAccountCount } } finally { utilsWallet.softNuke(rawAssets) utilsWallet.softNuke(genAsset) pt_rawAssets = null } }, // // address generation // newWalletAddressFromPrivKey: (p) => { const { assetName, accountName, key, eosActiveWallet, knownAddr, symbol, isNonStdAddr, nonStd_protectOp_txid } = p const network = module.exports.getUtxoNetwork(symbol) //console.log(`newWalletAddressFromPrivKey, symbol=${symbol}, assetName=${assetName} configWallet.walletsMeta=`, configWallet.walletsMeta) const assetMeta = configWallet.walletsMeta[assetName] var addr = !knownAddr ? module.exports.getAddressFromPrivateKey({ assetMeta, privKey: key.privKey, eosActiveWallet }) : knownAddr // perf (bulk import) - don't recompute the key if it's already been done var pubKey if (assetMeta.OP_CLTV && key.privKey) { var pair = bitcoinJsLib.ECPair.fromWIF(key.privKey, network) pubKey = pair.publicKey.toString('hex') utilsWallet.softNuke(pair) pair = null } return { symbol, addr, accountName, isNonStdAddr, // DMS: identifies a non-std addr nonStd_protectOp_txid, // DMS: the "parent" or originating protect_op txid path: key.path, // see config/wallet -- we don't have completely unique HD paths (e.g. BTC/SW, and testnets), but seems not to matter too much (?) txs: [], utxos: [], lastAddrFetchAt: undefined, pubKey, } }, getAddressFromPrivateKey: (p) => { const { assetMeta, privKey, eosActiveWallet } = p if (assetMeta.type === configWallet.WALLET_TYPE_UTXO) { return module.exports.getUtxoTypeAddressFromWif(privKey, assetMeta.symbol) } else if (assetMeta.type === configWallet.WALLET_TYPE_ACCOUNT) { return module.exports.getAccountTypeAddress(privKey, assetMeta.symbol, eosActiveWallet) } else utilsWallet.warn('### Wallet type ' + assetMeta.type + ' not supported!') }, getUtxoTypeAddressFromWif: (wif, symbol) => { var keyPair try { const network = module.exports.getUtxoNetwork(symbol) // bitgo networks: supports ZEC UInt16 pubKeyHash || scriptHash keyPair = bitgoUtxoLib.ECPair.fromWIF(wif, network) if (symbol === "BTC" || symbol === "LTC" || symbol === "LTC_TEST" || symbol === "BTC_SEG" || symbol === "BTC_TEST" || symbol === "BTC_SEG2" ) { const addr = module.exports.getUtxoTypeAddressFromPubKeyHex(keyPair.getPublicKeyBuffer().toString('hex'), symbol) utilsWallet.softNuke(keyPair) return addr } else { // bitgo-utxo-lib (note - can't use bitcoin-js payment.p2pkh with ZEC UInt16 pubKeyHash || scriptHash) var addr = keyPair.getAddress() if (symbol === 'BCHABC') { if (addr.startsWith('1')) { addr = bchAddr.toCashAddress(addr) } } utilsWallet.softNuke(keyPair) return addr } } catch (err) { utilsWallet.softNuke(keyPair) utilsWallet.error(`getUtxoTypeAddressFromWif - FAIL: ${err.message}`, err) return null } }, getUtxoTypeAddressFromPubKeyHex:(pubKeyHex, symbol) => { const reHex = /[0-9A-Fa-f]{6}/g; if (!pubKeyHex || pubKeyHex.length != 66 || !reHex.test(pubKeyHex)) return null //throw 'Invalid pubKeyHex' const network = module.exports.getUtxoNetwork(symbol) // bitgo networks: supports ZEC UInt16 pubKeyHash || scriptHash if (symbol === "BTC" || symbol === "LTC" || symbol === "LTC_TEST") { // P2PKH -- LEGACY -- bitcoinjs-lib (1 addr) const { address } = bitcoinJsLib.payments.p2pkh({ pubkey: Buffer.from(pubKeyHex, 'hex'), network }) // bitcoin-js payments (works with bitgo networks) return address } else if (symbol === "BTC_SEG" || symbol === "BTC_TEST") { // P2SH-WRAPPED SEGWIT (P2SH(P2WPKH)) -- bitcoinjs-lib -- addr (3 addr) const { address } = bitcoinJsLib.payments.p2sh({ redeem: bitcoinJsLib.payments.p2wpkh({ pubkey: Buffer.from(pubKeyHex, 'hex'), network }), network }) return address } else if (symbol === "BTC_SEG2") { // unwrapped P2WPKH -- w/ bitgo-utxo-lib -- NATIVE/UNWRAPPED SEGWIT (b addr, Bech32) const address = bitgoUtxoLib.address.fromOutputScript(bitgoUtxoLib.script.witnessPubKeyHash.output.encode(bitgoUtxoLib.crypto.hash160(Buffer.from(pubKeyHex, 'hex')))) return address } else return null //throw 'Unsupported' }, getAccountTypeAddress: (privKey, symbol, eosActiveWallet) => { try { if (symbol === "EOS") { if (eosActiveWallet !== undefined && eosActiveWallet !== null) { return eosActiveWallet.address } else { utilsWallet.warn(`## getAccountTypeAddress - eosActiveWallet undefined!`) return undefined } } else { return "0x" + ethereumJsUtil.privateToAddress(Buffer.from(utilsWallet.hextoba(privKey), 'hex')).toString('hex') } } catch (err) { utilsWallet.error(`getAccountTypeAddress - FAIL: ${err.message}`, err) return null } }, // // private key derivation // generateUtxoBip44Wifs: (p) => { const { entropySeed, symbol, addrNdx = 0, genCount = configWallet.WALLET_DEFAULT_ADDRESSES } = p var keyPairs = [] const network = module.exports.getUtxoNetwork(symbol) // bitgo if (network === undefined) throw 'generateUtxoBip44Wifs - unsupported type' var meta = configWallet.getMetaBySymbol(symbol) const entropySha256 = utilsWallet.sha256_shex(entropySeed) var root = bitgoUtxoLib.HDNode.fromSeedHex(entropySha256, network) // bitgo HDNode var accountNdx = 0 // scoop default account var chainNdx = 0 // bip44: 0=external chain, 1=internal chain (change addresses) for (var i = addrNdx; i < addrNdx + genCount; i++) { const path = `m/44'/${meta.bip44_index}'/${accountNdx}'/${chainNdx}/${i}` const child = root.derivePath(path) //var keyPair = ECPair.fromPrivateKey(child.privateKey, { network }) // bitcoin-js (no ZEC support, see https://github.com/bitcoinjs/bitcoinjs-lib/issues/865) var keyPair = child.keyPair // bitgo var wif = keyPair.toWIF() //utilsWallet.debug(`generateUtxoBip44Wifs - ${symbol} @ BIP44 path ${path}`) keyPairs.push({ privKey: wif, path }) } return keyPairs }, generateEthereumWallet: (p) => { const { entropySeed, addrNdx = 0, genCount = configWallet.WALLET_DEFAULT_ADDRESSES } = p try { var privKeys = [] const root = bip32.fromSeed(Buffer.from(utilsWallet.hextoba(utilsWallet.sha256_shex(entropySeed)))) var meta = configWallet.getMetaBySymbol('ETH') var accountNdx = 0 // scoop default account var chainNdx = 0 // bip44: 0=external chain, 1=internal chain (change addresses) for (var i = addrNdx; i < addrNdx + genCount; i++) { const path = `m/44'/${meta.bip44_index}'/${accountNdx}'/${chainNdx}/${i}` const child = root.derivePath(path) //utilsWallet.debug(`generateEthereumWallet - ETH @ BIP44 path ${path}`) privKeys.push({ privKey: utilsWallet.batohex(child.privateKey), path }) } return privKeys } catch (err) { utilsWallet.error(`generateEthereumWallet - FAIL: ${err.message}`, err) return null } }, // // helpers // encryptPrunedAssets: (currentAssets, apk, h_mpk) => { // prune var currentAssetsKeysOnly = {} Object.keys(currentAssets).map(assetName => { var assetAccounts = _.cloneDeep(currentAssets[assetName].accounts) currentAssetsKeysOnly[assetName] = { accounts: assetAccounts } }) // stringify var pt_assetsJsonPruned = JSON.stringify(currentAssetsKeysOnly, null, 1) // encrypt const e_assetsRawPruned = utilsWallet.aesEncryption(apk, h_mpk, pt_assetsJsonPruned) utilsWallet.softNuke(currentAssetsKeysOnly) utilsWallet.softNuke(pt_assetsJsonPruned) return e_assetsRawPruned }, getUtxoNetwork: (symbol) => { // https://github.com/BitGo/bitgo-utxo-lib/blob/master/src/networks.js // https://www.npmjs.com/package/@upincome/coininfo // https://github.com/libbitcoin/libbitcoin-system/wiki/Altcoin-Version-Mappings // https://github.com/libbitcoin/libbitcoin-system/issues/319 // https://github.com/bitcoinjs/bitcoinjs-lib/issues/1067 const coininfo = require('coininfo') switch (symbol) { case "BTC": return bitgoUtxoLib.networks.bitcoin case "BTC_SEG": return bitgoUtxoLib.networks.bitcoin case "BTC_SEG2": return bitgoUtxoLib.networks.bitcoin case "BTC_TEST": return bitgoUtxoLib.networks.testnet case "LTC": return bitgoUtxoLib.networks.litecoin case "LTC_TEST": return coininfo('LTC-TEST').toBitcoinJS() case "ZEC": return bitgoUtxoLib.networks.zcash case "ZEC_TEST": return bitgoUtxoLib.networks.zcashTest case "DASH": return bitgoUtxoLib.networks.dash case "BCHABC": return bitgoUtxoLib.networks.bitcoincash case "VTC": return coininfo('VTC').toBitcoinJS() case "QTUM": return coininfo('QTUM').toBitcoinJS() case "DGB": var ret = coininfo('DGB') ret.versions.bip32 = { public: 0x0488B21E, private: 0x0488ADE4 } var ret_js = ret.toBitcoinJS() return ret_js case "RVN": return coininfo('RVN').toBitcoinJS() default: return undefined } } } function sortAddresses(addresses) { const sorted = addresses.sort((a,b) => { // regular: m/44'/1'/0'/0/0 // protected: ~p/44'/1'/0'/0/0 // imported: ~i/44'/1'/1'/0/0 const ss_a = a.path.split('/') const ss_b = b.path.split('/') if (ss_a[0] != ss_b[0]) return ss_a[0].localeCompare(ss_b[0]) * -1 // m/ , ~p/ , ~i/ if (Number(ss_a[5]) < Number(ss_b[5])) return -1 if (Number(ss_a[5]) > Number(ss_b[5])) return +1 return 0 }) //console.log('sorted', sorted) return sorted }