UNPKG

scpx-wallet

Version:

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

471 lines (412 loc) 25.8 kB
// Distributed under AGPLv3 license: see /LICENSE for terms. Copyright 2019-2021 Dominic Morris. const CircularBuffer = require("circular-buffer") const io = require('socket.io-client') const axios = require('axios') //const axiosRetry = require('axios-retry') const BigNumber = require('bignumber.js') const configWS = require('../config/websockets') const configExternal = require('../config/wallet-external') const configWallet = require('../config/wallet') const actionsWallet = require('../actions') const walletUtxo = require('../actions/wallet-utxo') const utilsWallet = require('../utils') module.exports = { // insight tx and block subscriptions (diagnostics and balance polling, respectively) socketio_Setup_Insight: (networkConnected, networkStatusChanged, loaderWorker) => { //utilsWallet.debug('appWorker >> ${self.workerId} insight_Setup...') for (var assetSymbol in configWS.insightApi_ws_config) { if (assetSymbol === 'LTC_TEST' && !configWallet.WALLET_INCLUDE_LTC_TEST) continue if (assetSymbol === 'ZEC_TEST' && !configWallet.WALLET_INCLUDE_ZEC_TEST) continue if (assetSymbol === 'BTC_TEST' && !configWallet.WALLET_INCLUDE_BTC_TEST) continue (function (x) { try { // if we're called more than once, then the socket object already exists if (self.insightSocketIos[x] !== undefined) { // safari refocus handling // it may however be disconnected (e.g. when resuming from phone lock) // in this case, we detect it here and tear down the socket if (self.insightSocketIos[x].connected === false) { utilsWallet.warn(`appWorker >> ${self.workerId} INSIGHT WS ${x} - io: found disconnected socket for ${x} - nuking it!`) // important to tear down this existing socket properly (duplicates otherwise, and duplicate event handlers & flows - very bad) self.insightSocketIos[x].off() self.insightSocketIos[x].disconnect() self.insightSocketIos[x] = undefined // rest of this fn. will now recreate the socket } } // initial / main path if (self.insightSocketIos[x] === undefined) { // connect & init //networkConnected(x, true) // init UI //networkStatusChanged(x, null) //utilsWallet.debug(`appWorker >> ${self.workerId} INSIGHT WS ${x} - io: ${configWS.insightApi_ws_config[x].url}...`, null, { logServerConsole: true }) self.insightSocketIos[x] = io(configWS.insightApi_ws_config[x].url, { transports: ['websocket'] }) var socket = self.insightSocketIos[x] // // socket lifecycle // socket.on('connect', () => { //utilsWallet.debug(`appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - connect...`) try { if (!loaderWorker) { networkConnected(x, true) networkStatusChanged(x, { insight_url: configWS.insightApi_ws_config[x].url }) socket.emit('subscribe', 'inv') // subscribe new tx and new blocks } } catch (err) { utilsWallet.error(`### appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - connect(1), err=`, err) } }) socket.on('connect_failed', function () { utilsWallet.error(`appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - connect_failed`) try { if (!loaderWorker) { networkConnected(x, false) networkStatusChanged(x, { insight_url: configWS.insightApi_ws_config[x].url }) } } catch (err) { utilsWallet.error(`### appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - connect_failed, err=`, err) } }) socket.on('connect_error', function (socketErr) { utilsWallet.error(`appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - connect_error, socketErr=`, socketErr.message) try { if (!loaderWorker) { networkConnected(x, false) networkStatusChanged(x, { insight_url: configWS.insightApi_ws_config[x].url }) } } catch (err) { utilsWallet.error(`### appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - connect_error, err=`, err) } }) socket.on('disconnect', function () { utilsWallet.warn(`appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - disconnect`) try { if (!loaderWorker) { networkConnected(x, false) networkStatusChanged(x) } } catch (err) { utilsWallet.error(`### appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - disconnect, err=`, err) } }) socket.on('reconnect', () => { utilsWallet.warn(`appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - reconnect...`) try { if (!loaderWorker) { networkConnected(x, true) networkStatusChanged(x) socket.emit('subscribe', 'inv') } } catch (err) { utilsWallet.error(`### appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - reconnect, err=`, err) } }) socket.on('reconnect_failed', function () { utilsWallet.warn(`appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - reconnect_failed`) try { if (!loaderWorker) { networkConnected(x, false) networkStatusChanged(x) } } catch (err) { utilsWallet.error(`### appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - reconnect_failed, err=`, err) } }) socket.on('error', function (socketErr) { utilsWallet.warn(`appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - error, socketErr=`, socketErr) try { if (!loaderWorker) { networkConnected(x, false) networkStatusChanged(x) } } catch (err) { utilsWallet.error(`### appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - error, err=`, err) } }) // // subscriptions - new tx's and new blocks // if (!loaderWorker) { socket.on('tx', (tx) => { //utilsWallet.log(`appWorker >> ${self.workerId} INSIGHT TX ${x}`, x) // btc.com's "insight" server produces some weird responses (tx event is missing txid) if (tx.txid === undefined) { tx.txid = `@${Date.now().toString()}` } // calc spot mempool TPS over last BUF_CAP mempool tx's received const BUF_CAP = 5 var mempool_tps = 0 if (!self.mempool_tpsBuf[x]) self.mempool_tpsBuf[x] = new CircularBuffer(BUF_CAP) if (!self.mempool_tot[x]) self.mempool_tot[x] = 0 //console.log(self.mempool_tpsBuf[x]) if (!self.mempool_tpsBuf[x].toarray().some(p => p.txid == tx.txid)) { self.mempool_tpsBuf[x].push({ txid: tx.txid, timestamp: new Date().getTime() }) self.mempool_tot[x]++ //console.log('pushed...') } //console.log(`${x}: mempool_tpsBuf[x].size()=${mempool_tpsBuf[x].size()}`) if (mempool_tpsBuf[x].size() == BUF_CAP) { const buf1 = mempool_tpsBuf[x].get(0) const buf2 = mempool_tpsBuf[x].get(BUF_CAP - 1) const ms = buf2.timestamp - buf1.timestamp mempool_tps = BUF_CAP / (ms/1000) //console.log(`${x}: tot=${self.mempool_tot[x]} buf1=${buf1.timestamp} buf2=${buf2.timestamp} ms=${ms} mempool_tps=${mempool_tps}`) } // average the spot mempool TPS values - trying to deal with network transmit spikes const AVG_CAP = 10 if (!self.mempool_tpsAvg[x]) self.mempool_tpsAvg[x] = new CircularBuffer(AVG_CAP) if (mempool_tps < 15) { // gross hack: some insight servers are spitting out batches of tx's in bursts; no idea why - e.g. https://insight.dash.org/insight/ self.mempool_tpsAvg[x].push(mempool_tps) } const tps_values = self.mempool_tpsAvg[x].toarray() const mempool_tps_avg = tps_values.reduce((a,b) => a+b, 0) / tps_values.length // throttle these to max n per sec //const sinceLastTx = new Date().getTime() - self.lastTx[x] //if (isNaN(sinceLastTx) || sinceLastTx > 500) { // self.lastTx[x] = new Date().getTime() // Jan '21 - don't call this at all for tx's (perf) // networkStatusChanged(x, { txid: tx.txid, mempool_tps: mempool_tps_avg, insight_url: configWS.insightApi_ws_config[x].url }) //} }) socket.on('block', (blockHash) => { if (configWallet.WALLET_DISABLE_BLOCK_UPDATES) return if (configWS.insightApi_ws_config[x].subBlocks === false) { //utilsWallet.debug(`appWorker >> ${self.workerId} INSIGHT WS ${x} - IO - ignoring block: subBlocks=false`) } else { try { // requery balance check for asset on new block self.postMessage({ msg: 'REQUEST_STATE', status: 'REQ', data: { stateItem: 'ASSET', stateKey: x, context: 'ASSET_REFRESH_NEW_BLOCK' } }) // get reeived block height & time //axiosRetry(axios, configWallet.AXIOS_RETRY_3PBP) axios.get(configExternal.walletExternal_config[x].api.block(blockHash)) .then((resBlockData) => { if (resBlockData && resBlockData.data) { const receivedBlockNo = resBlockData.data.height const receivedBlockTime = new Date(resBlockData.data.time * 1000) utilsWallet.logMajor('green','white', `appWorker >> ${self.workerId} INSIGHT BLOCK ${x} ${receivedBlockNo} ${receivedBlockTime}`) // get node sync status getSyncInfo_Insight(x, receivedBlockNo, receivedBlockTime) } }) } catch (err) { utilsWallet.error(`### appWorker >> ${self.workerId} INSIGHT BLOCK ${x}, err=`, err) } } }) } } } catch(err) { utilsWallet.error(`appWorker >> ${self.workerId} INSIGHT WS ${x} - io: ${configWS.insightApi_ws_config[x].url}, err=`, err) utilsWallet.trace() networkConnected(x, false) networkStatusChanged(x) } })(assetSymbol) } }, getAddressBalance_Insight: (asset, address) => { const symbol = asset.symbol //utilsWallet.debug(`getAddressBalance_Insight v2_addrBal ${symbol}...`) return new Promise((resolve, reject) => { const axiosLongtimeout = axios.create({ timeout: 5000 } ) //axiosRetry(axiosLongtimeout, configWallet.AXIOS_RETRY_3PBP) axiosLongtimeout.get(configExternal.walletExternal_config[symbol].api.v2_addrBal(address) + '&dt=' + new Date().getTime()) .then(res => { if (res && res.data) { resolve({ symbol, balance: new BigNumber(res.data.balanceSat), unconfirmedBalance: new BigNumber(res.data.unconfirmedBalanceSat), address, }) } else { resolve(null) // allow batch promise.all calls to try all utilsWallet.error(`### getAddressBalance_Insight ${symbol} v2_addrBal - unexpected or missing insight data`) } }) .catch(err => { resolve(null) utilsWallet.error(`### getAddressBalance_Insight ${symbol} v2_addrBal, err=`, err) }) .finally(() => { resolve(null) }) }) }, // UTXO v2 -- gets balance and last n raw tx id's - one op. getAddressFull_Insight_v2: (wallet, asset, pollAddress, utxo_mempool_spentTxIds, allDispatchActions) => { const symbol = asset.symbol //utilsWallet.debug(`getAddressFull_Insight_v2 ${symbol}...`) //axiosRetry(axios, configWallet.AXIOS_RETRY_3PBP) const from = 0 const to = (configWallet.WALLET_MAX_TX_HISTORY || 888) - 1 return Promise.all([ axios.get(configExternal.walletExternal_config[symbol].api.v2_addrData(pollAddress, from, to)), axios.get(configExternal.walletExternal_config[symbol].api.utxo(pollAddress)) ]).then(async ([addrInfo, utxoInfo]) => { if (addrInfo && utxoInfo && addrInfo.data && utxoInfo.data) { var utxos = utxoInfo.data var addrData = addrInfo.data // prune unused utxo data //console.log('insight_utxos', utxos) utxos = utxos.map(p => { return { //address: p.address, //amount, //confirmations, //height, satoshis: p.satoshis, scriptPubKey, // DMS -- test this!!! -- needs to hold .addresses[], .hex, & .type... txid: p.txid, vout: p.vout, } }) // tx's const totalTxCount = addrData.txApperances + addrData.unconfirmedTxApperances // sp! // filter: new tx's, or known tx's that aren't yet enriched, or unconfirmed tx's const assetAddress = asset.addresses.find(p => p.addr == pollAddress) const newMinimalTxs = addrData.transactions.filter(p => !assetAddress.txs.some(p2 => p2.txid == p && p2.isMinimal == false && p2.block_no != -1) ) .map(p => { return { txid: p, isMinimal: true } }) // TX_MINIMAL const res = { balance: addrData.balanceSat, unconfirmedBalance: addrData.unconfirmedBalanceSat, //txs: new_txs, utxos, totalTxCount, cappedTxs: addrData.transactions.length < totalTxCount } if (newMinimalTxs.length > 0) { // queue enrich tx actions (will either take from the cache, or fetch, prune & populate the cache) const enrichOps = newMinimalTxs.map((tx) => { return enrichTx(wallet, asset, tx, pollAddress) }) // update batch await Promise.all(enrichOps) .then((enrichedTxs) => { const dispatchTxs = enrichedTxs.filter(p => p != null) if (dispatchTxs.length > 0) { //utilsWallet.debug(`getAddressFull_Insight_v2 ${symbol} ${pollAddress} - enrichTx done for ${dispatchTxs.length} tx's - requesting WCORE_SET_ENRICHED_TXS...`) const dispatchAction = { type: actionsWallet.WCORE_SET_ENRICHED_TXS, payload: { updateAt: new Date(), symbol: asset.symbol, addr: pollAddress, txs: dispatchTxs, res} } allDispatchActions.push(dispatchAction) } }) } // pass through the state update -- in v1 getAddressFull format const ret = Object.assign({}, res, { txs: newMinimalTxs } ) return ret } else { utilsWallet.error(`### getAddressFull_Insight_v2 ${symbol} no insight data`) } }) .catch((err) => { utilsWallet.error(`### getAddressFull_Insight_v2 ${symbol}, err=`, err) }) }, getSyncInfo_Insight: (symbol, receivedBlockNo = undefined, receivedBlockTime = undefined, networkStatusChanged = undefined) => { return getSyncInfo_Insight(symbol, receivedBlockNo, receivedBlockTime, networkStatusChanged) } } function getSyncInfo_Insight(symbol, receivedBlockNo = undefined, receivedBlockTime = undefined, networkStatusChanged = undefined) { if (symbol === 'LTC_TEST' && !configWallet.WALLET_INCLUDE_LTC_TEST) return if (symbol === 'ZEC_TEST' && !configWallet.WALLET_INCLUDE_ZEC_TEST) return if (symbol === 'BTC_TEST' && !configWallet.WALLET_INCLUDE_BTC_TEST) return if (configExternal.walletExternal_config[symbol] === undefined || configExternal.walletExternal_config[symbol].api == undefined || configExternal.walletExternal_config[symbol].api.sync === undefined) { utilsWallet.info(`appWorker >> ${self.workerId} getSyncInfo_Insight ${symbol} - ignoring: not setup for asset`) return } axios.get(configExternal.walletExternal_config[symbol].api.sync()) .then(syncData => { if (syncData && syncData.data) { const insightSyncStatus = syncData.data.status const insightSyncBlockChainHeight = syncData.data.blockChainHeight const insightSyncHeight = syncData.data.height const insightSyncError = syncData.data.error utilsWallet.log(`appWorker >> ${self.workerId} getSyncInfo_Insight ${symbol} - request dispatch SET_ASSET_BLOCK_INFO...`) const dispatchActions = [] const updateSymbols = [symbol] if (symbol === 'BTC') { // don't send redundant requests: causes 429's - use BTC's request for BTC_SEG updateSymbols.push('BTC_SEG') updateSymbols.push('BTC_SEG2') } if (symbol === 'BTC') { updateSymbols.forEach(p => { dispatchActions.push({ type: actionsWallet.SET_ASSET_BLOCK_INFO, payload: { symbol: p, receivedBlockNo: receivedBlockNo || insightSyncBlockChainHeight, receivedBlockTime: receivedBlockTime || new Date().getTime(), insightSyncStatus, insightSyncBlockChainHeight, insightSyncHeight, insightSyncError }}) }) } self.postMessage({ msg: 'REQUEST_DISPATCH_BATCH', status: 'DISPATCH', data: { dispatchActions } }) // TODO: (as needed) - calc block_tps avg, per worker-geth & worker-blockbook //... // update lights if (networkStatusChanged) { updateSymbols.forEach(p => { networkStatusChanged(p, { block_no: receivedBlockNo, insight_url: configWS.insightApi_ws_config[p].url }) }) } } }) } function enrichTx(wallet, asset, tx, pollAddress) { return new Promise((resolve, reject) => { // wallet owner is part of cache key because of relative fields: tx.sendToSelf and tx.isIncoming const cacheKey = `${asset.symbol}_${wallet.owner}_txid_${tx.txid}` const ownAddresses = asset.addresses.map(p => { return p.addr }) //utilsWallet.log(`** enrichTx - ${asset.symbol} ${tx.txid}...`) // try cache first //utilsWallet.idb_tx.getItem(cacheKey) utilsWallet.txdb_getItem(cacheKey) .then((cachedTx) => { if (cachedTx && cachedTx.block_no != -1) { // requery unconfirmed tx's cachedTx.fromCache = true utilsWallet.log(`** enrichTx - ${asset.symbol} ${tx.txid} RET-CACHE`) resolve(cachedTx) // return from cache } else { //axiosRetry(axios, configWallet.AXIOS_RETRY_3PBP) axios.get(configExternal.walletExternal_config[asset.symbol].api.v2_tx(tx.txid)) .then((txData) => { if (txData && txData.data) { // map tx (prunes vins, drops vouts) var mappedTx = walletUtxo.map_insightTxs([txData.data], ownAddresses, asset)[0] //utilsWallet.log(`** enrichTx - ${asset.symbol} ${tx.txid} - adding to cache, mappedTx=`, mappedTx) // add to cache mappedTx.addedToCacheAt = new Date() //utilsWallet.idb_tx.setItem(cacheKey, mappedTx) utilsWallet.txdb_setItem(cacheKey, mappedTx) .then(() => { utilsWallet.log(`** enrichTx - ${asset.symbol} ${tx.txid} - added to cache ok`) mappedTx.fromCache = false resolve(mappedTx) }) .catch((err) => { utilsWallet.reportErr(err) utilsWallet.error(`## enrichTx - ${asset.symbol} ${tx.txid} - error writing cache=`, err) resolve(null) // allow all enrich ops to run }) } else { utilsWallet.warn(`enrichTx - ${asset.symbol} ${tx.txid} - no data from insight`) resolve(null) } }) } }) .catch((err) => { utilsWallet.reportErr(err) utilsWallet.error('## enrichTx - error=', err) resolve(null) }) }) }