UNPKG

scpx-wallet

Version:

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

775 lines (683 loc) 43.6 kB
// Distributed under AGPLv3 license: see /LICENSE for terms. Copyright 2019-2021 Dominic Morris. const _ = require('lodash') const configWS = require('../config/websockets') const configExternal = require('../config/wallet-external') const configWallet = require('../config/wallet') const axios = require('axios'); configExternal.blockbookHeaders.set(axios, configExternal.blockbookHeaders) const isoWs = require('isomorphic-ws') const BigNumber = require('bignumber.js') const CircularBuffer = require("circular-buffer") const walletUtxo = require('../actions/wallet-utxo') const actionsWallet = require('../actions') const utilsWallet = require('../utils') const cache_bb_blocks = {} module.exports = { // BB v3 getAddressFull_Blockbook_v3: (wallet, asset, address, utxo_mempool_spentTxIds, allDispatchActions) => { return getAddressFull_Blockbook_v3(wallet, asset, address, utxo_mempool_spentTxIds, allDispatchActions) }, getAddressBalance_Blockbook_v3: (asset, address) => { return getAddressBalance_Blockbook_v3(asset, address) }, // converts blockbook tx format to insight-api format mapTx_BlockbookToInsight: (asset, bbTx) => { return mapTx_BlockbookToInsight(asset, bbTx) }, // called for initial block-sync state - and from new blocks // also -- called for keep-alives of the WS connections (for direct trezor node connections) getSyncInfo_Blockbook_v3: (symbol, receivedBlockNo = undefined, receivedBlockTime = undefined, networkStatusChanged = undefined) => { return getSyncInfo_Blockbook_v3(symbol, receivedBlockNo, receivedBlockTime, networkStatusChanged) }, // blockbook isosockets: note this is needed for a different BB API/interface compared to get_BlockbookSocketIo() // considered VOLATILE -- no built-in reconnect isosocket_Setup_Blockbook: (networkConnected, networkStatusChanged, loaderWorker, walletSymbols) => { return isosocket_Setup_Blockbook(networkConnected, networkStatusChanged, loaderWorker, walletSymbols) }, isosocket_Disconnect_Blockbook: (networkConnected, networkStatusChanged, loaderWorker, walletSymbols) => { return isosocket_Disconnect_Blockbook(networkConnected, networkStatusChanged, loaderWorker, walletSymbols) }, isosocket_send_Blockbook: (x, method, params, callback) => { return isosocket_send_Blockbook(x, method, params, callback) } } // BB v3 function getAddressFull_Blockbook_v3(wallet, asset, address, utxo_mempool_spentTxIds, allDispatchActions) { const symbol = asset.symbol //utilsWallet.log(`getAddressFull_Blockbook_v3 ${symbol}...`) return new Promise((resolve, reject) => { isosocket_send_Blockbook(symbol, 'getAccountInfo', { descriptor: address, details: 'txids', // { basic | balance | txids | txs } page: undefined, pageSize: configWallet.WALLET_MAX_TX_HISTORY || 888, from: undefined, to: undefined, contractFilter: undefined }, (balanceAndTxData) => { if (!balanceAndTxData) { utilsWallet.error(`## getAddressFull_Blockbook_v3 ${symbol} ${address} - no balanceAndTxData!`); reject(); return } // axiosRetry(axios, CONST.AXIOS_RETRY_EXTERNAL) // axios.get(configExternal.walletExternal_config[symbol].api.utxo(address)) // .then(async (utxoData) => { isosocket_send_Blockbook(symbol, 'getAccountUtxo', {descriptor: address} , async (utxosData) => { // if (address === '32FtNE5ShUDh4wQJm3bGYGtjKpFeJqeVEw') { // debugger // } if (!utxosData) { utilsWallet.error(`## getAddressFull_Blockbook_v3 ${symbol} ${address} - no utxosData`); reject(); return } if (utxosData.error) { utilsWallet.error(`## getAddressFull_Blockbook_v3 ${symbol} ${address} - errored utxosData`, utxosData.error); reject(); return } if (!Array.isArray(utxosData)) { utilsWallet.error(`## getAddressFull_Blockbook_v3 ${symbol} ${address} - invalid utxosData type`); reject(); return } const getUtxoSpecificOps = utxosData.map(utxo => { return new Promise((resolveSpecificUtxoOp) => { isosocket_send_Blockbook(symbol, 'getTransactionSpecific', { txid: utxo.txid } , async (utxoSpecificData) => { //utilsWallet.debug(`blockbook tx ${utxo.txid} for ${address} utxoSpecificData`, utxoSpecificData) // if (utxo.txid === '9df6df34bad59ed85f0001aeb02f180634a4fd027dc5477c4489300b70669d00') { // debugger // } if (!utxoSpecificData) { utilsWallet.error(`## getAddressFull_Blockbook_v3 ${symbol} ${utxo.txid} - no utxoSpecificData!`); resolveSpecificUtxoOp([]); return } if (utxoSpecificData.error) { //debugger // 10:07:15.116 [SW-ERR] ## getAddressFull_Blockbook_v3 BTC_TEST dce42dd5cc1d0810f6a5fba36e3965ee16dcb0b2b936c7feb18a9ffc1dd73b08 - error on getTransactionSpecific: "txid dce42dd5cc1d0810f6a5fba36e3965ee16dcb0b2b936c7feb18a9ffc1dd73b08: 500 Internal Server Error invalid character 'W' looking for beginning of value" // # seems inconsistent on btc_test; care if it repro's on mainnet -- NEW RATE LIMIT ON BLOCKBOCK NODES? // TODO: setup own BTC_TEST BB NODE... utilsWallet.error(`## getAddressFull_Blockbook_v3 ${symbol} ${utxo.txid} - error on getTransactionSpecific: ${JSON.stringify(utxoSpecificData.error.message)}`); resolveSpecificUtxoOp([]); return } if (!utxoSpecificData.vout) { utilsWallet.error(`## getAddressFull_Blockbook_v3 ${symbol} ${utxo.txid} - no utxoSpecificData.vout!`); resolveSpecificUtxoOp([]); return } // DMS - add all UTXOs for this TX that correspond to the query account // (or, that are cross-address/account OP_RETURN embeded data UTXOs) const resolveSpecificUtxos = [] for (var j = 0; j < utxoSpecificData.vout.length; j++) { const utxoSpecific = utxoSpecificData.vout[j] // // DMS: we *include* OP_RETURN outputs - we'll use the op_return data to allow beneficiary & benefactor to create the locking script (i.e. the address) // for the "protected" non-standard P2SH(DSIG/CLTV) outputs... // if ((utxo.vout == utxoSpecific.n && ( (utxoSpecific.scriptPubKey.addresses !== undefined && utxoSpecific.scriptPubKey.addresses.includes(address)) // p2sh || (utxoSpecific.scriptPubKey.address !== undefined && utxoSpecific.scriptPubKey.address == address && (utxoSpecific.scriptPubKey.type == 'witness_v0_keyhash' // p2wpkh || utxoSpecific.scriptPubKey.type == 'scripthash' // p2sh ) ) ) || (utxoSpecific.scriptPubKey.addresses === undefined && utxoSpecific.scriptPubKey.type === "nulldata") // op_return )) { resolveSpecificUtxos.push({ satoshis: Number(new BigNumber(utxoSpecific.value).times(1e8).toString()), //Number(utxo.value), txid: utxo.txid, vout: utxoSpecific.n, //utxo.vout scriptPubKey: { addresses: utxoSpecific.scriptPubKey.addresses || [utxoSpecific.scriptPubKey.address], hex: utxoSpecific.scriptPubKey.hex, type: utxoSpecific.scriptPubKey.type, } }) } } resolveSpecificUtxoOp(resolveSpecificUtxos) }) }) }) const utxoSpecifics = await Promise.all(getUtxoSpecificOps) const utxosFlattened = _.flatten(utxoSpecifics) // utxo's // console.log('blockbook_utxoData', utxoData) // const utxos = utxoData.map(p => { return { // satoshis: Number(p.value), // txid: p.txid, // vout: p.vout, // // TODO: *need* scriptPubKey.hex -- for new PSBT input... // } }) // it turns out that getAccountInfo(txids) does *not* return PROTECT_OP TX's; so, we must union with getAccountUtxo()'s txids (which does return p_op UTXOs) const addrTxs = _.union(_.uniq(utxosData.map(p => p.txid)), balanceAndTxData.txids || []) const totalTxCount = addrTxs.length // 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 == address) const newMinimalTxs = addrTxs.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: balanceAndTxData.balance, unconfirmedBalance: balanceAndTxData.unconfirmedBalance, utxos: utxosFlattened, totalTxCount, cappedTxs: addrTxs.length < totalTxCount, } if (newMinimalTxs.length > 0) { // queue enrich tx actions const enrichOps = newMinimalTxs.map((tx) => { return enrichTx(wallet, asset, tx, address) }) // update batch await Promise.all(enrichOps) .then((enrichedTxs) => { const dispatchTxs = enrichedTxs.filter(p => p != null) if (dispatchTxs.length > 0) { //utilsWallet.debug(`getAddressFull_Blockbook_v3 ${symbol} ${address} - 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: address, txs: dispatchTxs, res } } allDispatchActions.push(dispatchAction) } }) } // pass through the state update -- in v1 getAddressFull format const ret = Object.assign({}, res, { txs: newMinimalTxs } ) resolve(ret) }) }) }) } // converts blockbook tx format to insight-api format function mapTx_BlockbookToInsight(asset, bbTx) { const insightTx = { txid: bbTx.txid, version: bbTx.version, blockhash: (bbTx.blockhash !== undefined ? bbTx.blockhash : bbTx.blockHash !== undefined ? bbTx.blockHash : undefined), blockheight: (bbTx.blockheight == 0 || bbTx.blockHeight == 0) ? -1 : (bbTx.blockheight != undefined ? bbTx.blockheight : bbTx.blockHeight !== undefined ? bbTx.blockHeight : undefined), confirmations: bbTx.confirmations, time: (bbTx.blocktime !== undefined ? bbTx.blocktime : bbTx.blockTime !== undefined ? bbTx.blockTime : undefined), blocktime: (bbTx.blocktime !== undefined ? bbTx.blocktime : bbTx.blockTime !== undefined ? bbTx.blockTime : undefined), valueOut: Number(utilsWallet.toDisplayUnit(new BigNumber(bbTx.value), asset)), valueIn: Number(utilsWallet.toDisplayUnit(new BigNumber(bbTx.valueIn), asset)), fees: Number(utilsWallet.toDisplayUnit(new BigNumber(bbTx.fees), asset)), //size: bbTx.hex.length, } if (bbTx.vin === undefined) { debugger } insightTx.vin = bbTx.vin.map(p => { return { txid: p.txid, vout: p.vout, sequence: p.sequence, n: p.n, addr: p.addresses[0], valueSat: Number(p.value), value: Number(utilsWallet.toDisplayUnit(new BigNumber(p.value), asset)), //doubleSpentTxID: null, //scriptSig: ... } }) insightTx.vout = bbTx.vout.map(p => { return { value: utilsWallet.toDisplayUnit(new BigNumber(p.value), asset), n: p.n, scriptPubKey: { hex: p.hex, addresses: p.addresses, //asm: null, //type: null, }, //spentTxId: null, //spentIndex: null, //spentHeight: null, } }) //console.log('bbTx', bbTx) //console.log('insightTx', insightTx) return insightTx } function getAddressBalance_Blockbook_v3(asset, address) { const symbol = asset.symbol //utilsWallet.debug(`getAddressBalance_Blockbook_v3 ${symbol} ${address}...`) return new Promise((resolve, reject) => { const params = { descriptor: address, details: 'balance', // { basic | balance | txids | txs } page: undefined, pageSize: 10, from: undefined, to: undefined, contractFilter: undefined } isosocket_send_Blockbook(symbol, 'getAccountInfo', params, (data) => { //utilsWallet.debug(`getAddressBalance_Blockbook_v3 ${symbol} ${address} - data=`, data) if (data) { resolve({ symbol, balance: new BigNumber(data.balance), unconfirmedBalance: new BigNumber(data.unconfirmedBalance), address, }) } else { resolve({ symbol, balance: new BigNumber(0), unconfirmedBalance: new BigNumber(0) }) } }) }) } // called for initial block-sync state - and new blocks function getSyncInfo_Blockbook_v3(symbol, _receivedBlockNo = undefined, _receivedBlockTime = undefined, networkStatusChanged = undefined) { //utilsWallet.debug(`getSyncInfo_Blockbook_v3 ${symbol}...`) // cache BB rest data so we can reuse across tests (across wallet load/worker load cycles) - we get 429's otherwise async function bb_getBlock(blockNo, page) { //console.log('bb_getBlock - cache_bb_blocks', cache_bb_blocks) if (!cache_bb_blocks[symbol]) cache_bb_blocks[symbol] = new CircularBuffer(10) const url = configExternal.walletExternal_config[symbol].api.block(blockNo, page) const cache = cache_bb_blocks[symbol] if (cache.size() > 0 && cache.get(cache.size() - 1).url == url) { //console.log('bb_getBlock - returning cached for', url) return new Promise((resolve) => { resolve( cache.get(cache.size() - 1).data )}) } else { //console.log('bb_getBlock - fetching for', url) return axios.get(url) .then(blockData => { if (!blockData || !blockData.data) return null //console.log(`caching for ${url}, data=`, blockData.data) cache.push({ url, data: blockData.data }) //console.log(`returning for ${url}, data=`, blockData.data) return blockData.data }) .catch(err => { //utilsWallet.error(`## bb_getBlock, err=`, err, { logServerConsole: true }) return null }) } } // get node sync info isosocket_send_Blockbook(symbol, 'getInfo', {}, async (data) => { if (!configExternal.walletExternal_config[symbol].api) return const dispatchActions = [] // get current block - exact time & tx count const receivedBlockNo = _receivedBlockNo || data.bestheight || data.bestHeight const curBlock = await bb_getBlock(receivedBlockNo, 1) //console.log('curBlock', curBlock) const txCount = curBlock ? (curBlock.txCount ? curBlock.txCount : 0) : undefined const receivedBlockTime = curBlock ? curBlock.time : undefined // get prev block - exact time; for block TPS const cacheSymbol = symbol === 'BTC_SEG' || symbol === 'BTC_SEG2' ? 'BTC' : symbol // don't send synonymous requests (http 429) if (!self.blocks_time[cacheSymbol]) self.blocks_time[cacheSymbol] = [] if (!self.blocks_tps[cacheSymbol]) self.blocks_tps[cacheSymbol] = [] if (!self.blocks_height[cacheSymbol]) self.blocks_height[cacheSymbol] = 0 var block_time = 0 if (txCount && receivedBlockTime) { if (!self.blocks_time[cacheSymbol][receivedBlockNo - 1]) { const prevBlock = await bb_getBlock(receivedBlockNo - 1, 1) if (prevBlock) { self.blocks_time[cacheSymbol][receivedBlockNo - 1] = prevBlock.time } } if (self.blocks_time[cacheSymbol][receivedBlockNo - 1]) { const prevBlockTime = self.blocks_time[cacheSymbol][receivedBlockNo - 1] block_time = receivedBlockTime - prevBlockTime if (self.blocks_height[cacheSymbol] < receivedBlockNo) { self.blocks_height[cacheSymbol] = receivedBlockNo self.blocks_tps[cacheSymbol].push(block_time > 0 ? txCount / block_time : 0) } } } else { //utilsWallet.warn(`## bb_getBlock - missing txCount || receivedBlockTime - probable 429`, null, { logServerConsole: true }) } // update synonymous symbols const updateSymbols = [symbol] if (symbol === 'BTC') { updateSymbols.push('BTC_SEG') updateSymbols.push('BTC_SEG2') } // update batch - to state if (receivedBlockNo && receivedBlockTime) { updateSymbols.forEach(p => { dispatchActions.push({ type: actionsWallet.SET_ASSET_BLOCK_INFO, payload: { symbol: p, receivedBlockNo, receivedBlockTime } }) }) if (symbol === 'ETH' || symbol === 'ETH_TEST') { // eth[_test] - update erc20s const erc20_symbols = Object.keys(configExternal.erc20Contracts) erc20_symbols.forEach(erc20_symbol => { const meta = configWallet.getMetaBySymbol(erc20_symbol) if ((symbol === 'ETH' && !meta.isErc20_Ropsten) || (symbol === 'ETH_TEST' && meta.isErc20_Ropsten)) { dispatchActions.push({ type: actionsWallet.SET_ASSET_BLOCK_INFO, payload: { symbol: erc20_symbol, receivedBlockNo, receivedBlockTime } }) } }) } } self.postMessage({ msg: 'REQUEST_DISPATCH_BATCH', status: 'DISPATCH', data: { dispatchActions } }) // update lights - block tps if (receivedBlockNo && txCount && block_time > 0) { if (networkStatusChanged) { updateSymbols.forEach(p => { networkStatusChanged(p, { block_no: receivedBlockNo, block_txCount: txCount, block_tps: self.blocks_tps[cacheSymbol].reduce((a,b) => a + b, 0) / self.blocks_tps[cacheSymbol].length, block_count: self.blocks_tps[cacheSymbol].length, block_time, bb_url: configWS.blockbook_ws_config[p].url }) }) } } }) } function isosocket_Disconnect_Blockbook(networkConnected, networkStatusChanged, loaderWorker, walletSymbols) { var disconnectCount = 0 for (var x in configWS.blockbook_ws_config) { if (self.bb_Sockets[x] !== undefined) { if (self.bb_Sockets[x].readyState == 0 || self.bb_Sockets[x].readyState == 1) { // connecting || open self.bb_Sockets[x].close() self.bb_Sockets[x] = undefined disconnectCount++ } if (!loaderWorker) { networkConnected(x, false) networkStatusChanged(x, {}) } } } return disconnectCount } // blockbook isosockets: note this is needed for a different BB API/interface compared to get_BlockbookSocketIo() // considered VOLATILE -- no built-in reconnect function isosocket_Setup_Blockbook(networkConnected, networkStatusChanged, loaderWorker, walletSymbols) { const setupSymbols = [] //utilsWallet.debug(`appWorker >> ${self.workerId} isosocket_Setup_Blockbook...`) for (var assetSymbol in configWS.blockbook_ws_config) { // exclude if not in the loaded wallet if (walletSymbols && walletSymbols.length > 0) { if (!walletSymbols.includes(assetSymbol)) { utilsWallet.warn(`appWorker >> ${self.workerId} isosocket_Setup_Blockbook (skipping ${assetSymbol} - not in wallet)`, null, { logServerConsole: true }) continue } } // static exclusions if (assetSymbol === 'ETH_TEST') { if (!configWallet.WALLET_INCLUDE_ETH_TEST) continue } else if (assetSymbol === 'LTC_TEST') { if (!configWallet.WALLET_INCLUDE_LTC_TEST) continue } else if (assetSymbol === 'ZEC_TEST') { if (!configWallet.WALLET_INCLUDE_ZEC_TEST) continue } else if (assetSymbol === 'BTC_TEST') { if (!configWallet.WALLET_INCLUDE_BTC_TEST) continue } else if (!configWallet.getSupportedMetaKeyBySymbol(assetSymbol)) continue setupSymbols.push( (function (x) { // if we're called more than once, then the socket object already exists if (self.bb_Sockets[x] !== undefined) { // safari refocus handling if (self.bb_Sockets[x].readyState == 2 || self.bb_Sockets[x].readyState == 3) { // if "closing" or "closed" respectively (connecting=0, open=1) utilsWallet.warn(`appWorker >> ${self.workerId} isosocket_Setup_Blockbook ${x} - found disconnected socket for ${x} - nuking it!`) self.bb_Sockets[x].close() self.bb_Sockets[x] = undefined } } // initial / main path if (self.bb_Sockets[x] === undefined) { // connect & init // networkConnected(x, true) // init UI // networkStatusChanged(x, null) const ws_url = new URL(configWS.blockbook_ws_config[x].url) utilsWallet.warn(`appWorker >> ${self.workerId} bb_Sockets CONNECTING!!! ${x}... hostname=${ws_url.hostname} origin=${ws_url.origin} ws_url=`, ws_url, { logServerConsole: true }) self.bb_Sockets[x] = new isoWs(configWS.blockbook_ws_config[x].url + "/websocket", configWallet.WALLET_ENV === "BROWSER" ? undefined : { headers: { "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'), } }) var socket = self.bb_Sockets[x] socket.symbol = x // add a property to the socket object, for logging in case it won't connect self.bb_Sockets_messageID[x] = 0 // init early, testing... self.bb_Sockets_pendingMessages[x] = {} self.bb_Sockets_subscriptions[x] = {} // socket lifecycle socket.onerror = (err) => { utilsWallet.error(`appWorker >> ${self.workerId} bb_Sockets ${x} - ##`, err) } socket.onopen = () => { utilsWallet.log(`appWorker >> ${self.workerId} bb_Sockets ${x} - connected ok...`) try { // setup (exactly once) a keep-alive timer; needed for direct Trezor WS connections to stop server idle drops if (self.bb_Sockets_keepAliveIntervalID[x] === undefined) { self.bb_Sockets_keepAliveIntervalID[x] = setInterval(() => { isosocket_send_Blockbook(x, 'getInfo', {}, (data) => { //utilsWallet.log(`keep-alive isoWS ${x} getInfo`, data) }) // note: rate limiting of WS requests by trezor }, 1000 * 30) } if (!loaderWorker) { networkConnected(x, true) // init UI networkStatusChanged(x, { bb_url: configWS.blockbook_ws_config[x].url }) if (configWS.blockbook_ws_config[x].subBlocks === true) { // // subscribe new block from BB -- note, no new TX subscription in BB // const method = 'subscribeNewBlock' const params = {} if (self.bb_Sockets_subId_NewBlock[x]) { delete self.bb_Sockets_subscriptions[x][self.bb_Sockets_subId_NewBlock[x]] self.bb_Sockets_subId_NewBlock[x] = "" } self.bb_Sockets_subId_NewBlock[x] = isosocket_sub_Blockbook(x, method, params, function (result) { if (!configWallet.WALLET_DISABLE_BLOCK_UPDATES) { if (result) { if (result.subscribed === true) { //utilsWallet.debug(`appWorker >> ${self.workerId} bb_Sockets ${x} - block - subscribed OK`) } else { const receivedBlockNo = result.height const receivedBlockTime = new Date().getTime() // TODO: getBlock & use actual utilsWallet.logMajor('cyan','black', `appWorker >> ${self.workerId} BB BLOCK ${x} - ${receivedBlockNo}`) // save blockheight & time on asset getSyncInfo_Blockbook_v3(x, receivedBlockNo, receivedBlockTime, networkStatusChanged) // requery balance check for asset on new block - updates confirmed counts self.postMessage({ msg: 'REQUEST_STATE', status: 'REQ', data: { stateItem: 'ASSET', stateKey: x, context: 'ASSET_REFRESH_NEW_BLOCK' } }) // // NOTE: this *duplicates* functionality in worker-geth; // we aren't getting up-to-date data from BB getAddressTxids when we trigger from worker-geth // (it's an edge case: seems to only trigger when receiving two same/identical TX's in a short period) // // eth - same for all erc20s if (x === 'ETH' || x === 'ETH_TEST') { const erc20_symbols = Object.keys(configExternal.erc20Contracts) erc20_symbols.forEach(erc20_symbol => { const meta = configWallet.getMetaBySymbol(erc20_symbol) if (meta == undefined) { utilsWallet.error(`undefined wallet meta for ${erc20_symbol}`) } else { //console.log(`BB ${x} -> ${erc20_symbol} -> ${meta.isErc20_Ropsten}`) if ((x === 'ETH' && !meta.isErc20_Ropsten) || (x === 'ETH_TEST' && meta.isErc20_Ropsten)) { // self.postMessage({ msg: 'REQUEST_DISPATCH_BATCH', status: 'DISPATCH', // data: { dispatchActions: [{ // type: actionsWallet.SET_ASSET_BLOCK_INFO, // payload: { symbol: erc20_symbol, receivedBlockNo, receivedBlockTime: new Date().getTime() }} ] } // }) // // todo? (perf - but probably rapidly diminishing returns here) // change REQUEST_STATE to accept [] of asset for update in // a single batch via ASSET_REFRESH_NEW_BLOCK -> refreshAssetFull/refreshAssetBalance // (the latter two fn's would return [] of dispatchActions and caller (worker.js) would // send one batch of actions to update eth+[erc20's] in one hit) // self.postMessage({ msg: 'REQUEST_STATE', status: 'REQ', data: { stateItem: 'ASSET', stateKey: erc20_symbol, context: 'ASSET_REFRESH_NEW_BLOCK' } }) } } }) } } } } }) } } } catch (err) { utilsWallet.error(`### appWorker >> ${self.workerId} bb_Sockets ${x} - connect, err=`, err) } } socket.onclose = () => { utilsWallet.warn(`appWorker >> ${self.workerId} bb_Sockets ${x} - onclose...`) self.bb_Sockets[x] = undefined // nuke this so volatileSockets_ReInit() triggers another setup try { // reconnect - this supplements volatileSockets_ReInit() for faster reconnection // UPDATE: allow explicit disconnects //isosocket_Setup_Blockbook(networkConnected, networkStatusChanged) // ## // very ugly - but worker-geth:socket.onclose isn't triggering reliably - this is, for some reason self.geth_Sockets[x] = undefined // help worker-geth if (!loaderWorker) { networkConnected(x, false) networkStatusChanged(x) } } catch (err) { utilsWallet.error(`### appWorker >> ${self.workerId} bb_Sockets ${x} - onclose callback, err=`, err) } } // // manual message id <-> callback handling // socket.onmessage = (msg) => { if (msg && msg.data) { var resp = JSON.parse(msg.data) var callback = self.bb_Sockets_pendingMessages[x][resp.id] if (callback != undefined) { delete self.bb_Sockets_pendingMessages[x][resp.id] callback(resp.data) } else { callback = self.bb_Sockets_subscriptions[x][resp.id] if (callback != undefined) { callback(resp.data) } else { utilsWallet.error(`### appWorker >> ${self.workerId} bb_Sockets ${x} - UNKNOWN MESSAGE: no callback, msg =`, msg) } } } } return x } })(assetSymbol) ) } return setupSymbols.filter(p => p !== undefined) } function isosocket_send_Blockbook(x, method, params, callback) { if (self.bb_Sockets[x] === undefined) { utilsWallet.warn(`appWorker >> ### ${self.workerId} isosocket_send_Blockbook ${x} - ignoring: NO SOCKET SETUP`) return } if (self.bb_Sockets[x].readyState != 1) { utilsWallet.warn(`appWorker >> ### ${self.workerId} isosocket_send_Blockbook ${x} - ignoring: invalid socket readyState=`, self.bb_Sockets[x].readyState) return } var id = self.bb_Sockets_messageID[x].toString() self.bb_Sockets_messageID[x]++ self.bb_Sockets_pendingMessages[x][id] = callback var req = { id, method, params } self.bb_Sockets[x].send(JSON.stringify(req)) return id } 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.txdb_getItem(cacheKey) .then((cachedTx) => { if (cachedTx && cachedTx.block_no != -1) { // requery unconfirmed tx's cachedTx.fromCache = true //utilsWallet.debug(`** enrichTx - ${asset.symbol} ${tx.txid} RET-CACHE`) resolve(cachedTx) // return from cache } else { isosocket_send_Blockbook(asset.symbol, 'getTransaction', { txid: tx.txid }, (bbTx) => { if (bbTx) { if (bbTx.error) { utilsWallet.error(`### enrichTx - ${asset.symbol} ${tx.txid} - error from BB:`, bbTx.error) resolve(null) } else { // if (tx.txid == '0x58077838e7bf98c88f61a349e64c15816e19ccad8005df9aa33b65fc4c305ae0') { // debugger // } const insightTx = mapTx_BlockbookToInsight(asset, bbTx) // DMS - detect protect_op TX's, and save txhex for these //if (bbTx.version == 2 && bbTx.vout.length == 4 // && bbTx.vout[0].value > 0 && bbTx.vout[0].isAddress == true // protected output (dsigCltv) // && bbTx.vout[1].value == 0 && bbTx.vout[1].isAddress == false // op_return output (versioning) // && bbTx.vout[2].value == 0 && bbTx.vout[2].isAddress == true // beneficiary zero-value output (identification) // && bbTx.vout[3].isAddress == true // benefactor change output (change) -- allow zero change //) { // DMS - actually, we need the hex for all TX's -- see wallet-btc-p2sh::createTxHex_BTC_P2SH() and how it looks up inputTx; // namely, if we send funds to a PROTECT_OP-generated address with a *standard* transaction, then we will the need the hex of this std-tx // for createTxHex_BTC_P2SH() to be able to create dsigCltv() redeem script (i.e. spend it) insightTx.hex = bbTx.hex //} // map tx (prunes vins, drops vouts) const mappedTx = walletUtxo.map_insightTxs([insightTx], ownAddresses, asset)[0] //utilsWallet.log(`** enrichTx - ${asset.symbol} ${tx.txid} - adding to cache, mappedTx=`, mappedTx) // add to cache mappedTx.addedToCacheAt = new Date() 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) }) } } else { utilsWallet.warn(`enrichTx - ${asset.symbol} ${tx.txid} - no data from BB`) resolve(null) } }) } }) .catch((err) => { utilsWallet.reportErr(err) utilsWallet.error('## enrichTx - error=', err) resolve(null) }) }) } function isosocket_sub_Blockbook(x, method, params, callback) { if (self.bb_Sockets[x] === undefined) { utilsWallet.warn(`appWorker >> ${self.workerId} isosocket_sub_Blockbook ${x} - ignoring: NO SOCKET SETUP`) return } if (self.bb_Sockets[x].readyState != 1) { utilsWallet.warn(`appWorker >> ### ${self.workerId} isosocket_sub_Blockbook ${x} - ignoring: invalid socket readyState=`, self.bb_Sockets[x].readyState) return } var id = self.bb_Sockets_messageID[x].toString() self.bb_Sockets_messageID[x]++ self.bb_Sockets_subscriptions[x][id] = callback var req = { id, method,params } self.bb_Sockets[x].send(JSON.stringify(req)) return id } function isosocket_unsub_Blockbook(method, id, params, callback) { if (self.bb_Sockets[x] === undefined) { utilsWallet.warn(`appWorker >> ${self.workerId} isosocket_unsub_Blockbook ${x} - ignoring: NO SOCKET SETUP`) return } if (self.bb_Sockets[x].readyState != 1) { utilsWallet.warn(`appWorker >> ### ${self.workerId} isosocket_unsub_Blockbook ${x} - ignoring: invalid socket readyState=`, self.bb_Sockets[x].readyState) return } delete self.bb_Sockets_subscriptions[x][id] self.bb_Sockets_pendingMessages[x][id] = callback var req = { id, method, params } self.bb_Sockets[x].send(JSON.stringify(req)) return id }