scpx-wallet
Version:
Scoop Core Wallet: dual-signature timelock crypto wallet - multi-asset, cross-platform and open-source
810 lines (724 loc) • 43 kB
JavaScript
// Distributed under AGPLv3 license: see /LICENSE for terms. Copyright 2019-2021 Dominic Morris.
const io = require('socket.io-client')
const BigNumber = require('bignumber.js')
const _ = require('lodash')
const workerPrices = require('./worker-prices')
const workerWeb3 = require('./worker-web3')
const workerInsight = require('./worker-insight')
const workerGeth = require('./worker-geth')
const workerAddressMempool = require('./worker-blockbook-mempool')
const workerAddressMonitor = require('./worker-addr-monitor')
const workerPushTx = require('./worker-pushtx')
const workerExternal = require('./worker-external')
const workerBlockbook = require('./worker-blockbook')
const workerAccount = require('./worker-account')
const workerUtxo = require('./worker-insight')
const configWS = require('../config/websockets')
const configWallet = require('../config/wallet')
const configExternal = require('../config/wallet-external')
const walletExternal = require('../actions/wallet-external')
const walletP2shBtc = require('../actions/wallet-btc-p2sh')
const utilsWallet = require('../utils')
//import SubWorker_GetAddrFull from 'worker-loader!./subworker-get-addr-full.js'
// setup
var workerThreads = undefined
try {
workerThreads = require('worker_threads')
} catch(err) {
console.warn(`Failed to require(worker_threads): browser-env assumed...`)
}
const workerId = !workerThreads ? new Date().getTime() : workerThreads.threadId
if (workerThreads) { // server
workerThreads.parentPort.onmessage = handler
self = global
self.postMessage = (msg) => { return workerThreads.parentPort.postMessage(msg) }
}
else { // browser
onmessage = handler
}
self.window = self // for web3, and utilsWallet.getMainThreadGlobalScope in web worker context
self.workerId = !workerThreads ? new Date().getTime() : workerThreads.threadId
// sockets & webs3s
self.priceSocket = undefined // socket.io-client
self.insightSocketIos = {} // socket.io-client
self.blockbookSocketIos = {} // socket.io-client
self.bb_Sockets = {} // isomorphic-ws
self.bb_Sockets_messageID = []
self.bb_Sockets_pendingMessages = []
self.bb_Sockets_subscriptions = []
self.bb_Sockets_subId_NewBlock = []
self.bb_Sockets_keepAliveIntervalID = []
self.bb_Sockets_aborted = false
self.insight_OwnAddrTxIds = {} // server sending >1 new tx notification - processed inbound tx list; disregard if tx is already in this list (one list for all assets, probably fine!)
self.blockbook_OwnAddrTxIds = {} // "
self.geth_BlockNos = {} // similar issue to address monitors: geth web3 sub - disregard if block already processed (polled) -- seeing sometimes same block sent twice
self.geth_Sockets = {} // eth - isomorphic-ws - used for slightly faster tx and block polling compared to web3 subscriptions
self.web3_Sockets = {} // eth - web3 socket for faster balance polling compared to HttpProvider
// tx subscriptions - for throttling and TPS calcs
// self.mempool_tpsBuf = {}
// self.mempool_tpsAvg = {}
// self.mempool_tot = {}
// self.blocks_time = {}
// self.blocks_tps = {}
// self.blocks_height = {}
function resetConnectionStats() {
self.mempool_tpsBuf = {}
self.mempool_tpsAvg = {}
self.mempool_tot = {}
self.blocks_time = {}
self.blocks_tps = {}
self.blocks_height = {}
}
resetConnectionStats()
self.dirtyDbFile = 'scp_tx.db'
// error handlers
if (configWallet.WALLET_ENV === "SERVER") {
if (!configWallet.IS_DEV) {
process.on('unhandledRejection', (reason, promise) => {
utilsWallet.error(`## unhandledRejection (appWorker) - ${reason}`, promise, { logServerConsole: true })
})
process.on('uncaughtException', (err, origin) => {
utilsWallet.error(`## uncaughtException (appWorker) - ${err.toString()}`, origin, { logServerConsole: true })
})
}
}
utilsWallet.logMajor('green','white', `... appWorker - ${configWallet.WALLET_VER} (${configWallet.WALLET_ENV}) >> ${workerId} - workerThreads(node): ${workerThreads !== undefined} - init ...`, null, { logServerConsole: true })
//
// handler: for main-thread postMessage ==> app-worker
//
async function handler(e) {
if (!e) { utilsWallet.error(`appWorker >> ${workerId} no event data`); return Promise.resolve() }
const eventData = e.data !== undefined && e.data.data !== undefined ? e.data : e // node 10 experimental worker threads vs node 13 / brower env
if (!eventData.msg || !eventData.data) { utilsWallet.error(`appWorker >> ${workerId} bad event, workerThreads=${workerThreads} e=`, e); return Promise.resolve() }
const msg = eventData.msg
const data = eventData.data
// StMaster - read & apply passed stm payload (i.e. dynamic add to walletConfig et al...)
//utilsWallet.log(`StMaster - (app-worker) got data... >> ${workerId} - workerThreads(node): ${workerThreads !== undefined}`, data)
if (data !== undefined) {
if (data.stm_ApiPayload !== undefined) {
if (configWallet.get_stm_ApiPayload() === undefined) {
utilsWallet.log(`StMaster - (app-worker) setting stm_ApiPayload... >> ${workerId} - workerThreads(node): ${workerThreads !== undefined}`, data.stm_ApiPayload)
configWallet.set_stm_ApiPayload(data.stm_ApiPayload)
utilsWallet.log(`StMaster - (app-worker) set stm_ApiPayload... >> ${workerId} - configWallet.get_stm_ApiPayload()=`, configWallet.get_stm_ApiPayload())
await configWallet.getSupportedWalletTypes()
}
}
}
switch (msg) {
case 'SERVER_INIT_TX_DB': // setup tx db cache (dirty - replaces node-persist)
//utilsWallet.debug(`appWorker >> ${self.workerId} INIT_SERVER_DIRTY_DB...`, null, { logServerConsole: true })
dirtyDbInit()
break
// ## broken -- see dirtyDbClear
// case 'SERVER_NUKE_TX_DB':
// //utilsWallet.debug(`appWorker >> ${self.workerId} SERVER_NUKE_TX_DB...`)
// dirtyDbClear()
// break
case 'DIAG_PING': {
//utilsWallet.debug(`appWorker >> ${self.workerId} DIAG_PING...`)
const pongTime = new Date().getTime()
self.postMessage({ msg: 'DIAG_PONG', status: 'RES', data: { pongTime } })
break
}
case 'NOTIFY_USER': {
// posts the notification payload back to the main thread, so it can display accordingly
// (toastr notification in browser, console log on server)
//utilsWallet.debug(`appWorker >> ${self.workerId} NOTIFY_USER...`, data)
self.postMessage({ msg: 'NOTIFY_USER', status: 'RES', data })
break
}
case 'CONNECT_PRICE_SOCKET': {
//utilsWallet.debug(`appWorker >> ${self.workerId} CONNECT_PRICE_SOCKET...`)
workerPrices.priceSocket_Connect()
break
}
case 'FETCH_PRICES': {
workerPrices.fetch()
break
}
case 'DISCONNECT_PRICE_SOCKET': {
//utilsWallet.debug(`appWorker >> ${self.workerId} DISCONNECT_PRICE_SOCKET...`)
workerPrices.priceSocket_Disconnect()
break
}
case 'INIT_INSIGHT_SOCKETIO': {
//utilsWallet.debug(`appWorker >> ${self.workerId} INIT_INSIGHT_SOCKETIO...`)
workerInsight.socketio_Setup_Insight(networkConnected, networkStatusChanged, data.loaderWorker)
Object.values(configWallet.walletsMeta).filter(p => p.type === configWallet.WALLET_TYPE_UTXO && !p.use_BBv3).forEach(p => {
if (!configWallet.getSupportedMetaKeyBySymbol(p.symbol)) return
GetSyncInfo(p.symbol)
})
break
}
case 'INIT_GETH_ISOSOCKETS': {
//utilsWallet.debug(`appWorker >> ${self.workerId} INIT_GETH_ISOSOCKETS...`)
const setupCount = workerGeth.isosocket_Setup_Geth(networkConnected, networkStatusChanged, data.loaderWorker, data.walletSymbols)
if (setupCount > 0) {
utilsWallet.log(`appWorker >> ${self.workerId} INIT_GETH_ISOSOCKETS - DONE - (re)connected=`, setupCount, { logServerConsole: true })
}
break
}
case 'DISCONNECT_GETH_ISOSOCKETS': {
resetConnectionStats()
const disconnectCount = workerGeth.isosocket_Disconnect_Geth(networkConnected, networkStatusChanged, data.loaderWorker, data.walletSymbols)
if (disconnectCount > 0) {
utilsWallet.log(`appWorker >> ${self.workerId} DISCONNECT_GETH_ISOSOCKETS - DONE - disconnected=`, disconnectCount, { logServerConsole: true })
}
break
}
case 'INIT_BLOCKBOOK_ISOSOCKETS': {
self.bb_Sockets_aborted = false
const walletFirstPoll = data.walletFirstPoll == true
const timeoutMs = data.timeoutMs
const setupSymbols = workerBlockbook.isosocket_Setup_Blockbook(networkConnected, networkStatusChanged, data.loaderWorker, data.walletSymbols)
utilsWallet.log(`appWorker >> ${self.workerId} INIT_BLOCKBOOK_ISOSOCKETS... setupSymbols=`, setupSymbols)
if (setupSymbols.length > 0 || walletFirstPoll) {
const startWaitAt = new Date().getTime()
const wait_intId = setInterval(() => { // wait/poll for all sockets to be ready, then postback either success all or some failed
// if first wallet login, report on all asset sockets, otherwise just on those that were connected
const bbSocketValues =
walletFirstPoll
? Object.values(self.bb_Sockets)
: Object.values(self.bb_Sockets).filter(p => p === undefined || setupSymbols.some(p2 => p2 === p.symbol))
const bbSocketKeys =
walletFirstPoll
? Object.keys(self.bb_Sockets)
: setupSymbols
const allReady = bbSocketValues.some(p => !p || p.readyState != 1) === false
const symbolsConnected = bbSocketValues.filter(p => p && p.readyState == 1).map(p => p && p.symbol)
const displaySymbolsConnected = _.uniq(
bbSocketValues.filter(p => p && p.readyState == 1).map(p => Object.values(configWallet.walletsMeta).find(p2 => p2.symbol === p.symbol).displaySymbol)
)
const symbolsNotConnected = bbSocketValues.filter(p => p && p.readyState != 1).map(p => p.symbol).concat(bbSocketKeys.filter(p => self.bb_Sockets[p] === undefined))
const elapsedMs = new Date().getTime() - startWaitAt
//utilsWallet.debug(`appWorker >> ${self.workerId} INIT_BLOCKBOOK_ISOSOCKETS - elapsedMs=${elapsedMs} - allReady=`, allReady, { logServerConsole: true })
if (self.bb_Sockets_aborted) { // received disconnect (logout) signal?
clearInterval(wait_intId)
utilsWallet.log(`appWorker >> ${self.workerId} INIT_BLOCKBOOK_ISOSOCKETS - ABORTED`, null, { logServerConsole: true })
self.postMessage({ msg: 'BLOCKBOOK_ISOSOCKETS_DONE', status: 'RES', data: { walletFirstPoll, aborted: true } })
}
if (allReady) { // all requested connections setup
clearInterval(wait_intId)
if (symbolsConnected.length > 0) {
utilsWallet.log(`appWorker >> ${self.workerId} INIT_BLOCKBOOK_ISOSOCKETS - DONE - (re)connected=`, symbolsConnected.join(','), { logServerConsole: true })
}
self.postMessage({ msg: 'BLOCKBOOK_ISOSOCKETS_DONE', status: 'RES', data: { walletFirstPoll, symbolsConnected: displaySymbolsConnected, symbolsNotConnected } })
}
else { // some failed
if (elapsedMs > timeoutMs) {
clearInterval(wait_intId)
utilsWallet.error(`appWorker >> ${self.workerId} INIT_BLOCKBOOK_ISOSOCKETS - ## timeout elapsed: sockets still not all readyState=1 ##`, null, { logServerConsole: true })
self.postMessage({ msg: 'BLOCKBOOK_ISOSOCKETS_DONE', status: 'RES', data: { walletFirstPoll, symbolsConnected: displaySymbolsConnected, symbolsNotConnected } })
}
}
}, 888)
}
Object.values(configWallet.walletsMeta).filter(p => p.type === configWallet.WALLET_TYPE_UTXO && p.use_BBv3).forEach(p => {
if (!configWallet.getSupportedMetaKeyBySymbol(p.symbol)) return
GetSyncInfo(p.symbol)
})
break
}
case 'DISCONNECT_BLOCKBOOK_ISOSOCKETS': {
resetConnectionStats()
self.bb_Sockets_aborted = true
self.bb_Sockets = {}
const disconnectCount = workerBlockbook.isosocket_Disconnect_Blockbook(networkConnected, networkStatusChanged, data.loaderWorker, data.walletSymbols)
if (disconnectCount > 0) {
utilsWallet.log(`appWorker >> ${self.workerId} DISCONNECT_BLOCKBOOK_ISOSOCKETS - DONE - disconnected=`, disconnectCount, { logServerConsole: true })
}
break
}
case 'INIT_WEB3_SOCKET': {
//utilsWallet.debug(`appWorker >> ${self.workerId} INIT_WEB3_SOCKET...`)
var setupCount = workerWeb3.web3_Setup_SocketProvider(data.walletSymbols)
//if (data.wallet && data.wallet.assets) {
// TODO: take in data.wallet; iterate erc20's; call totalSupply() & postback
// const mainnetErc20s = data.wallet.assets.filter(p => p.addressType === configWallet.ADDRESS_TYPE_ETH && utilsWallet.isERC20(p) && !p.isErc20_Ropsten);
// const testnetErc20s = data.wallet.assets.filter(p => p.addressType === configWallet.ADDRESS_TYPE_ETH && utilsWallet.isERC20(p) && p.isErc20_Ropsten);
// utilsWallet.warn(`INIT_WEB3_SOCKET - mainnetErc20s`, mainnetErc20s)
// utilsWallet.warn(`INIT_WEB3_SOCKET - testnetErc20s`, testnetErc20s)
//...
//}
Object.values(configWallet.walletsMeta).filter(p => p.type === configWallet.WALLET_TYPE_ACCOUNT).forEach(p => {
if (!configWallet.getSupportedMetaKeyBySymbol(p.symbol)) return
GetSyncInfo(p.symbol)
})
if (setupCount > 0) {
utilsWallet.log(`appWorker >> ${self.workerId} INIT_WEB3_SOCKET - DONE - connected=`, setupCount, { logServerConsole: true })
}
break
}
case 'DISCONNECT_WEB3_SOCKET': {
resetConnectionStats()
const disconnectCount = workerWeb3.web3_Disconnect_SocketProvider(data.walletSymbols)
if (disconnectCount > 0) {
utilsWallet.log(`appWorker >> ${self.workerId} DISCONNECT_WEB3_SOCKET - DONE - disconnected=`, disconnectCount, { logServerConsole: true })
}
break
}
case 'GET_ETH_TX_FEE_WEB3': {
//utilsWallet.debug(`appWorker >> ${self.workerId} GET_ETH_TX_FEE_WEB3...`)
workerWeb3.getGasPrices(data.asset, data.params).then(result => {
utilsWallet.log('GET_ETH_TX_FEE_WEB3_DONE: posting back', result)
self.postMessage({ msg: 'GET_ETH_TX_FEE_WEB3_DONE', status: 'RES', data: { fees: result, assetSymbol: data.asset.symbol } })
})
break
}
case 'GET_ETH_ESTIMATE_TX_GAS':
//utilsWallet.debug(`appWorker >> ${self.workerId} GET_ETH_ESTIMATE_TX_GAS...`)
workerWeb3.estimateGasTx(data.asset, data.params).then(result => {
utilsWallet.log('GET_ETH_ESTIMATE_TX_GAS_DONE: posting back', result)
self.postMessage({ msg: 'GET_ETH_ESTIMATE_TX_GAS_DONE', status: 'RES', data: { fees: result, assetSymbol: data.asset.symbol } })
})
break
case 'GET_ETH_TX_HEX_WEB3': {
//utilsWallet.debug(`appWorker >> ${self.workerId} GET_ETH_TX_HEX_WEB3...`)
workerWeb3.createTxHex_Eth(data.asset, data.params, data.privateKey).then(result => {
utilsWallet.log('GET_ETH_TX_HEX_WEB3: posting back', result)
self.postMessage({ msg: 'GET_ETH_TX_HEX_WEB3_DONE', status: 'RES', data: { txHex: result, assetSymbol: data.asset.symbol } })
})
break
}
case 'GET_ERC20_TX_HEX_WEB3': {
//utilsWallet.debug(`appWorker >> ${self.workerId} GET_ERC20_TX_HEX_WEB3...`)
workerWeb3.createTxHex_erc20(data.asset, data.params, data.privateKey).then(result => {
utilsWallet.log('GET_ERC20_TX_HEX_WEB3: posting back', result)
self.postMessage({ msg: 'GET_ERC20_TX_HEX_WEB3_DONE', status: 'RES', data: { txHex: result, assetSymbol: data.asset.symbol } })
})
break
}
case 'PUSH_TX_WEB3': {
//utilsWallet.debug(`appWorker >> ${self.workerId} PUSH_TX_WEB3...`)
workerWeb3.pushRawTransaction_Account(data.payTo, data.asset, data.txHex).then(result => {
utilsWallet.log('PUSH_TX_WEB3: posting back', result)
self.postMessage({ msg: 'PUSH_TX_WEB3_DONE', status: 'RES', data: { res: result.res, err: result.err, assetSymbol: data.asset.symbol } })
})
break
}
case 'PUSH_TX_BLOCKBOOK': {
//utilsWallet.debug(`appWorker >> ${self.workerId} PUSH_TX_BLOCKBOOK...`)
workerPushTx.blockbook_pushTx(data.asset, data.txhex, data.wallet)
break
}
case 'CONNECT_ADDRESS_MONITORS': {
//utilsWallet.debug(`appWorker >> ${self.workerId} CONNECT_ADDRESS_MONITORS...`)
if (data && data.wallet) {
workerAddressMonitor.addressMonitors_Sub_Unsub(data.wallet, true)
}
break
}
case 'DISCONNECT_ADDRESS_MONITORS': {
//utilsWallet.debug(`appWorker >> ${self.workerId} DISCONNECT_ADDRESS_MONITORS...`)
if (data && data.wallet) {
workerAddressMonitor.addressMonitors_Sub_Unsub(data.wallet, false)
}
break
}
case 'STATE_RESPONSE': {
//utilsWallet.debug(`appWorker >> ${self.workerId} STATE_RESPONSE`)
const stateItem = data.stateItem
const stateKey = data.stateKey
const value = data.value
const context = data.context
if (stateItem === 'ASSET') {
const { asset, wallet, ux } = value
// process balance (& tx/utxo) updates
if (context === 'ASSET_REFRESH_ADDR_MONITOR') { // caller is an address monitor
//utilsWallet.log('DBG1 - ASSET_REFRESH_ADDR_MONITOR')
refreshAssetsFull([asset], wallet)
}
else if (context === 'ASSET_REFRESH_NEW_BLOCK') { // caller is new block subscriber
const pendingInitialLoad = wallet.assets.filter(p => p.lastAssetUpdateAt === undefined)
if (pendingInitialLoad.length > 0) {
utilsWallet.warn(`appWorker >> ${self.workerId} - ASSET_REFRESH_NEW_BLOCK - ${asset.symbol} - not all assets yet loaded: ignoring - pendingInitialLoad=`, pendingInitialLoad.map(p => p.symbol).join(', '))
}
else {
// if we have pending tx's, we want to do a full update, otherwise a lightweight balance update is sufficient
const unconfirmed_txs = walletExternal.getAll_unconfirmed_txs(asset)
const local_txs = walletExternal.getAll_local_txs(asset)
if (unconfirmed_txs.length > 0 || local_txs.length > 0) {
//utilsWallet.log('DBG1 - ASSET_REFRESH_NEW_BLOCK ' + asset.symbol + ' got pending txs -- doing full update...')
refreshAssetsFull([asset], wallet)
}
else {
//utilsWallet.log('DBG1 - ASSET_REFRESH_NEW_BLOCK ' + asset.symbol + ' no pending txs -- doing light update (balance refresh)...')
refreshAssetBalance(asset, wallet)
}
}
}
}
else {
utilsWallet.warn(`appWorker >> ${self.workerId} unexpected stateItem=`, stateItem)
}
break
}
//
// asset refresh requests - note: request to refresh an erc20 asset are actually requests to update eth
//
case 'REFRESH_ASSET_BALANCE': {
//utilsWallet.debug(`appWorker >> ${self.workerId} REFRESH_ASSET_BALANCE ${data.asset.symbol}...`)
refreshAssetBalance(data.asset, data.wallet)
break
}
case 'REFRESH_ASSET_FULL': {
utilsWallet.logMajor('magenta','blue', `appWorker >> ${self.workerId} REFRESH_ASSET_FULL ${data.asset.symbol}...`, null, { logServerConsole: true })
refreshAssetsFull([data.asset], data.wallet)
break
}
case 'REFRESH_MULTI_ASSET_FULL': {
utilsWallet.warn(`appWorker >> ${self.workerId} REFRESH_MULTI_ASSET_FULL ${data.assets.map(p => p.symbol).join()}...`)
refreshAssetsFull(data.assets, data.wallet)
break
}
case 'POST_OFFLINE_CHECK': {
//utilsWallet.debug(`appWorker >> ${self.workerId} POST_OFFLINE_CHECK...`)
postOfflineCheck()
break
}
// arbitrary address balances -- used by privkey import; consolidated return format, unlike wallet-external
case 'GET_ANY_ADDRESS_BALANCE': {
const addrs = data.addrs
utilsWallet.logMajor(`appWorker >> ${self.workerId} GET_ANY_ADDRESS_BALANCE... asset, addrs=`, data.asset, data.addrs)
//debugger
if (data.asset.symbol === 'ETH' || data.asset.symbol === 'ETH_TEST' || utilsWallet.isERC20(data.asset.symbol)) {
const ops = data.addrs.map(addr => { return workerAccount.getAddressBalance_Account(data.asset.symbol, addr, false) })
Promise.all(ops)
.then(results => {
const balanceData =
results.filter(p => p != null)
.map(p => { return {
addr: p.address,
bal: {
symbol: data.asset.symbol,
balance: new BigNumber(p.bal).toString(),
unconfirmedBalance: new BigNumber(0).toString(),
}
}})
self.postMessage({ msg: 'ADDRESS_BALANCE_RESULT', status: 'RES', data: balanceData })
})
}
else {
const balanceUpdateFn = data.asset.use_BBv3 // BB WS interface bulk seems much better
? workerBlockbook.getAddressBalance_Blockbook_v3
: workerInsight.getAddressBalance_Insight
const ops = data.addrs.map(addr => { return balanceUpdateFn(data.asset, addr, false) })
Promise.all(ops)
.then(results => {
const balanceData =
results.filter(p => p != null)
.map(p => { return {
addr: p.address,
bal: {
symbol: data.asset.symbol,
balance: p.balance.toString(),
unconfirmedBalance: p.unconfirmedBalance.toString(),
}
}})
self.postMessage({ msg: 'ADDRESS_BALANCE_RESULT', status: 'RES', data: balanceData })
})
}
break
}
// get initial block/sync info
case 'GET_SYNC_INFO': {
//if (configWallet.getSupportedMetaKeyBySymbol(data.symbol)) {
GetSyncInfo(data.symbol)
//}
break
}
// scan for non-standard addresses - add any found to our address-monitor list
case 'SCAN_NON_STANDARD_ADDRESSES': {
utilsWallet.logMajor('magenta','blue', `appWorker >> ${self.workerId} SCAN_NON_STANDARD_ADDRESSES ${data.asset.symbol}...`, null, { logServerConsole: true })
const dispatchActions = []
const nonStdAddrs_Txs = [] // { nonStdAddr, protect_op_txid }
walletP2shBtc.scan_NonStdOutputs({ asset: data.asset, dispatchActions, nonStdAddrs_Txs },)
var mergedDispatchActions = mergeDispatchActions(data.asset, dispatchActions)
if (mergedDispatchActions.length > 0) {
//utilsWallet.logMajor('magenta','blue', `appWorker >> ${self.workerId} SCAN_NON_STANDARD_ADDRESSES ${data.asset.symbol}, mergedDispatchActions=`, mergedDispatchActions, { logServerConsole: true })
self.postMessage({ msg: 'REQUEST_DISPATCH_BATCH', status: 'DISPATCH', data: { dispatchActions: mergedDispatchActions } })
}
else utilsWallet.log(`appWorker >> ${self.workerId} SCAN_NON_STANDARD_ADDRESSES... no dispatch actions found`)
if (nonStdAddrs_Txs.length > 0) {
//utilsWallet.logMajor('magenta','blue', `appWorker >> ${self.workerId} SCAN_NON_STANDARD_ADDRESSES ${data.asset.symbol}, nonStdAddrs_Txs=`, nonStdAddrs_Txs, { logServerConsole: true })
self.postMessage({ msg: 'ADD_NON_STANDARD_ADDRESSES', status: 'EXEC', data: { asset: data.asset, nonStdAddrs_Txs } })
}
else utilsWallet.log(`appWorker >> ${self.workerId} SCAN_NON_STANDARD_ADDRESSES... no new non-std addr's found`)
break
}
}
return Promise.resolve()
function GetSyncInfo(symbol) {
//utilsWallet.debug(`appWorker >> ${self.workerId} ${symbol} GET_SYNC_INFO...`)
if ((symbol === 'ZEC_TEST' && !configWallet.WALLET_INCLUDE_ZEC_TEST)
|| (symbol === 'LTC_TEST' && !configWallet.WALLET_INCLUDE_LTC_TEST)
|| (symbol === 'BTC_TEST' && !configWallet.WALLET_INCLUDE_BTC_TEST)
|| (symbol === 'ETH_TEST' && !configWallet.WALLET_INCLUDE_ETH_TEST)
) {
return
}
const meta = configWallet.getMetaBySymbol(symbol)
if (meta.type === configWallet.WALLET_TYPE_UTXO) {
// don't send redundant requests: causes 429's
// (BTC's GetSyncInfo will update BTC_SEG)
if (symbol === 'BTC_SEG' || symbol === 'BTC_SEG2') return
if (meta.use_BBv3) {
workerBlockbook.getSyncInfo_Blockbook_v3(symbol, undefined, undefined, networkStatusChanged)
}
else {
workerInsight.getSyncInfo_Insight(symbol, undefined, undefined, networkStatusChanged)
}
}
else if (meta.type === configWallet.WALLET_TYPE_ACCOUNT) {
if (symbol === 'ETH' || symbol === 'ETH_TEST') {
workerGeth.getSyncInfo_Geth(symbol, undefined, undefined, networkStatusChanged)
}
}
}
//
// main actions for asset address balance & tx updates
// these fn's populate the store data after retrieving data from 3PBPs (blockbook, insight, web3)
//
function refreshAssetsFull(assets, wallet) { //}, utxo_known_spentTxIds) {
var allDispatchActions = []
const refreshAssetOps = assets.map((asset) => {
return new Promise((resolveAssetOp, rejectAssetOp) => {
// !! different creation semantics for node? (maybe not after v13 upgrade)
// const subWorker = new SubWorker_GetAddrFull()
// subWorker.addEventListener('message', e => {
// const message = e.data;
// console.log(`[From subWorker]: ${message}`);
// })
// subWorker.postMessage({asset, wallet});
// but this approach fails -- because get_BlockbookSocketIo() socket can't be shared to the child worker
//****
workerAddressMempool.mempool_get_BB_txs(asset, wallet) //, (utxo_mempool_spentTxIds) => {
//utilsWallet.debug(`appWorker >> ${self.workerId} refreshAssetsFull ${asset.symbol}`) // - utxo_mempool_spentTxIds=`, utxo_mempool_spentTxIds)
//console.time(`refreshAssetFull_${asset.symbol}`)
// get BB scoket, for account types (needed for ETH v2)
var bbSocket
if (asset.type === configWallet.WALLET_TYPE_ACCOUNT && asset.symbol !== 'EOS') {
bbSocket = get_BlockbookSocketIo(asset)
}
// when called from worker-pushtx, we can augment BB's mempool (which lags) with known spent txid's
// deprecated - utxo_known_spentTxIds
//const spentTxIds = _.uniq(utxo_mempool_spentTxIds.concat(utxo_known_spentTxIds))
// query each address
var assetDispatchActions = []
const refreshAddrOps = asset.addresses.map(a => {
return new Promise((resolveAddrOp, rejectAddrOp) => {
const addrNdx = asset.addresses.findIndex(p => p.addr === a.addr)
// ### d+10 eth this is *failing* (intermittent geth WS issue?) but callback always resolves
// so, lastAssetUpdateAt is being set, and loadAllAssets sees eth as "done"
// 1 - need to detect failure state here
// 2 - need a new flag "allAddressesLoaded" only set on happy path
//...
workerExternal.getAddressFull_External({ wallet, asset, addrNdx, bbSocket, /*utxo_mempool_spentTxIds: spentTxIds,*/ }, (dispatchActions) => {
if (dispatchActions.length > 0) {
assetDispatchActions = [...assetDispatchActions, ...dispatchActions]
}
resolveAddrOp()
})
})})
//****
Promise.all(refreshAddrOps)
.then((res) => {
// web3 eth cleanup -- needed for ETH dedicatedWeb3 cleanup
for (var addrNdx=0 ; addrNdx < asset.addresses.length ; addrNdx++) {
workerExternal.getAddressFull_Cleanup({ wallet, asset, addrNdx })
}
// merge asset dispatch actions
if (assetDispatchActions.length > 0) {
//utilsWallet.log(`appWorker >> ${self.workerId} - refreshAssetsFull - ${asset.symbol} - allDispatchActions.length=${allDispatchActions.length}`)
allDispatchActions = [...allDispatchActions, ...mergeDispatchActions(asset, assetDispatchActions)]
}
resolveAssetOp()
})
})
})
Promise.all(refreshAssetOps)
.then((res) => {
// dispatch merged asset actions to reducer - all assets, all addresses one batch
if (allDispatchActions.length > 0) {
utilsWallet.log(`appWorker >> ${self.workerId} - refreshAssetsFull - ${assets.map(p => p.symbol).join()} - allDispatchActions.length=${allDispatchActions.length}`)
self.postMessage({ msg: 'REQUEST_DISPATCH_BATCH', status: 'DISPATCH', data: { dispatchActions: allDispatchActions } } ) // post dispatch batch request
}
})
}
function refreshAssetBalance(asset, wallet) {
workerAddressMempool.mempool_get_BB_txs(asset, wallet) //, (utxo_mempool_spentTxIds) => {
//utilsWallet.debug(`appWorker >> ${self.workerId} refreshAssetBalance ${asset.symbol}`) // - utxo_mempool_spentTxIds=`, utxo_mempool_spentTxIds)
// get BB scoket, for account types (needed for ETH v2)
var bbSocket
if (asset.type === configWallet.WALLET_TYPE_ACCOUNT && asset.symbol !== 'EOS') {
bbSocket = get_BlockbookSocketIo(asset)
}
var allDispatchActions = []
const refreshOps = asset.addresses.map(a => {
return new Promise((resolve, reject) => {
const addrNdx = asset.addresses.findIndex(p => p.addr === a.addr)
workerExternal.getAddressBalance_External({ wallet, asset, addrNdx, /*utxo_mempool_spentTxIds,*/ bbSocket },
(dispatchActions) => {
if (dispatchActions.length > 0) {
allDispatchActions = [...allDispatchActions, ...dispatchActions]
}
resolve()
})
})})
Promise.all(refreshOps)
.then((res) => {
if (allDispatchActions.length > 0) {
//utilsWallet.debug(`appWorker >> ${self.workerId} refreshAssetBalance - ${asset.symbol} allDispatchActions.length=${allDispatchActions.length}`)
allDispatchActions = mergeDispatchActions(asset, allDispatchActions)
self.postMessage({ msg: 'REQUEST_DISPATCH_BATCH', status: 'DISPATCH', data: { dispatchActions: allDispatchActions } } ) // post dispatch batch request
}
})
}
// perf - transmogrify multiple WCORE_SET_ADDRESS_FULL / WCORE_SET_ENRICHED_TXS actions into a single WCORE_SET_ADDRESSES_FULL_MULTI / WCORE_SET_ENRICHED_TXS_MULTI
// (results in one store update instead of thousands)
function mergeDispatchActions(asset, allDispatchActions) {
// n WCORE_SET_ADDRESS_FULL ==> 1 WCORE_SET_ADDRESSES_FULL_MULTI
const setAddressFullActions = allDispatchActions.filter(p => p.type === 'WCORE_SET_ADDRESS_FULL')
if (setAddressFullActions.length > 0) {
const payloadAddresses = setAddressFullActions.map(p => p.payload.newAddr) // n payload.newAddr ==> 1 payload.newAddresses[]
const newAction_setAddressFull_Multi = {
type: 'WCORE_SET_ADDRESSES_FULL_MULTI',
payload: {
newAddresses: payloadAddresses,
symbol: asset.symbol,
updateAt: new Date()
}
}
allDispatchActions = allDispatchActions.filter(p => p.type !== 'WCORE_SET_ADDRESS_FULL')
allDispatchActions = allDispatchActions.concat(newAction_setAddressFull_Multi)
}
// n WCORE_SET_ENRICHED_TXS ==> 1 WCORE_SET_ENRICHED_TXS_MULTI
const enrichTxActions = allDispatchActions.filter(p => p.type === 'WCORE_SET_ENRICHED_TXS')
if (enrichTxActions.length > 0) {
const payloadAddrTxs = enrichTxActions.map(p => {
return { addr: p.payload.addr, txs: p.payload.txs, res: p.payload.res } } ) // n payload.addr ==> 1 payload.addrTxs
const newAction_setEnrichedTxs_Multi = {
type: 'WCORE_SET_ENRICHED_TXS_MULTI',
payload: {
addrTxs: payloadAddrTxs,
symbol: asset.symbol,
updateAt: new Date()
}
}
allDispatchActions = allDispatchActions.filter(p => p.type !== 'WCORE_SET_ENRICHED_TXS')
allDispatchActions = allDispatchActions.concat(newAction_setEnrichedTxs_Multi)
}
return allDispatchActions
}
//
// server file cache (npm dirty)
//
function dirtyDbInit() {
utilsWallet.log(`global.txdb_dirty: init...`, null, { logServerConsole: true })
global.txdb_dirty = require('dirty')(self.dirtyDbFile)
global.txdb_dirty.on('load', function() {
utilsWallet.log(`global.txdb_dirty: init OK.`, null, { logServerConsole: true })
self.postMessage({ msg: 'SERVER_INIT_TX_DB_DONE', status: 'RES', data: { } })
})
}
/*function dirtyDbClear() { // ## broken
utilsWallet.log(`global.txdb_dirty: clear...`, null, { logServerConsole: true })
// this fails to actually delete lines - just appends new undefined lines (so key declarations are duplicated)
// global.txdb_dirty.forEach((key,val) => {
// //global.txdb_dirty.rm(key, (e) => {
// global.txdb_dirty.set(key, undefined)
// utilsWallet.log(`dirty: remove ${key}...`, { logServerConsole: true })
// //})
// })
// this works
global.txdb_dirty.close()
const fs = require('fs')
const exists = fs.existsSync(self.dirtyDbFile)
if (exists) {
fs.unlinkSync(self.dirtyDbFile)
}
// but this (no matter how/where dirty is re-init'd) causes CLI commands to get written to the file after re-init (?!)
//sglobal.txdb_dirty = require('dirty')(self.dirtyDbFile)
// so we're left without a txdb, and no way to reinitialize it
self.postMessage({ msg: 'SERVER_NUKE_TX_DB_DONE', status: 'RES', data: {} })
}*/
//
// network, misc
//
function postOfflineCheck() {
httpGetAsyncNoCache(configWallet.API_URL + 'ol', (xmlHttp) => {
self.postMessage({ msg: 'OFFLINE_CHECK_RESPONSE', status: 'RES', data: {
xmlHttpStatus: !xmlHttp ? undefined : xmlHttp.status,
responseText: !xmlHttp ? undefined : xmlHttp.responseText
} })
xmlHttp = null
})
}
function httpGetAsyncNoCache(theUrl, callback) {
var xmlHttp = new XMLHttpRequest()
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4)
callback(xmlHttp)
}
xmlHttp.open("GET", theUrl + '?q=' + new Date().getTime(), true) // true for asynchronous
//xmlHttp.setRequestHeader('Cache-Control', 'no-cache')
xmlHttp.send(null)
}
function networkStatusChanged(symbol, info) {
////utilsWallet.debug(`appWorker >> ${self.workerId} networkStatusChanged ${symbol} txid=${txid}`)
self.postMessage({ msg: 'NETWORK_STATUS_CHANGE', status: 'ok', data: { symbol, info } })
}
function networkConnected(symbol, connected) {
//utilsWallet.debug(`appWorker >> ${self.workerId} networkConnected ${symbol} connected=${connected}`)
self.postMessage({ msg: 'NETWORK_CONNECTED_CHANGE', status: 'ok', data: { symbol, connected } })
}
}
self.get_BlockbookSocketIo = function(asset) {
const socketToUse =
asset.symbol === 'ETH_TEST' || asset.isErc20_Ropsten ? 'ETH_TEST'
: utilsWallet.isERC20(asset) ? 'ETH'
: asset.symbol
var socket = self.blockbookSocketIos[socketToUse]
if (socket === undefined) {
if (configWS.blockbook_ws_config[socketToUse] === undefined) {
utilsWallet.error(`appWorker >> ${self.workerId} get_BlockbookSocketIo ${asset.symbol}: no socket config!`)
}
else {
try {
//utilsWallet.debug(`appWorker >> ${self.workerId} get_BlockbookSocketIo ${asset.symbol}: creating new socket...`)
const ws_url = new URL(configWS.blockbook_ws_config[socketToUse].url)
socket = io(configWS.blockbook_ws_config[socketToUse].url, {
transports: ['websocket'],
transportOptions: {
websocket: {
extraHeaders: {
"User-Agent": configExternal.blockbookHeaders["User-Agent"], //"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
"Connection": configExternal.blockbookHeaders["Connection"], //"Upgrade",
"Upgrade": configExternal.blockbookHeaders["Upgrade"], //"websocket",
"Sec-WebSocket-Extensions": configExternal.blockbookHeaders["Sec-WebSocket-Extensions"], //"permessage-deflate; client_max_window_bits",
"Sec-WebSocket-Version": configExternal.blockbookHeaders["Sec-WebSocket-Version"], //"13",
"Accept-Encoding": configExternal.blockbookHeaders["Accept-Encoding"], //"gzip, deflate, br",
"Accept-Language": configExternal.blockbookHeaders["Accept-Language"], //"en-US,en;q=0.9,id;q=0.8",
"Cache-Control": configExternal.blockbookHeaders["Cache-Control"], //"no-cache",
"Pragma": configExternal.blockbookHeaders["Pragma"], //"no-cache",
"Host": ws_url.hostname,
"Origin": ws_url.origin.replace('wss', 'https'),
}
}
}
})
self.blockbookSocketIos[socketToUse] = socket
socket.on('connect', function() {
utilsWallet.warn(`appWorker >> ${self.workerId} BLOCKBOOK WS ${asset.symbol} - IO - connected...`)
})
socket.on('reconnect', () => {
utilsWallet.log(`appWorker >> ${self.workerId} BLOCKBOOK WS ${asset.symbol} - IO - reconnected...`)
})
}
catch(err) {
utilsWallet.error(`appWorker >> ${self.workerId} BLOCKBOOK WS - err=`, err)
}
}
}
return socket
}