UNPKG

scpx-wallet

Version:

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

722 lines (631 loc) 37.2 kB
// Distributed under AGPLv3 license: see /LICENSE for terms. Copyright 2019-2021 Dominic Morris. const bitcoinJsLib = require('bitcoinjs-lib') const bitgoUtxoLib = require('bitgo-utxo-lib') const bchAddr = require('bchaddrjs') const BigNumber = require('bignumber.js') const _ = require('lodash') const actionsWallet = require('.') const walletUtxo = require('./wallet-utxo') const walletAccount = require('./wallet-account') const configWallet = require('../config/wallet') const configExternal = require('../config/wallet-external') const utilsWallet = require('../utils') const walletP2shBtc = require('./wallet-btc-p2sh') const walletP2pkhBtc = require('./wallet-btc-p2pkh') const walletP2pkhAlts = require('./wallet-alts-p2pkh') module.exports = { // // process asset full state updates // getAddressFull_ProcessResult: (res, asset, addrNdx) => { //utilsWallet.debug(`getAddressFull_ProcessResult - ${asset.symbol} addrNdx=${addrNdx}...`) if (!res || !res.txs) return null if (configWallet.TEST_PAD_TXS) testPadTxs(res) if (configWallet.TEST_LARGE_BALANCE > 0) res.balance = configWallet.TEST_LARGE_BALANCE const balanceChanged = res.balance != asset.addresses[addrNdx].balance || res.unconfirmedBalance != asset.addresses[addrNdx].unconfirmedBalance const firstLoad = asset.addresses[addrNdx].lastAddrFetchAt === undefined var testingPaddedTxs = configWallet.TEST_PAD_TXS ? true : false const new_txs = res.txs.filter(p => { return !asset.addresses[addrNdx].txs.some(p2 => { return p2.txid === p.txid }) }) const anyNewTx = new_txs.length > 0 var new_txs_value if (asset.type === configWallet.WALLET_TYPE_UTXO) { new_txs_value = new_txs.reduce((sum,p) => { // utxo vin values that this addr contributed to var txAddrValue = new BigNumber(0) if (p.utxo_vin !== undefined) { // UTXO v2 - skip minimal tx's txAddrValue = p.utxo_vin .filter(p2 => { return p2.addr == asset.addresses[addrNdx] }) .map(p2 => { return p2.valueSat }) .reduce((sum2,p2) => { return sum2.plus(new BigNumber(p2)) }, new BigNumber(0)) } return sum.plus(txAddrValue) }, new BigNumber(0)) } else { new_txs_value = new_txs .filter(p => { return p.value !== undefined }) // ETH v2 - skip minimal tx's .reduce((sum,p) => { return sum.plus(new BigNumber(utilsWallet.toCalculationUnit(p.value, asset).times(p.isIncoming ? +1 : -1))) .plus(new BigNumber(utilsWallet.toCalculationUnit(p.isIncoming || utilsWallet.isERC20(asset) ? 0 : (new BigNumber(p.fees).times(-1)), asset))) }, new BigNumber(0)) } const delta_bal_conf = new BigNumber(res.balance).minus(new BigNumber(asset.addresses[addrNdx].balance)) const delta_bal_unconf = new BigNumber(res.unconfirmedBalance).minus(new BigNumber(asset.addresses[addrNdx].unconfirmedBalance)) const min_accept_delta = asset.addressType === configWallet.ADDRESS_TYPE_ETH ? 1 : configWallet.UTXO_DUST_SAT const anyPendingLocalTxs = getAll_local_txs(asset).length > 0 if ( // initial load or testing - accept firstLoad || testingPaddedTxs // utxo & account - MAIN ATOMIC UPDATE FILTER -- delta on tx's value and the balance are in sync || (balanceChanged && anyNewTx && new_txs_value.minus(delta_bal_conf).abs() <= min_accept_delta) // account only (eth) - CASHFLOW TOKENS -- we can get balance updates without *any* transactions! // > this happens when we subscribe to an issuance by sending eth to the CFT contract < // in this case, accept a state change on the balance update, but only if there aren't any unconfirmed/pending tx's // (the last condition keeps the CFT's working in the normal erc20 receive case [bug otherwise is balance updates to high, then settles to correct value]) || (asset.isCashflowToken && balanceChanged && anyPendingLocalTxs == false) // account - new tx but no balance change -- accept (note we don't accept the inverse) // this is to work around blockbook not giving us atomic tx/balance updates; // on receive, get balance update without tx update, which we ignore in favour of our local_tx // accepting the inverse lets us accept the new tx (when BB eventually reports it), in the case where we've just logged in // and are waiting for a lagging BB tx, and firstLoad has already accepted the BB balance //|| (asset.type === configWallet.WALLET_TYPE_ACCOUNT && newTx && !balanceChanged) //|| (asset.type === configWallet.WALLET_TYPE_ACCOUNT && balanceChanged && newTx) //|| (asset.type === configWallet.WALLET_TYPE_ACCOUNT && !delta_bal_conf.eq(0)) // try BTC send-all issue fix // ****** //|| (asset.type === configWallet.WALLET_TYPE_UTXO) // utxo - accept *any* change to confirmed //|| (asset.type === configWallet.WALLET_TYPE_UTXO && !delta_bal_conf.eq(0)) // utxo - accept an unconf change only if it matches tx change //|| (asset.type === configWallet.WALLET_TYPE_UTXO && new_txs_value.minus(delta_bal_unconf).abs() <= min_accept_delta) ) { var newAddr = Object.assign({}, asset.addresses[addrNdx], res) newAddr.lastAddrFetchAt = new Date() utilsWallet.log(`getAddressFull_ProcessResult - ${asset.symbol} - addrNdx=${addrNdx} - ACCEPTING STATE UPDATE: newTx=${anyNewTx} balanceChanged=${balanceChanged}`) const dispatchAction = { type: actionsWallet.WCORE_SET_ADDRESS_FULL, payload: { updateAt: new Date(), symbol: asset.symbol, newAddr} } return dispatchAction } else { //utilsWallet.log(`getAddressFull_ProcessResult - ${asset.symbol} - addrNdx=${addrNdx} - dropping state update! newTx=${newTx}, balanceChanged=${balanceChanged}, new_txs_value=${new_txs_value.toString()}, delta_bal_conf=${delta_bal_conf.toString()}`) return null } }, // payTo: [ { receiver: 'address', value: 'value'} ... ] createAndPushTx: (p, callback) => { const { store, payTo, wallet, asset, feeParams = {}, sendFromAddrNdx = -1, useUtxos, apk, h_mpk, } = p if (payTo !== undefined && payTo.some(p => p.value < 0)) { throw 'Insufficient balance' } console.log('createAndPushTx/payTo.dsigCltvSpenderPubKey', payTo.dsigCltvSpenderPubKey) utilsWallet.log(`*** createAndPushTx (wallet-external) ${asset.symbol}... payTo=`, payTo) createTxHex({ payTo, asset, encryptedAssetsRaw: wallet.assetsRaw, feeParams, sendMode: true, sendFromAddrNdx, useUtxos, apk: apk, h_mpk: h_mpk, }) .then(resCreateTxHex => { const txHex = resCreateTxHex.hex pushTransactionHex(store, payTo, wallet, asset, txHex, (resPushTxHex, err) => { if (err) { utilsWallet.error(`## createAndPushTx (wallet-external) ${asset.symbol}, err=`, err) callback(null, err) } else { utilsWallet.logMajor('green','white', `Broadcast txid=${resPushTxHex.tx.txid}`, txHex, { logServerConsole: true }) store.dispatch({ type: actionsWallet.WCORE_PUSH_LOCAL_TX, payload: { symbol: asset.symbol, tx: resPushTxHex.tx } }) callback({ tx: resPushTxHex.tx, psbt: resCreateTxHex.psbt }) } }) }) .catch(err => { utilsWallet.error(`### createAndPushTx (wallet-external) createTxHex FAILED - ${asset.symbol} err=`, err) try { let message = err.response.data.errors[0].error callback(null, message) } catch (_) { callback(null, err.message || err.toString()) } }) }, exploreAssetAddress: (asset, addrNdx) => { if (configExternal.walletExternal_config[asset.symbol] !== undefined) { const a_n = asset.addresses[addrNdx] const explorer = configExternal.walletExternal_config[asset.symbol].explorerPath(a_n.addr) window.open(explorer, '_blank') } }, // // Combines all txs and local_txs across all addresses // getAll_txs: (asset) => { return getAll_txs(asset) }, getAll_local_txs: (asset) => { return getAll_local_txs(asset) }, getAll_unconfirmed_txs: (asset) => { return getAll_unconfirmed_txs(asset) }, getAll_protect_op_txs: (p) => { return getAll_protect_op_txs(p) }, // // Combines local_tx data with tx and balance fields ; two distinct sets of balance data: // // the first (main) set is {conf, unconf, avail, total} - this is top-level "account" data, suitable for main display // for utxo-types, we might be waiting for a change utxo to be returned to us; so we also return { utxo_avail, utxo_changePending } - used by the send screen // // if no addrNdx supplied, returns aggregated data for all addresses, otherwise restricts to the supplied address index // get_combinedBalance: (asset, addrNdx = -1, excludeProtectedAddresses) => { if (asset === undefined || asset.addresses === undefined) return const meta = configWallet.walletsMeta[asset.name.toLowerCase()] var ret = { conf: new BigNumber(0), unconf: new BigNumber(0), pending_out: new BigNumber(0), pending_in: new BigNumber(0), has_pending: false, avail: new BigNumber(0), total: new BigNumber(0), unconfirmed_tx_count: 0, allAddressesFetched: false, } // filter all or single address var addresses if (addrNdx == -1) { // all addresses addresses = asset.addresses if (excludeProtectedAddresses) { addresses = addresses.filter(p => !p.isNonStdAddr) } } else { addresses = [] // specific address if (asset.addresses[addrNdx]) addresses.push(asset.addresses[addrNdx]) else return ret } // confirmed & unconfirmed balances, aggregated over all addresses const totalConfirmed = addresses.reduce((sum,p) => { return new BigNumber(p.balance || 0).plus(new BigNumber(sum)) }, 0) const totalUnconfirmed = addresses.reduce((sum,p) => { return new BigNumber(p.unconfirmedBalance || 0).plus(new BigNumber(sum)) }, 0) if (addresses.some(p => p.balance === undefined || p.unconfirmedBalance === undefined)) { ret.allAddressesFetched = false } else { ret.allAddressesFetched = true } ret.conf = totalConfirmed || new BigNumber(0) ret.unconf = totalUnconfirmed || new BigNumber(0) // we need to supplement address-level unconfirmed data with local_tx data; // (1) eth doesn't give us any concept of unconfirmed // (2) similarly for segwit, our insight node has no knowledge of unconfirmed segwit tx's // assign (subtract) sum of pending local txs to unconfirmed balance from external source; // see wallet reducer for how local_txs are reconciled/removed as they're fetched from external sources var cu_local_txs_pendingOut = asset.local_txs .filter(p => p.isIncoming === false || p.sendToSelf === true) .filter(p => meta.addressType !== configWallet.ADDRESS_TYPE_ETH || (addresses.some(p2 => p2.addr.toLowerCase() === p.account_from.toLowerCase() ))) .filter(p => meta.addressType !== configWallet.ADDRESS_TYPE_BTC || (addresses.some(p2 => p2.addr === p.toOrFrom ))) .reduce((sum,p) => { var cu_value = utilsWallet.toCalculationUnit(p.value, asset) var cu_fees = utilsWallet.toCalculationUnit(p.fees, asset) var bn_total = new BigNumber(cu_value).plus(utilsWallet.isERC20(asset) ? 0 : cu_fees) return sum.plus(bn_total.times(-1)) }, new BigNumber(0)) var cu_local_txs_pendingIn = asset.local_txs .filter(p => p.isIncoming === true || p.sendToSelf === true) .filter(p => meta.addressType !== configWallet.ADDRESS_TYPE_ETH || (addresses.some(p2 => p2.addr.toLowerCase() === p.account_to.toLowerCase() ))) .filter(p => meta.addressType !== configWallet.ADDRESS_TYPE_BTC || (addresses.some(p2 => p2.addr === p.toOrFrom ))) .reduce((sum,p) => { var cu_value = utilsWallet.toCalculationUnit(p.value, asset) return sum.plus(new BigNumber(cu_value)) }, new BigNumber(0)) // modify unconfirmed with sum of pending inbound (+ve) and outbound (-ve) ret.unconf = ret.unconf.plus(cu_local_txs_pendingOut) // -ve ret.unconf = ret.unconf.plus(cu_local_txs_pendingIn) // +ve // the above modified unconf field can net to zero (if pending in and out values are the same in both directions), // so we also return summed pending in and out values: ret.pending_out = cu_local_txs_pendingOut ret.pending_in = cu_local_txs_pendingIn ret.has_pending = cu_local_txs_pendingOut.isLessThan(0) || cu_local_txs_pendingIn.isGreaterThan(0) || totalUnconfirmed != 0 // available balance: deduct any pending out, don't credit any pending in ret.avail = ret.conf.plus(ret.unconf) //.minus(cu_local_txs_pendingOut.abs()) // net off any pending local_tx out value //.minus(totalUnconfirmed < 0 ? new BigNumber(totalUnconfirmed).abs() : 0) // net off any address (3PBP) pending out value // available balance: DMS - deduct any balances that arise from protect_op/weAreBeneficiary tx's // (each such utxo needs a different locktime to be spent, so they must be spent one by one...) if (asset.type === configWallet.WALLET_TYPE_UTXO) { if (asset.OP_CLTV) { if (excludeProtectedAddresses == false) { // no need to do this if we've already excluded the p_op tx addr's const p_op_txs = getAll_protect_op_txs({ asset, weAreBeneficiary: true, weAreBenefactor: false }) if (p_op_txs.length > 0) { const beneficiary_addrs = asset.addresses.filter(p => p_op_txs.some(p2 => p2.p_op_addrNonStd == p.addr)) if (beneficiary_addrs.length > 0) { const beneficiaryConfirmed = beneficiary_addrs.reduce((sum,p) => { return new BigNumber(p.balance || 0).plus(new BigNumber(sum)) }, 0) const beneficiaryUnconfirmed = beneficiary_addrs.reduce((sum,p) => { return new BigNumber(p.unconfirmedBalance || 0).plus(new BigNumber(sum)) }, 0) if (beneficiaryConfirmed.isGreaterThan(0) || beneficiaryUnconfirmed.isGreaterThan(0)) { ret.avail = ret.avail.minus(beneficiaryConfirmed).minus(beneficiaryUnconfirmed) } } } } } } // total balance: confirmed and unconfirmed ret.total = ret.conf.plus(ret.unconf) // eth - round dust values to zero (all because can't get Geth to accept precise full-send amounts) if (asset.symbol === 'ETH' || asset.symbol === 'ETH_TEST') { if (configWallet.ETH_COALESCE_DUST_TO_ZERO && ret.avail.isGreaterThan(0) && ret.avail.isLessThanOrEqualTo(configWallet.ETH_DUST_WEI)) { //utilsWallet.log(`get_combinedBalance - rounding dust (avail) wei for ${asset.symbol} (${ret.avail})`) ret.avail = new BigNumber(0) } if (configWallet.ETH_COALESCE_DUST_TO_ZERO && ret.total.isGreaterThan(0) && ret.total.isLessThanOrEqualTo(configWallet.ETH_DUST_WEI)) { //utilsWallet.log(`get_combinedBalance - rounding dust (total) wei for ${asset.symbol} (${ret.total})`) ret.total = new BigNumber(0) } } // TODO -- should also be rounding ERC20 dust values - observed (sometimes) - "1e-20" or similar on send all erc20 //... // get total # of pending tx's -- external and local const unconfirmed_txs = getAll_unconfirmed_txs(asset) ret.unconfirmed_tx_count = asset.local_txs.length + unconfirmed_txs.length return ret }, // // Compute a specific tx fee, for the supplied tx details // computeTxFee: async (p) => { var { asset, receiverAddress, feeData, sendValue, dsigCltvSpenderPubKey, dsigCltvSpenderLockHours, encryptedAssetsRaw, useFastest, useSlowest, useUtxos, apk, h_mpk } = p if (!feeData) { throw 'Invalid parameter - feeData' } if (!asset) { throw 'Invalid parameter - asset' } if (!encryptedAssetsRaw) { throw 'Invalid parameter - encryptedAssetsRaw' } if (!apk) { throw 'Invalid parameter - apk' } if (!h_mpk) { throw 'Invalid parameter - h_mpk' } var ret = {} if (asset.type === configWallet.WALLET_TYPE_UTXO) { var cu_satPerKB = useFastest ? feeData.fastest_satPerKB : useSlowest ? feeData.slow_satPerKB : feeData.fast_satPerKB var du_satPerKB = Number(utilsWallet.toDisplayUnit(new BigNumber(cu_satPerKB), asset)) if (!sendValue) { sendValue = 0 } const payTo = [{ receiver: receiverAddress || configExternal.walletExternal_config[asset.symbol].donate, value: sendValue, dsigCltvSpenderPubKey, dsigCltvSpenderLockHours }] // we need to pass some fee into createTxHex; we only care here though about the returned tx size data const feeParams = { txFee: { fee: (du_satPerKB / 4) } } const res = await createTxHex({ payTo, asset, encryptedAssetsRaw, feeParams, sendMode: false, sendFromAddrNdx: -1, useUtxos, apk: apk, h_mpk: h_mpk, }) if (res !== undefined) { const cu_fee = new BigNumber(Math.ceil(((res.byteLength / 1024) * cu_satPerKB))) // tx KB size * sat/KB const du_fee = Number(utilsWallet.toDisplayUnit(cu_fee, asset)) ret = { inputsCount: res.inputsCount, utxo_vsize: res.vSize, utxo_satPerKB: cu_satPerKB, utxo_byteLength: res.byteLength, fee: du_fee } } else { utilsWallet.error(`Failed to construct tx hex for ${asset.symbol}, payTo=`, payTo) throw 'Failed to construct tx - ensure you have sufficient inputs for the specified value' } } else if (asset.type === configWallet.WALLET_TYPE_ACCOUNT) { if (asset.addressType === configWallet.ADDRESS_TYPE_ETH) { var gasPriceToUse = useFastest ? feeData.gasprice_fastest : useSlowest ? feeData.gasprice_safeLow : feeData.gasprice_fast var gasLimitToUse = feeData.gasLimit // default "estimate" - from wallet/actions.getAssetFeeData() // erc20's -- if asset flag set: use estimateGas + a multiplier (override hard-coded erc20_transferGasLimit); // required for complex transfer() functions, e.g. cashflow tokens if (asset.erc20_gasEstimateMultiplier) { const dummyTxParams = { from: asset.addresses[0].addr, //configExternal.walletExternal_config[asset.symbol].donate, to: configExternal.walletExternal_config[asset.symbol].donate, value: sendValue, gasLimit: feeData.gasLimit, gasPrice: gasPriceToUse, } utilsWallet.log(`erc20 - dummyTxParams`, dummyTxParams) const dummyTxHex = await walletAccount.createTxHex_Account({ asset, params: dummyTxParams, privateKey: undefined }) if (dummyTxHex && dummyTxHex.txParams) { const gasTxEstimate = await walletAccount.estimateTxGas_Account({ asset, params: dummyTxHex.txParams }) utilsWallet.log(`erc20 - gasEstimate`, gasTxEstimate) utilsWallet.log(`erc20 - asset`, asset) utilsWallet.log(`erc20 - asset.erc20_gasEstimateMultiplier`, asset.erc20_gasEstimateMultiplier) if (gasTxEstimate && gasTxEstimate > 0) { // use modified web3 gas estimate gasLimitToUse = Math.max( Math.ceil(gasTxEstimate * asset.erc20_gasEstimateMultiplier), asset.erc20_gasMin ) utilsWallet.log(`erc20 - estimatedGas`, gasLimitToUse) } } else utilsWallet.warn(`erc20 - failed to get tx params`) } // eth -- if receiver addr supplied: use estimateGas to override feeData; // required for complex payable functions, e.g. cashflow tokens if (receiverAddress) { if (utilsWallet.isERC20(receiverAddress)) { if (asset.symbol === 'ETH_TEST' || asset.symbol === 'ETH') { const dummyTxParams = { from: asset.addresses[0].addr, // ##? will fail if sending from ndx != 0? will need sending index to be passed? to: receiverAddress, value: sendValue, gasLimit: 7000000, //feeData.gasLimit, gasPrice: gasPriceToUse, } utilsWallet.log(`eth(_test) - dummyTxParams`, dummyTxParams) const dummyTxHex = await walletAccount.createTxHex_Account({ asset, params: dummyTxParams, privateKey: undefined }) if (dummyTxHex && dummyTxHex.txParams) { const gasTxEstimate = await walletAccount.estimateTxGas_Account({ asset, params: dummyTxHex.txParams }) utilsWallet.log(`eth(_test) - gasEstimate`, gasTxEstimate) utilsWallet.log(`eth(_test) - asset`, asset) if (gasTxEstimate && gasTxEstimate > 0) { // use modified web3 gas estimate gasLimitToUse = Math.ceil(gasTxEstimate * 1.2) utilsWallet.log(`eth(_test) - estimatedGas`, gasLimitToUse) } } else utilsWallet.warn(`eth(_test) - failed to get tx params`) } } } // ret var du_ethFee = new BigNumber(gasLimitToUse) .dividedBy(1000000000) .multipliedBy(new BigNumber(gasPriceToUse)) .dividedBy(1000000000) .toString() ret = { inputsCount: 1, eth_gasLimit: gasLimitToUse, eth_gasPrice: gasPriceToUse, fee: du_ethFee } } else throw(`Unknown account address type`) } else throw(`Unknown asset type`) utilsWallet.log(`computeTxFee ${asset.symbol} ${sendValue} - ret=`, ret) return ret }, // // this is called at validation-time on send screen to determine the total vbytes needed for the TX, as well as at send-time; // UTXOs are cached in the asset object and form part of the asset's full fetch payload // createTxHex: (params) => { return createTxHex(params) }, } // // create tx hex - all assets // async function createTxHex(params) { const { payTo, asset, encryptedAssetsRaw, feeParams, sendMode = true, sendFromAddrNdx = -1, useUtxos, apk, h_mpk } = params //console.log('createTxHex/payTo.dsigCltvSpenderPubKey', payTo.dsigCltvSpenderPubKey) if (!asset) throw 'Invalid or missing asset' if (!payTo || payTo.length == 0 || !payTo[0].receiver) throw 'Invalid or missing payTo' if (payTo.length != 1) throw 'send-many is not supported' if (payTo[0].dsigCltvSpenderPubKey !== undefined && !asset.OP_CLTV) throw 'Invalid dsigCltvSpenderPubKey for asset' if (!feeParams || !feeParams.txFee) throw 'Invalid or missing feeParams' if (!encryptedAssetsRaw || encryptedAssetsRaw.length == 0) throw 'Invalid or missing encryptedAssetsRaw' if (!apk || apk.length == 0) throw 'Invalid or missing apk' if (!h_mpk || h_mpk.length == 0) throw 'Invalid or missing h_mpk' utilsWallet.log(`*** createTxHex (wallet-external) ${asset.symbol}...`) const validationMode = !sendMode const skipSigningOnValidation = true // source UTXOs - all utxos, across all wallet addresses var utxos = [] asset.addresses //.filter(a_n => a_n.addr !== payTo.dsigCltvReceiver) // todo: (edgecase) - exclude any UTXOs from the dsigCltvReceiverAddr (for same-account testing...) .forEach(a_n => utxos.extend(a_n.utxos.map(p => { return Object.assign({}, p, { address: a_n.addr } )}))) utxos = _.uniqWith( utxos.filter(utxo_n => utxo_n.satoshis > 0) // required: we don't explicitly prune outputs when they are spent (only a cache-clear drops them) .filter(utxo_n => utxo_n.scriptPubKey.type !== "nulldata"), // exclude OP_RETURN outputs _.isEqual) if (asset.type === configWallet.WALLET_TYPE_UTXO) { if (asset.OP_CLTV) { // spending specific UTXOs (e.g. all PROTECT_OPs via CLAIMABLE-CLAIM) - filter out all other UTXOs if (useUtxos !== undefined && useUtxos.length > 0) { utxos = utxos.filter(p => useUtxos.some(p2 => p2.txid == p.txid && p2.vout == p.vout)) } else { // spending across multiple outputs - filter out PROTECT_OP UTXOs (they must be spent specifically, by supplying useUtxos) //const p_op_txs = getAll_protect_op_txs({ asset, weAreBeneficiary: true, weAreBenefactor: true }) //utxos = utxos.filter(p => !p_op_txs.some(p2 => p2.txid == p.txid)) utxos = utxos.filter(p => asset.addresses.filter(p2 => p2.isNonStdAddr).some(p2 => p2.addr == p.address) == false) } } //console.log('utxos', utxos) if (utxos.length == 0) throw 'Insufficient or invalid UTXO(s)' } // get private keys var pt_AssetsJson = utilsWallet.aesDecryption(apk, h_mpk, encryptedAssetsRaw) if (!pt_AssetsJson || pt_AssetsJson === '') throw('Failed decrypting assets') var pt_assetsObj = JSON.parse(pt_AssetsJson) var pt_asset = pt_assetsObj[asset.name.toLowerCase()] utilsWallet.softNuke(pt_assetsObj) pt_AssetsJson = null // flatten accounts: addr -> privKey var addrPrivKeys = [] pt_asset.accounts.forEach(account => { account.privKeys.forEach(privKey => { const addrInfo = asset.addresses.find(p => p.path === privKey.path) // lookup the addr (rather than recompute it, faster) if (!addrInfo) { utilsWallet.error(`failed to lookup addr for path ${privKey.path}`) } addrPrivKeys.push( { addr: addrInfo.addr, privKey: privKey.privKey } ) }) }) utilsWallet.softNuke(pt_asset) switch (asset.type) { case configWallet.WALLET_TYPE_UTXO: { // get total receiver output value, for return const cu_sendValue = payTo.reduce((sum,p) => { return sum.plus(new BigNumber(p.value).times(100000000)) }, BigNumber(0)) // get required inputs & outputs const utxoParams = { changeAddress: payTo[0].changeAddr || asset.addresses[0].addr, outputs: payTo.map(p => { return { receiver: p.receiver, value: new BigNumber(p.value).times(100000000).toString(), dsigCltvSpenderPubKey: p.dsigCltvSpenderPubKey, dsigCltvSpenderLockHours: p.dsigCltvSpenderLockHours }}), feeSatoshis: Math.floor(feeParams.txFee.fee * 100000000), utxos, } var txSkeleton try { txSkeleton = await walletUtxo.getUtxo_InputsOutputs(asset.symbol, utxoParams) //, true /*sendMode*/) //throwOnInsufficient } catch (err) { if (sendMode) return Promise.reject(err) // we're sending a tx: the error will propagate to client else return undefined // we're estimating fees for a tx: the error will be handled internally } if (!txSkeleton) throw 'Failed parsing tx skeleton' if (sendMode) { console.log('txSkeleton', txSkeleton) } // construct tx hex - switch on asset const opsWallet = require('./wallet') const network = opsWallet.getUtxoNetwork(asset.symbol) var tx, hex, vSize, byteLength if (asset.symbol === 'ZEC' || asset.symbol === 'DASH' || asset.symbol === 'VTC' || asset.symbol === 'QTUM' || asset.symbol === 'DGB' || asset.symbol === 'BCHABC' || asset.symbol === 'ZEC_TEST' || asset.symbol === 'RVN') { // // UTXO - P2PKH - bitgo-utxo tx builder (https://github.com/BitGo/bitgo-utxo-lib/issues/12, https://blog.bitgo.com/how-to-create-a-zcash-sapling-compatible-multisig-transaction-98e45657c48d ) // var { tx, hex, vSize, byteLength } = walletP2pkhAlts.createTxHex_Alts_P2PKH({ asset, validationMode, skipSigningOnValidation, addrPrivKeys, txSkeleton }) } else { if (asset.symbol === "BTC_SEG" || asset.symbol === "BTC_TEST") { // // UTXO - P2SH(...) - bitcoin-js PSBT (Partially Signed Bitcoin Transaction Format - BIP174) // var { tx, hex, vSize, byteLength, psbt } = walletP2shBtc.createTxHex_BTC_P2SH({ asset, validationMode, addrPrivKeys, txSkeleton, dsigCltvSpenderPubKey: payTo[0].dsigCltvSpenderPubKey, dsigCltvSpenderLockHours: payTo[0].dsigCltvSpenderLockHours, }) } else { // BTC || BTC_SEG2 // // UTXO - P2PKH || P2WPKH - bitcoin-js tx builder // var { tx, hex, vSize, byteLength } = walletP2pkhBtc.createTxHex_BTC_P2PKH({ asset, validationMode, skipSigningOnValidation, addrPrivKeys, txSkeleton }) } } utilsWallet.softNuke(addrPrivKeys) return new Promise((resolve, reject) => { resolve({ hex, vSize, byteLength, inputsCount: txSkeleton.inputs.length, _cu_sendValue: cu_sendValue.toString(), get cu_sendValue() { return this._cu_sendValue }, set cu_sendValue(value) { this._cu_sendValue = value }, psbt, }) }) } case configWallet.WALLET_TYPE_ACCOUNT: { const receiver = payTo[0].receiver const value = payTo[0].value if (sendFromAddrNdx < 0 || sendFromAddrNdx > asset.addresses.length - 1) { utilsWallet.error(`### createTxHex (wallet-external ACCOUNT) ${asset.symbol} - bad addrNdx supplied`) return new Promise((resolve, reject) => { reject('Bad addrNdx') }) } const senderAddr = asset.addresses[sendFromAddrNdx].addr var wif = addrPrivKeys.find(p => { return p.addr === senderAddr }).privKey payTo.senderAddr = senderAddr // record sender -- it's passed through post-tx send and is recorded on the local_tx const txParams = { from: senderAddr, to: receiver, value: value, gasLimit: feeParams.txFee.eth_gasLimit, gasPrice: feeParams.txFee.eth_gasPrice, } const walletAccount = require('./wallet-account') const txHexAndValue = await walletAccount.createTxHex_Account({ asset, params: txParams, privateKey: wif }) utilsWallet.softNuke(addrPrivKeys) return new Promise((resolve, reject) => { resolve( { hex: txHexAndValue.txhex, cu_sendValue: txHexAndValue.cu_sendValue.toString() } )}) } default: utilsWallet.error('Wallet type ' + asset.type + ' not supported!') break } } // // push tx // function pushTransactionHex(store, payTo, wallet, asset, txHex, callback) { utilsWallet.log(`*** pushTransactionHex (wallet-external) ${asset.symbol} txHex=`, txHex) switch (asset.type) { case configWallet.WALLET_TYPE_UTXO: walletUtxo.pushRawTransaction_Utxo(wallet, asset, txHex, (res, err) => { callback(res, err) }) break case configWallet.WALLET_TYPE_ACCOUNT: walletAccount.pushRawTransaction_Account(store, asset, payTo, txHex, (res, err) => { callback(res, err) }) break default: throw 'Unsupported asset type' } } function getAll_txs(asset) { return utilsWallet.getAll_txs(asset) } function getAll_local_txs(asset) { return utilsWallet.getAll_local_txs(asset) } function getAll_unconfirmed_txs(asset) { return utilsWallet.getAll_unconfirmed_txs(asset) } function getAll_protect_op_txs(p) { return utilsWallet.getAll_protect_op_txs(p) } function testPadTxs(res) { for (let i=0 ; i < configWallet.TEST_PAD_TXS ; i++) { res.txs.push( { block_no: 1452313, sendToSelf: false, confirmed: true, date: new Date().toString(), fees: 0.00001, isIncoming: false, toOrFrom: "mkjxRwEFtvW7WBVcwodEPNrHKfESdTsNT5", txid: `TEST_TX_${i}`, value: "0.42424242", utxo_vin: [], //utxo_vout: [], } ) } } Array.prototype.extend = function (other_array) { if (other_array) { other_array.forEach(function (v) { this.push(v) }, this) } }