diff --git a/.gitignore b/.gitignore index 538465843ed..765e751187e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ node_modules/ .DS_Store *.swp .zed +coverage +.nyc_output # VIM ignore [._]*.s[a-w][a-z] diff --git a/packages/bitcore-node/Dockerfile b/packages/bitcore-node/Dockerfile index a782d6d82fb..2b0341cbd97 100644 --- a/packages/bitcore-node/Dockerfile +++ b/packages/bitcore-node/Dockerfile @@ -15,4 +15,4 @@ RUN npm run compile # Start the server WORKDIR /bitcore/packages/bitcore-node -CMD ["node", "./build/src/server.js"] \ No newline at end of file +CMD ["node", ".*.tsserver.js"] diff --git a/packages/bitcore-node/scripts/reloadConfig.sh b/packages/bitcore-node/scripts/reloadConfig.sh new file mode 100755 index 00000000000..92dbd4fdcee --- /dev/null +++ b/packages/bitcore-node/scripts/reloadConfig.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +dir=$(pwd) + +if [ $# = 0 ]; then + pid_paths=$dir/pids/* + + for path in $pid_paths; do + pid=$(cat "$path") + printf "$(basename "$path" .pid)::$pid " + pids="$pids $pid" + done + echo '' + + kill -USR1 $pids && + echo "Refreshed all workers" + exit 0 +fi + +if [ $1 = --help ]; then + cat << EOF +Usage: $(basename "$0") [OPTIONS] [WORKER...] + +Reload configuration for bitcore workers + +Options: + --help Show this help message and exit + list List all running workers + +Arguments: + WORKER Name(s) of worker(s) to reload configs (e.g., all api p2p) + If no worker is specified, reload all running workers configs. + +Examples: + $(basename "$0") Reload config for all workers + $(basename "$0") api p2p Reload config for 'api' and 'p2p' workers + $(basename "$0") list List all running workers +EOF + exit 0 +fi + +if [ $1 = list ]; then + pid_paths=$(ls $dir/pids/*.pid 2>/dev/null) + for path in $pid_paths; do + worker=$(basename "$path" .pid) + pid=$(cat "$path") + printf "%-3s %s\n" "$worker" "$pid" + done + exit 0 +fi + +for worker in $@; do + if [ ! -f "$dir/pids/$worker.pid" ]; then + echo "$worker is not running\n$worker.pid not found in $dir/pids" + case $worker in + all|api|p2p) ;; + *) + echo "$worker is not a standard worker\nstandard workers: all, api, p2p" + ;; + esac + exit 1 + fi +done + +pid_paths=$( + for worker in $@; do + printf "$dir/pids/$worker.pid " + done +) + +pids=$( + for path in $pid_paths; do + cat $path + printf ' ' + done +) + +kill -USR1 $pids && + +cat << EOF +Sent reload signal(s) SIGUSR1 to '$@' +pids: $pids +EOF \ No newline at end of file diff --git a/packages/bitcore-node/src/config.ts b/packages/bitcore-node/src/config.ts index ef87042b8d0..53e521306de 100644 --- a/packages/bitcore-node/src/config.ts +++ b/packages/bitcore-node/src/config.ts @@ -14,13 +14,13 @@ function findConfig(): ConfigType | undefined { if (bitcoreConfigPath[0] === '~') { bitcoreConfigPath = bitcoreConfigPath.replace('~', homedir()); } - + if (!fs.existsSync(bitcoreConfigPath)) { throw new Error(`No bitcore config exists at ${bitcoreConfigPath}`); } - + const bitcoreConfigStat = fs.statSync(bitcoreConfigPath); - + if (bitcoreConfigStat.isDirectory()) { if (!fs.existsSync(path.join(bitcoreConfigPath, 'bitcore.config.json'))) { throw new Error(`No bitcore config exists in directory ${bitcoreConfigPath}`); @@ -28,14 +28,14 @@ function findConfig(): ConfigType | undefined { bitcoreConfigPath = path.join(bitcoreConfigPath, 'bitcore.config.json'); } logger.info('Using config at: ' + bitcoreConfigPath); - + let rawBitcoreConfig; try { rawBitcoreConfig = fs.readFileSync(bitcoreConfigPath).toString(); } catch (error) { throw new Error(`Error in loading bitcore config\nFound file at ${bitcoreConfigPath}\n${error}`); } - + let bitcoreConfig; try { bitcoreConfig = JSON.parse(rawBitcoreConfig).bitcoreNode; @@ -63,7 +63,7 @@ function setTrustedPeers(config: ConfigType): ConfigType { } return config; } -const Config = function(): ConfigType { +const loadConfig = function(): ConfigType { let config: ConfigType = { maxPoolSize: 50, port: 3000, @@ -130,4 +130,4 @@ const Config = function(): ConfigType { return config; }; -export default Config(); \ No newline at end of file +export default loadConfig; diff --git a/packages/bitcore-node/src/modules/bitcoin/p2p.ts b/packages/bitcore-node/src/modules/bitcoin/p2p.ts index 30910f32016..4d72e791619 100644 --- a/packages/bitcore-node/src/modules/bitcoin/p2p.ts +++ b/packages/bitcore-node/src/modules/bitcoin/p2p.ts @@ -5,6 +5,7 @@ import { StateStorage } from '../../models/state'; import { TransactionStorage } from '../../models/transaction'; import { ChainStateProvider } from '../../providers/chain-state'; import { Libs } from '../../providers/libs'; +import { Config } from '../../services/config'; import { BaseP2PWorker } from '../../services/p2p'; import { SpentHeightIndicators } from '../../types/Coin'; import { IUtxoNetworkConfig } from '../../types/Config'; @@ -57,6 +58,10 @@ export class BitcoinP2PWorker extends BaseP2PWorker { network: this.network, messages: this.messages }); + + process.on('SIGUSR1', async () => { + await this.reload(); + }); } cacheInv(type: number, hash: string): void { @@ -194,6 +199,36 @@ export class BitcoinP2PWorker extends BaseP2PWorker { } } + async reload() { + this.chainConfig = Config.chainConfig({ chain: this.chain, network: this.network }) as IUtxoNetworkConfig; + const configPeerUris: string[] = []; + + for (const peer of Object.values(this.chainConfig.trustedPeers) as any[]) { + const uri = peer.host + ':' + peer.port; + configPeerUris.push(uri); + const hashes = Object.values(this.pool._addrs).map((a: any) => a.hash); + const addr = this.pool._addAddr({ ip: { v4: peer.host }, port: peer.port }); + if (!hashes.includes(addr.hash)) { + logger.info(`Adding peer ${uri}`); + } + } + + for (const addr of Object.values(this.pool._addrs) as any[]) { + const uri = addr.ip.v4 + ':' + addr.port; + if (!configPeerUris.includes(uri)) { + this.pool._addrs = (this.pool._addrs as any[]).filter(({ hash }) => hash !== addr.hash); + if (this.pool._connectedPeers[addr.hash]) { + logger.info(`Removing peer ${uri}`); + } else { + logger.info(`Removing unconnected peer ${uri}`); + continue; + } + this.pool._connectedPeers[addr.hash].disconnect(); + delete this.pool._connectedPeers[addr.hash]; + } + }; + } + public async getHeaders(candidateHashes: string[]): Promise { let received = false; return new Promise(async resolve => { diff --git a/packages/bitcore-node/src/modules/moralis/api/csp.ts b/packages/bitcore-node/src/modules/moralis/api/csp.ts index 5b8fc4fbcb6..925485fc234 100644 --- a/packages/bitcore-node/src/modules/moralis/api/csp.ts +++ b/packages/bitcore-node/src/modules/moralis/api/csp.ts @@ -2,7 +2,7 @@ import os from 'os'; import { Web3 } from '@bitpay-labs/crypto-wallet-core'; import { LRUCache } from 'lru-cache'; import request from 'request'; -import config from '../../../config'; +import { Config } from '../../../../src/services/config'; import logger from '../../../logger'; import { MongoBound } from '../../../models/base'; import { CacheStorage } from '../../../models/cache'; @@ -21,8 +21,6 @@ import { isDateValid } from '../../../utils'; import { normalizeChainNetwork } from '../../../utils'; import { ReadableWithEventPipe } from '../../../utils/streamWithEventPipe'; - - export interface MoralisAddressSubscription { id?: string; message?: string; @@ -32,8 +30,8 @@ export interface MoralisAddressSubscription { export class MoralisStateProvider extends BaseEVMStateProvider { baseUrl = 'https://deep-index.moralis.io/api/v2.2'; baseStreamUrl = 'https://api.moralis-streams.com/streams/evm'; - apiKey = config.externalProviders?.moralis?.apiKey; - baseWebhookurl = config.externalProviders?.moralis?.webhookBaseUrl; + apiKey = Config.get().externalProviders?.moralis?.apiKey; + baseWebhookurl = Config.get().externalProviders?.moralis?.webhookBaseUrl; headers = { 'Content-Type': 'application/json', 'X-API-Key': this.apiKey, @@ -43,8 +41,20 @@ export class MoralisStateProvider extends BaseEVMStateProvider { constructor(chain: string) { super(chain); + this.loadConfig(); + } + + loadConfig() { + const config = Config.get(); + this.apiKey = config.externalProviders?.moralis?.apiKey; + this.baseWebhookurl = config.externalProviders?.moralis?.webhookBaseUrl; + this.headers = { + 'Content-Type': 'application/json', + 'X-API-Key': this.apiKey, + }; } + // @override async getBlockBeforeTime(params: GetBlockBeforeTimeParams): Promise { const { chain, network, time } = params; @@ -108,7 +118,7 @@ export class MoralisStateProvider extends BaseEVMStateProvider { const blockRange = await this.getBlocksRange({ ...params, chainId }); const tipHeight = Number(await web3.eth.getBlockNumber()); let isReading = false; - + const stream = new ReadableWithEventPipe({ objectMode: true, async read() { @@ -199,7 +209,7 @@ export class MoralisStateProvider extends BaseEVMStateProvider { } }); transactionStream = txStream.eventPipe(transactionStream); - + // Do not await these promises. They are not critical to the stream. WalletAddressStorage.updateLastQueryTime({ chain: this.chain, network, address }) .catch(e => logger.warn(`Failed to update ${this.chain}:${network} address lastQueryTime: %o`, e)), @@ -224,7 +234,7 @@ export class MoralisStateProvider extends BaseEVMStateProvider { convertedBlock.nextBlockHash = nextBlock?.hash!; blocks.push(convertedBlock); } - + const tipHeight = Number(await web3.eth.getBlockNumber()); return { tipHeight, blocks }; } @@ -480,10 +490,10 @@ export class MoralisStateProvider extends BaseEVMStateProvider { /** * Request wrapper for moralis Streams (subscriptions) - * @param method - * @param url - * @param body - * @returns + * @param method + * @param url + * @param body + * @returns */ _subsRequest(method: string, url: string, body?: any) { return new Promise((resolve, reject) => { diff --git a/packages/bitcore-node/src/modules/ripple/api/csp.ts b/packages/bitcore-node/src/modules/ripple/api/csp.ts index 65e25bafc80..493e9c1e727 100644 --- a/packages/bitcore-node/src/modules/ripple/api/csp.ts +++ b/packages/bitcore-node/src/modules/ripple/api/csp.ts @@ -3,12 +3,12 @@ import util from 'util'; import { CryptoRpc } from '@bitpay-labs/crypto-rpc'; import { ObjectId } from 'mongodb'; import request from 'request'; -import Config from '../../../config'; import logger from '../../../logger'; import { CacheStorage } from '../../../models/cache'; import { ICoin } from '../../../models/coin'; import { WalletAddressStorage } from '../../../models/walletAddress'; import { InternalStateProvider } from '../../../providers/chain-state/internal/internal'; +import { Config } from '../../../services/config'; import { Storage } from '../../../services/storage'; import { IBlock } from '../../../types/Block'; import { ChainNetwork } from '../../../types/ChainNetwork'; @@ -39,7 +39,7 @@ export class RippleStateProvider extends InternalStateProvider implements IChain constructor(public chain: string = 'XRP') { super(chain, RippleDbWalletTransactions); - this.config = Config.chains[this.chain]; + this.config = Config.get().chains[this.chain]; } async getClient(network: string) { diff --git a/packages/bitcore-node/src/providers/chain-state/evm/api/csp.ts b/packages/bitcore-node/src/providers/chain-state/evm/api/csp.ts index 8fdbecbd52b..c47d8b754c3 100644 --- a/packages/bitcore-node/src/providers/chain-state/evm/api/csp.ts +++ b/packages/bitcore-node/src/providers/chain-state/evm/api/csp.ts @@ -79,7 +79,7 @@ export class BaseEVMStateProvider extends InternalStateProvider implements IChai return; } BaseEVMStateProvider.rpcInitialized[chain] = true; - + const configs = Config.get().chains[chain] as IChainConfig; for (const [network, config] of Object.entries(configs)) { const chainNetwork = normalizeChainNetwork(chain, network); @@ -207,7 +207,7 @@ export class BaseEVMStateProvider extends InternalStateProvider implements IChai async getAaveUserAccountData(params: { network: string; address: string; version: AaveVersion }): Promise { const { network, address, version } = params; const poolAddress = getAavePoolAddress(this.chain, network, version); - + if (!poolAddress) { throw new Error( `Unsupported Aave pool for chain "${this.chain}", network "${network}", version "${version}".` @@ -482,7 +482,7 @@ export class BaseEVMStateProvider extends InternalStateProvider implements IChai const tx = await this._getTransaction(params); let { found } = tx; const { tipHeight } = tx; - + if (found) { let confirmations = 0; if (found.blockHeight && found.blockHeight >= 0) { @@ -712,7 +712,7 @@ export class BaseEVMStateProvider extends InternalStateProvider implements IChai const result = await ExternalApiStream.onStream(transactionStream, req!, res!, { jsonl: true }); if (!result?.success) { logger.error('Error mid-stream (streamWalletTransactions): %o', result.error?.log || result.error); - } + } return resolve(); } catch (err) { return reject(err); @@ -770,7 +770,7 @@ export class BaseEVMStateProvider extends InternalStateProvider implements IChai let windowSize = 100n; const { web3 } = await this.getWeb3(network); const tip = await web3.eth.getBlockNumber(); - + if (isNaN(args.startBlock!) || isNaN(args.endBlock!)) { throw new Error('startBlock and endBlock must be numbers'); } @@ -788,7 +788,7 @@ export class BaseEVMStateProvider extends InternalStateProvider implements IChai endBlock = Utils.BI.min([endBlock ?? tip, tip]) as bigint; startBlock = Utils.BI.max([startBlock != null ? startBlock : endBlock - 10000n, 0n]) as bigint; - + if (startBlock! > endBlock) { throw new Error('startBlock cannot be greater than endBlock'); } else if (endBlock - startBlock > 10000n) { @@ -1011,7 +1011,7 @@ export class BaseEVMStateProvider extends InternalStateProvider implements IChai } blockId = undefined; } - + if (date) { startDate = new Date(date); endDate = new Date(date); diff --git a/packages/bitcore-node/src/providers/chain-state/evm/api/routes.ts b/packages/bitcore-node/src/providers/chain-state/evm/api/routes.ts index 8c4e3b15459..61c039d48de 100644 --- a/packages/bitcore-node/src/providers/chain-state/evm/api/routes.ts +++ b/packages/bitcore-node/src/providers/chain-state/evm/api/routes.ts @@ -1,7 +1,6 @@ import { Web3 } from '@bitpay-labs/crypto-wallet-core'; import cors from 'cors'; import { Router } from 'express'; -import config from '../../../../config'; import logger from '../../../../logger'; import { WebhookStorage } from '../../../../models/webhook'; import { Config } from '../../../../services/config'; @@ -49,7 +48,7 @@ export class EVMRouter { this.getAaveReserveData(router); this.getAaveReserveTokensAddresses(router); }; - + private setMultiSigRoutes(router: Router) { this.getMultisigEthInfo(router); this.getMultisigContractInstantiationInfo(router); @@ -175,7 +174,7 @@ export class EVMRouter { // Reference: https://docs.optimism.io/builders/app-developers/transactions/estimates const packedRawTx = Web3.utils.encodePacked(rawTx); const rawTxBuf = Buffer.from(packedRawTx!.slice(2), 'hex'); - + const { web3 } = await this.csp.getWeb3(network); const gasPriceOracle = new web3.eth.Contract(OPGasPriceOracleAbi, OPGasPriceOracleAddress); let l1DataFee: bigint; @@ -223,7 +222,7 @@ export class EVMRouter { let { network } = req.params; const { percentile } = req.params; const priorityFeePercentile = Number(percentile) || 15; - + network = network.toLowerCase(); try { const fee: { feerate: number } = await this.csp.getPriorityFee({ network, percentile: priorityFeePercentile }); @@ -235,7 +234,7 @@ export class EVMRouter { }); }; - private streamGnosisWalletTransactions(router: Router) { + private streamGnosisWalletTransactions(router: Router) { router.get(`/api/${this.chain}/:network/ethmultisig/transactions/:multisigContractAddress`, async (req, res) => { const { network, multisigContractAddress } = req.params; try { @@ -296,7 +295,7 @@ export class EVMRouter { private _validateMoralisWebhook(req, res, next) { - const secret = config.externalProviders?.moralis?.streamSecret; + const secret = Config.get().externalProviders?.moralis?.streamSecret; if (!secret) { return res.status(404).send('Moralis not configured'); } @@ -312,7 +311,7 @@ export class EVMRouter { } private postMoralisWebhook(router: Router) { - const webhookCors = config.externalProviders?.moralis?.webhookCors; + const webhookCors = Config.get().externalProviders?.moralis?.webhookCors; router.post(`/webhook/${this.chain}/:network/moralis`, cors(webhookCors), this._validateMoralisWebhook, async (req, res) => { try { const { network } = req.params; diff --git a/packages/bitcore-node/src/providers/chain-state/svm/api/csp.ts b/packages/bitcore-node/src/providers/chain-state/svm/api/csp.ts index 2cff5652f09..c114c7cb43a 100644 --- a/packages/bitcore-node/src/providers/chain-state/svm/api/csp.ts +++ b/packages/bitcore-node/src/providers/chain-state/svm/api/csp.ts @@ -218,7 +218,7 @@ export class BaseSVMStateProvider extends InternalStateProvider implements IChai const result = await ExternalApiStream.onStream(stream, req!, res!); if (!result?.success) { logger.error('Error mid-stream (streamTransactions): %o', result.error?.log || result.error); - } + } return resolve(); } catch (err: any) { logger.error('Error streaming block transactions: %o', err.stack || err.message || err); @@ -235,7 +235,7 @@ export class BaseSVMStateProvider extends InternalStateProvider implements IChai const result = await ExternalApiStream.onStream(addressStream, req!, res!, { jsonl: true }); if (!result?.success) { logger.error('Error mid-stream (streamAddressTransactions): %o', result.error?.log || result.error); - } + } return resolve(); } catch (err) { return reject(err); @@ -651,7 +651,7 @@ export class BaseSVMStateProvider extends InternalStateProvider implements IChai const result = await ExternalApiStream.onStream(stream, req!, res!, { jsonl: true }); if (!result?.success) { logger.error('Error mid-stream (streamBlocks): %o', result.error?.log || result.error); - } + } return resolve(); } catch (err: any) { logger.error('Error streaming blocks: %o', err.stack || err.message || err); @@ -702,11 +702,11 @@ export class BaseSVMStateProvider extends InternalStateProvider implements IChai if (!chain || !network) { throw new Error('Missing required chain and/or network param'); } - + // limit - 1 because startBlock is inclusive; ensure limit is >= 0 limit = Math.max(limit - 1, 0); - let height: number | null = null; + let height: number | null = null; if (date) { startDate = new Date(date); endDate = new Date(date); @@ -770,7 +770,7 @@ export class BaseSVMStateProvider extends InternalStateProvider implements IChai async _findSlotByDate(network: string, targetDate: Date): Promise { const { connection } = await this.getRpc(network); - let lo = await connection.getFirstAvailableBlock().send(); + let lo = await connection.getFirstAvailableBlock().send(); let hi = await connection.getSlot({ commitment: 'finalized' }).send(); let result: bigint | null = null; const targetTime = Math.floor(targetDate.getTime() / 1000); @@ -801,7 +801,7 @@ export class BaseSVMStateProvider extends InternalStateProvider implements IChai await new Promise(resolve => setTimeout(resolve, 500)); continue; } - + if (blockTime === null) { lo = mid + 1n; } else if (blockTime < targetTime) { @@ -812,7 +812,7 @@ export class BaseSVMStateProvider extends InternalStateProvider implements IChai } mid = 0n; // reset mid for next iteration } - + return Number(result) || null; } @@ -884,13 +884,13 @@ export class BaseSVMStateProvider extends InternalStateProvider implements IChai if (addr.state === 'initialized') { const { value } = await connection.getTokenAccountBalance(addr.pubkey).send(); result.push({ mintAddress: addr.mint, ataAddress: addr.pubkey, decimals: value.decimals }); - } + } } return result; } async getSPLTokenInfo( - network: string, + network: string, tokenAddress: string ): Promise<{ name: string; symbol: string; decimals: number; programType: string | undefined; programAddress: string | undefined }> { const TOKEN_PROGRAM_ADDRESS = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'; @@ -901,7 +901,7 @@ export class BaseSVMStateProvider extends InternalStateProvider implements IChai let programAddress; let name = ''; let symbol = ''; - + try { let error; let token; @@ -910,13 +910,13 @@ export class BaseSVMStateProvider extends InternalStateProvider implements IChai } catch (e) { error = e; } - + if (token) { name = token.metadata.name; symbol = token.metadata.symbol; decimals = token.mint.decimals; } else { - // If a token doesn't use the Token Metadata Standard (above), it uses the Solana Labs Token List (below). + // If a token doesn't use the Token Metadata Standard (above), it uses the Solana Labs Token List (below). // This list is obsolete since June 20,2022 const provider = await new TokenListProvider().resolve(); const networkId = { diff --git a/packages/bitcore-node/src/routes/api/fee.ts b/packages/bitcore-node/src/routes/api/fee.ts index 37823bb98e5..4c6e9150f25 100644 --- a/packages/bitcore-node/src/routes/api/fee.ts +++ b/packages/bitcore-node/src/routes/api/fee.ts @@ -1,5 +1,5 @@ import express, { Request, Response } from 'express'; -import config from '../../config'; +import { Config } from '../../../src/services/config'; import logger from '../../logger'; import { ChainStateProvider } from '../../providers/chain-state'; import { QueryType } from '../../types/Api'; @@ -21,7 +21,7 @@ router.get('/:target', CacheMiddleware(CacheTimes.Second), async (req: Request, const { target } = req.params; let { mode } = req.query as { mode?: FeeMode }; const { txType, signatures } = req.query as QueryType; - + if (!chain || !network) { return res.status(400).send('Missing required param'); } @@ -35,7 +35,7 @@ router.get('/:target', CacheMiddleware(CacheTimes.Second), async (req: Request, return res.status(400).send('invalid target specified'); } if (!mode) { - mode = (config.chains[chain]?.[network] as IUtxoNetworkConfig)?.defaultFeeMode; + mode = (Config.get().chains[chain]?.[network] as IUtxoNetworkConfig)?.defaultFeeMode; } else if (!feeModes[chain]) { mode = undefined; } else if (!feeModes[chain]?.includes(mode)) { diff --git a/packages/bitcore-node/src/routes/api/wallet.ts b/packages/bitcore-node/src/routes/api/wallet.ts index 4ce02e77c20..298714b57a7 100644 --- a/packages/bitcore-node/src/routes/api/wallet.ts +++ b/packages/bitcore-node/src/routes/api/wallet.ts @@ -1,6 +1,6 @@ import { Validation } from '@bitpay-labs/crypto-wallet-core'; import { Request, Response, Router } from 'express'; -import config from '../../config'; +import { Config } from '../../../src/services/config'; import logger from '../../logger'; import { ChainStateProvider } from '../../providers/chain-state'; import { StreamWalletAddressesParams } from '../../types/namespaces/ChainStateProvider'; @@ -112,7 +112,7 @@ router.post('/:pubKey', Auth.authenticateMiddleware, async (req: AuthenticatedRe if (req.headers['x-reprocess']) { const reprocessOk = Auth.verifyRequestSignature({ message: ['reprocess', '/addAddresses' + pubKey, JSON.stringify(req.body)].join('|'), - pubKey: config.services.socket.bwsKeys[0], + pubKey: Config.get().services.socket.bwsKeys[0], signature: req.headers['x-reprocess'] }); if (!reprocessOk) { diff --git a/packages/bitcore-node/src/routes/index.ts b/packages/bitcore-node/src/routes/index.ts index 10efc4ebc1c..d7b5e04db9a 100644 --- a/packages/bitcore-node/src/routes/index.ts +++ b/packages/bitcore-node/src/routes/index.ts @@ -1,6 +1,5 @@ import cors from 'cors'; import express from 'express'; -import config from '../config'; import { Config } from '../services/config'; import * as apiRoutes from './api'; import { CacheMiddleware, CacheTimes, LogMiddleware, RateLimiter } from './middleware'; @@ -24,7 +23,7 @@ app.use( const chains = Config.chains(); const networks: any = {}; for (const chain of chains) { - for (const network of Object.keys(config.chains[chain])) { + for (const network of Object.keys(Config.get().chains[chain])) { networks[chain] = networks[chain] || {}; Object.assign(networks[chain], { [network]: true diff --git a/packages/bitcore-node/src/routes/status.ts b/packages/bitcore-node/src/routes/status.ts index b79154f93df..1c9c8734dfb 100644 --- a/packages/bitcore-node/src/routes/status.ts +++ b/packages/bitcore-node/src/routes/status.ts @@ -1,5 +1,5 @@ import express from 'express'; -import config from '../config'; +import { Config } from '../../src/services/config'; import { PerformanceTracker } from '../decorators/Loggify'; import { StateStorage } from '../models/state'; import { ChainNetwork } from '../types/ChainNetwork'; @@ -8,8 +8,8 @@ const router = express.Router({ mergeParams: true }); router.get('/enabled-chains', function(_, res) { const chainNetworks = new Array(); - for (const chain of Object.keys(config.chains)) { - for (const network of Object.keys(config.chains[chain])) { + for (const chain of Object.keys(Config.get().chains)) { + for (const network of Object.keys(Config.get().chains[chain])) { chainNetworks.push({ chain, network }); } } diff --git a/packages/bitcore-node/src/services/api.ts b/packages/bitcore-node/src/services/api.ts index bc6d01a10c9..f07c7affba8 100644 --- a/packages/bitcore-node/src/services/api.ts +++ b/packages/bitcore-node/src/services/api.ts @@ -1,5 +1,4 @@ import * as http from 'http'; -import config from '../config'; import { LoggifyClass } from '../decorators/Loggify'; import logger from '../logger'; import app from '../routes'; @@ -46,6 +45,17 @@ export class ApiService { this.stopped = false; this.httpServer = new http.Server(app); this.httpServer.timeout = this.timeout; + + process.on('SIGUSR1', async () => { + this.reload(); + }); + + process.on('message', async (msg: any) => { + if (msg === 'reloadconfig') { + await this.reload(); + } + }); + this.httpServer.listen(this.port, () => { logger.info(`Starting API Service on port ${this.port}`); this.socketService.start({ server: this.httpServer }); @@ -54,6 +64,16 @@ export class ApiService { return this.httpServer; } + async reload() { + if (this.port !== Config.get().port) { + this.port = Config.get().port; + if (!this.stopped) { + await this.stop(); + await this.start(); + } + } + } + async stop() { this.stopped = true; await this.socketService.stop(); @@ -69,5 +89,5 @@ export class ApiService { // TOOO: choose a place in the config for the API timeout and include it here export const Api = new ApiService({ - port: config.port + port: Config.get().port }); diff --git a/packages/bitcore-node/src/services/config.ts b/packages/bitcore-node/src/services/config.ts index 8360767bb72..03c62b5a00d 100644 --- a/packages/bitcore-node/src/services/config.ts +++ b/packages/bitcore-node/src/services/config.ts @@ -1,4 +1,6 @@ -import config from '../config'; +import cluster from 'cluster'; +import loadConfig from '../config'; +import logger from '../logger'; import { ChainNetwork } from '../types/ChainNetwork'; import { ConfigType } from '../types/Config'; import { valueOrDefault } from '../utils'; @@ -6,19 +8,61 @@ import { valueOrDefault } from '../utils'; type ServiceName = keyof ConfigType['services']; export class ConfigService { - _config: ConfigType; + config: ConfigType; - constructor({ _config = config } = {}) { - this._config = _config; + constructor({ config = loadConfig() } = {}) { + this.config = config; + + // Listen for SIGUSR1 on both main and child processes + process.on('SIGUSR1', () => { + this.reload(); + if (cluster.workers) { + for (const worker of Object.values(cluster.workers)) { + worker?.send('reloadconfig'); + } + } + }); + process.on('message', msg => { + if (msg === 'reloadconfig') { + this.reload(); + } + }); + } + + public reload() { + const oldConfig = this.config; + this.config = loadConfig(); + + // Only show config change for one process + if (!cluster.isPrimary) + return; + const diff = (obj1: any, obj2: any, path: string[] = []) => { + const changes: string[] = []; + const keys = new Set([...Object.keys(obj1 || {}), ...Object.keys(obj2 || {})]); + for (const key of keys) { + const val1 = obj1?.[key]; + const val2 = obj2?.[key]; + const currentPath = [...path, key]; + if (typeof val1 === 'object' && val1 !== null && typeof val2 === 'object' && val2 !== null) { + changes.push(...diff(val1, val2, currentPath)); + } else if (val1 !== val2) { + changes.push(currentPath.join('.')); + logger.info(`${currentPath.join('.')} ${JSON.stringify(val1)} -> ${JSON.stringify(val2)}`); + } + } + return changes; + }; + + diff(oldConfig, this.config); } public get() { - return this._config; + return this.config; } public updateConfig(partialConfig: Partial) { const newConfig = Object.assign({}, this.get(), partialConfig); - this._config = newConfig; + this.config = newConfig; } public chains() { diff --git a/packages/bitcore-node/src/services/p2p.ts b/packages/bitcore-node/src/services/p2p.ts index 3a1ca687b5c..615109d1459 100644 --- a/packages/bitcore-node/src/services/p2p.ts +++ b/packages/bitcore-node/src/services/p2p.ts @@ -61,6 +61,43 @@ export class P2pManager { logger.error('P2P Worker %o:%o died: %o', chain, network, e.stack || e.message || e); } } + + process.on('SIGUSR1', async () => { + const chainConfigs = this.configService.get().chains; + const activeChainNetworks: string[] = []; + for (const worker of this.workers) { + const { chain, network } = worker; + if (chainConfigs[chain][network].disabled) { + logger.info(`Stopping ${chain} ${network}`); + await worker.stop(); + this.workers = this.workers.filter(w => w !== worker); + } else { + activeChainNetworks.push(chain + ':' + network); + } + } + for (const chain in chainConfigs) { + for (const network in chainConfigs[chain]) { + if (!chainConfigs[chain][network].disabled && !activeChainNetworks.includes(chain + ':' + network)) { + if (this.workerClasses[chain][network] === undefined) { + logger.warn(`${chain}:${network} has not been registered`); + continue; + } + const p2pWorker = new this.workerClasses[chain][network]({ + chain, + network, + chainConfig: chainConfigs[chain][network], + }); + this.workers.push(p2pWorker); + try { + logger.info(`Starting ${chain} ${network}`); + p2pWorker.start(); + } catch (e: any) { + logger.error('P2P Worker %o:%o died: %o', chain, network, e.stack || e.message || e); + } + } + } + } + }); } } @@ -68,8 +105,8 @@ export class BaseP2PWorker { protected lastHeartBeat = ''; protected queuedRegistrations = new Array(); protected stopping = false; - protected chain = ''; - protected network = ''; + public chain = ''; + public network = ''; public isSyncingNode = false; constructor(protected params: { chain; network; chainConfig; blockModel?: BaseBlock }) {} diff --git a/packages/bitcore-node/src/services/worker.ts b/packages/bitcore-node/src/services/worker.ts index bd4ca332514..9a5c9692e3a 100644 --- a/packages/bitcore-node/src/services/worker.ts +++ b/packages/bitcore-node/src/services/worker.ts @@ -1,6 +1,6 @@ import cluster, { Worker as ClusterWorker } from 'cluster'; import { EventEmitter } from 'events'; -import config from '../config'; +import { Config } from '../../src/services/config'; import { LoggifyClass } from '../decorators/Loggify'; import logger from '../logger'; import { CallbackType } from '../types/Callback'; @@ -24,7 +24,7 @@ export class WorkerService extends EventEmitter { if (cluster.isPrimary) { logger.verbose(`Master ${process.pid} is running`); if (!args.DEBUG) { - for (let worker = 0; worker < config.numWorkers; worker++) { + for (let worker = 0; worker < Config.get().numWorkers; worker++) { this.startWorker(worker, 0); } } diff --git a/packages/bitcore-node/src/workers/all.ts b/packages/bitcore-node/src/workers/all.ts index c8ef66e7058..203e4ecbb3e 100644 --- a/packages/bitcore-node/src/workers/all.ts +++ b/packages/bitcore-node/src/workers/all.ts @@ -1,5 +1,6 @@ import cluster from 'cluster'; import 'source-map-support/register'; +import fs from 'fs'; import logger from '../logger'; import { Modules } from '../modules'; import { Api } from '../services/api'; @@ -23,6 +24,8 @@ export const FullClusteredWorker = async () => { services.push(Storage, Event); if (cluster.isPrimary) { + fs.mkdirSync('pids', { recursive: true }); + fs.writeFileSync('pids/all.pid', String(process.pid)); services.push(P2P); if (args.DEBUG) { services.push(Api); @@ -48,6 +51,10 @@ const stop = async () => { } stopping = true; + if (cluster.isPrimary) { + fs.unlinkSync('pids/all.pid'); + } + setTimeout(() => { logger.error('All workers did not shut down gracefully after 30 seconds, exiting'); process.exit(1); diff --git a/packages/bitcore-node/src/workers/api.ts b/packages/bitcore-node/src/workers/api.ts index 049e29660b7..ab921053a33 100644 --- a/packages/bitcore-node/src/workers/api.ts +++ b/packages/bitcore-node/src/workers/api.ts @@ -1,5 +1,6 @@ import cluster from 'cluster'; import 'source-map-support/register'; +import fs from 'fs'; import logger from '../logger'; import { Modules } from '../modules'; import { Api } from '../services/api'; @@ -22,6 +23,8 @@ export const ClusteredApiWorker = async () => { services.push(Storage, Event); if (cluster.isPrimary) { + fs.mkdirSync('pids', { recursive: true }); + fs.writeFileSync('pids/api.pid', String(process.pid)); if (args.DEBUG || !args.CLUSTER) { services.push(Api); } else { @@ -46,6 +49,10 @@ const stop = async () => { } stopping = true; + if (cluster.isPrimary) { + fs.unlinkSync('pids/api.pid'); + } + setTimeout(() => { logger.warn('API Worker did not shut down gracefully after 30 seconds, exiting'); process.exit(1); diff --git a/packages/bitcore-node/src/workers/p2p.ts b/packages/bitcore-node/src/workers/p2p.ts index 64354308624..630a374ecab 100644 --- a/packages/bitcore-node/src/workers/p2p.ts +++ b/packages/bitcore-node/src/workers/p2p.ts @@ -1,5 +1,6 @@ import cluster from 'cluster'; import 'source-map-support/register'; +import fs from 'fs'; import logger from '../logger'; import { Modules } from '../modules'; import { Config } from '../services/config'; @@ -18,6 +19,9 @@ export const P2pWorker = async () => { process.on('SIGTERM', stop); process.on('SIGINT', stop); + fs.mkdirSync('pids', { recursive: true }); + fs.writeFileSync('pids/p2p.pid', String(process.pid)); + services.push(Storage, Event); const { CHAIN: chain, NETWORK: network } = process.env; @@ -54,6 +58,8 @@ const stop = async () => { } stopping = true; + fs.unlinkSync('pids/p2p.pid'); + setTimeout(() => { logger.warn('P2P Worker did not shut down gracefully after 30 seconds, exiting'); process.exit(1); diff --git a/packages/bitcore-node/test/benchmark/benchmark.ts b/packages/bitcore-node/test/benchmark/benchmark.ts index c76330afeb1..27208959813 100644 --- a/packages/bitcore-node/test/benchmark/benchmark.ts +++ b/packages/bitcore-node/test/benchmark/benchmark.ts @@ -1,11 +1,11 @@ import { BitcoreLib as bitcoreLib } from '@bitpay-labs/crypto-wallet-core'; -import config from '../../src/config'; import { Storage } from '../../src/services/storage'; import { BitcoinBlockStorage } from '../../src/models/block'; import { BitcoinBlockType } from '../../src/types/namespaces/Bitcoin/Block'; import { resetDatabase } from '../helpers/index.js'; import * as crypto from 'crypto'; import { BitcoinTransactionType } from '../../src/types/namespaces/Bitcoin/Transaction'; +import { Config } from '../../src/services/config'; const { Transaction, PrivateKey } = bitcoreLib; const UnspentOutput = Transaction.UnspentOutput; @@ -109,7 +109,7 @@ function newAddress() { function startBenchmarkDatabase() { const storageArgs = { - dbHost: config.dbHost, + dbHost: Config.get().dbHost, dbName: 'bitcore-benchmark' }; diff --git a/packages/bitcore-node/test/helpers/integration.ts b/packages/bitcore-node/test/helpers/integration.ts index c6b33c05f53..a1a1d81acf0 100644 --- a/packages/bitcore-node/test/helpers/integration.ts +++ b/packages/bitcore-node/test/helpers/integration.ts @@ -1,10 +1,10 @@ -import config from '../../src/config'; +import { Config } from '../../src/services/config'; import { Modules } from '../../src/modules'; import { Storage } from '../../src/services/storage'; import { wait } from '../../src/utils'; const storageArgs = { - dbHost: config.dbHost, + dbHost: Config.get().dbHost, dbName: 'bitcore-integration' }; diff --git a/packages/bitcore-node/test/integration/ethereum/p2p.test.ts b/packages/bitcore-node/test/integration/ethereum/p2p.test.ts index 0ba553ca7a3..f19165f19e1 100644 --- a/packages/bitcore-node/test/integration/ethereum/p2p.test.ts +++ b/packages/bitcore-node/test/integration/ethereum/p2p.test.ts @@ -4,7 +4,6 @@ import { Web3 } from '@bitpay-labs/crypto-wallet-core'; import sinon from 'sinon'; import fs from 'fs'; import { CryptoRpc } from '@bitpay-labs/crypto-rpc'; -import config from '../../../src/config'; import { CacheStorage } from '../../../src/models/cache'; import { EthP2pWorker } from '../../../src/modules/ethereum/p2p/p2p'; import { EVMBlockStorage } from '../../../src/providers/chain-state/evm/models/block'; @@ -14,6 +13,7 @@ import { wait } from '../../../src/utils'; import { resetDatabase } from '../../helpers'; import { intAfterHelper, intBeforeHelper } from '../../helpers/integration'; import { BaseEVMStateProvider } from '../../../src/providers/chain-state/evm/api/csp'; +import { Config } from '../../../src/services/config'; describe('Ethereum', function() { // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -25,7 +25,7 @@ describe('Ethereum', function() { const chain = 'ETH'; const network = 'regtest'; let chainConfig: IEVMNetworkConfig; - + const name = 'EthereumWallet-Ci'; const storageType = 'Level'; const baseUrl = 'http://localhost:3000/api'; @@ -33,7 +33,7 @@ describe('Ethereum', function() { const phrase = 'kiss talent nerve fossil equip fault exile execute train wrist misery diet'; const accounts = { erigon: '0x67b1d87101671b127f5f8714789C7192f7ad340e', geth: '0xeC12CD1Ab86F83C1B26C5caa38126Bc4299b6CBa' }; const privKeys = { erigon: '26e86e45f6fc45ec6e2ecd128cec80fa1d1505e5507dcd2ae58c3130a7a97b48', geth: '0xf9ad2207e910cd649c9a32063dea3656380c32fa07d6bb9be853687ca585a015' }; - + async function getWallet() { let wallet: BitcoreClient.Wallet; try { @@ -58,7 +58,7 @@ describe('Ethereum', function() { return wallet; } } - + async function sendTransaction(from, to, amount, web3, wallet, nonce = 0) { if (!wallet) { wallet = await getWallet(); @@ -82,7 +82,7 @@ describe('Ethereum', function() { } before(async function() { - chainConfig = config.chains[chain][network] as IEVMNetworkConfig; + chainConfig = Config.get().chains[chain][network] as IEVMNetworkConfig; await BitcoreClient.Wallet.deleteWallet({ name, storageType }).catch(() => { /* ignore if it doesn't exist */ }); await intBeforeHelper(); await resetDatabase(); diff --git a/packages/bitcore-node/test/integration/matic/p2p.test.ts b/packages/bitcore-node/test/integration/matic/p2p.test.ts index d6601cd85f8..40f36c522a7 100644 --- a/packages/bitcore-node/test/integration/matic/p2p.test.ts +++ b/packages/bitcore-node/test/integration/matic/p2p.test.ts @@ -2,7 +2,6 @@ import * as BitcoreClient from '@bitpay-labs/bitcore-client'; import { expect } from 'chai'; import { Web3, Transactions } from '@bitpay-labs/crypto-wallet-core'; import sinon from 'sinon'; -import config from '../../../src/config'; import { CacheStorage } from '../../../src/models/cache'; import { EVMBlockStorage } from '../../../src/providers/chain-state/evm/models/block'; import { EVMP2pWorker } from '../../../src/providers/chain-state/evm/p2p/p2p'; @@ -11,6 +10,7 @@ import { IEVMNetworkConfig } from '../../../src/types/Config'; import { wait } from '../../../src/utils'; import { resetDatabase } from '../../helpers'; import { intAfterHelper, intBeforeHelper } from '../../helpers/integration'; +import { Config } from '../../../src/services/config'; describe('Polygon', function() { // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -67,9 +67,9 @@ describe('Polygon', function() { const signedTx = await wallet.signTx({ tx, signingKeys: [{ privKey: privKeys[from] }] }); await web3.eth.sendSignedTransaction(signedTx); } - + before(async function() { - chainConfig = config.chains[chain][network]; + chainConfig = Config.get().chains[chain][network]; await intBeforeHelper(); await resetDatabase(); await Api.start(); diff --git a/packages/bitcore-node/test/integration/models/block.test.ts b/packages/bitcore-node/test/integration/models/block.test.ts index cf3222168d2..7be72d9209b 100644 --- a/packages/bitcore-node/test/integration/models/block.test.ts +++ b/packages/bitcore-node/test/integration/models/block.test.ts @@ -12,7 +12,7 @@ describe('Block Model', function() { // eslint-disable-next-line @typescript-eslint/no-this-alias const suite = this; this.timeout(30000); - + async function insertBlocks() { await BitcoinBlockStorage.collection.insertOne({ chain: 'BTC', diff --git a/packages/bitcore-node/test/integration/models/coin.test.ts b/packages/bitcore-node/test/integration/models/coin.test.ts index 14a9e5b5da6..482f64f69d6 100644 --- a/packages/bitcore-node/test/integration/models/coin.test.ts +++ b/packages/bitcore-node/test/integration/models/coin.test.ts @@ -13,7 +13,7 @@ describe('Coin Model', function() { // eslint-disable-next-line @typescript-eslint/no-this-alias const suite = this; this.timeout(30000); - + function createNewTxid() { const seed = (Math.random() * 10000).toString(); return crypto @@ -22,12 +22,12 @@ describe('Coin Model', function() { .digest() .toString('hex'); } - + async function addTx(tx: IBtcTransaction, outputs: ICoin[]) { await TransactionStorage.collection.insertOne(tx as IBtcTransaction); await CoinStorage.collection.insertMany(outputs as ICoin[]); } - + async function makeMempoolTxChain(chain: string, network: string, startingTxid: string, chainLength = 1) { let txid = startingTxid; let nextTxid = createNewTxid(); @@ -190,7 +190,7 @@ describe('Coin Model', function() { } as ICoin; }); await addTx(mempoolTx, mempoolOutputs); - + // update existing outputs to be spent by mempool tx await CoinStorage.collection.updateMany( { chain, network, mintTxid: tx1.hash }, diff --git a/packages/bitcore-node/test/integration/models/transaction.test.ts b/packages/bitcore-node/test/integration/models/transaction.test.ts index 1800e77bd45..a7efbfd0efe 100644 --- a/packages/bitcore-node/test/integration/models/transaction.test.ts +++ b/packages/bitcore-node/test/integration/models/transaction.test.ts @@ -216,7 +216,7 @@ describe('Transaction Model', function() { const wallet = new ObjectId(); const address = '0x3Ec3dA6E14BE9518A9a6e92DdCC6ACfF2CEFf4ef'; - + beforeEach(async () => { await WalletAddressStorage.collection.insertOne({ chain, diff --git a/packages/bitcore-node/test/integration/models/wallet.test.ts b/packages/bitcore-node/test/integration/models/wallet.test.ts index 0a2c05a5565..71ff44e6bbb 100644 --- a/packages/bitcore-node/test/integration/models/wallet.test.ts +++ b/packages/bitcore-node/test/integration/models/wallet.test.ts @@ -1,6 +1,5 @@ import { Wallet, IWalletExt } from '@bitpay-labs/bitcore-client'; import { expect } from 'chai'; -import config from '../../../src/config'; import { WalletStorage } from '../../../src/models/wallet'; import { WalletAddressStorage } from '../../../src/models/walletAddress'; import { AsyncRPC } from '../../../src/rpc'; @@ -8,6 +7,7 @@ import { Api } from '../../../src/services/api'; import { Event } from '../../../src/services/event'; import { IUtxoNetworkConfig } from '../../../src/types/Config'; import { intAfterHelper, intBeforeHelper } from '../../helpers/integration'; +import { Config } from '../../../src/services/config'; describe('Wallet Model', function() { // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -24,7 +24,7 @@ describe('Wallet Model', function() { let rpc: AsyncRPC; before(async function() { - chainConfig = config.chains[chain][network] as IUtxoNetworkConfig; + chainConfig = Config.get().chains[chain][network] as IUtxoNetworkConfig; creds = chainConfig.rpc; rpc = new AsyncRPC(creds.username, creds.password, creds.host, creds.port); await intBeforeHelper(); diff --git a/packages/bitcore-node/test/integration/routes/block.test.ts b/packages/bitcore-node/test/integration/routes/block.test.ts index 6f8797e7c41..15218ec1db1 100644 --- a/packages/bitcore-node/test/integration/routes/block.test.ts +++ b/packages/bitcore-node/test/integration/routes/block.test.ts @@ -155,7 +155,7 @@ describe('Block Routes', function() { { fee: 11000, size: 1056 }, ] }, - { chain: 'BCH', height: 100, + { chain: 'BCH', height: 100, transactions: [ { fee: 0, size: 133, coinbase: true }, { fee: 2000, size: 1056 }, @@ -163,7 +163,7 @@ describe('Block Routes', function() { { fee: 2500, size: 1056 }, { fee: 3000, size: 1056 }, { fee: 3500, size: 1056 } - ] + ] }, { chain: 'BCH', height: 101 }, { chain: 'BCH', height: 102 } @@ -334,7 +334,7 @@ describe('Block Routes', function() { done(); }); }); - + it('should get coins by block hash and limit coins to 3', done => { request.get(`/api/BTC/regtest/block/${block100Hash}/coins/3/1`).expect(200, (err, res) => { if (err) return done(err); diff --git a/packages/bitcore-node/test/integration/verification.test.ts b/packages/bitcore-node/test/integration/verification.test.ts index 1278a403d2d..7821814d6db 100644 --- a/packages/bitcore-node/test/integration/verification.test.ts +++ b/packages/bitcore-node/test/integration/verification.test.ts @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import config from '../../src/config'; import { BitcoinBlockStorage } from '../../src/models/block'; import { CoinStorage } from '../../src/models/coin'; import { TransactionStorage } from '../../src/models/transaction'; @@ -17,7 +16,7 @@ describe('VerificationPeer', function() { const chain = 'BTC'; const network = 'regtest'; const address = '2MuYKLUaKCenkEpwPkWUwYpBoDBNA2dgY3t'; - + let chainConfig: IUtxoNetworkConfig; let creds: IUtxoNetworkConfig['rpc']; let rpc: AsyncRPC; @@ -36,7 +35,7 @@ describe('VerificationPeer', function() { await rpc.sendtoaddress(address, 0.1); } } - + function addBlock1() { return BitcoinBlockStorage.collection.insertOne({ chain, @@ -57,7 +56,7 @@ describe('VerificationPeer', function() { processed: true }); } - + function addTx() { return TransactionStorage.collection.insertOne({ chain, @@ -93,13 +92,13 @@ describe('VerificationPeer', function() { wallets: [] }); } - + // eslint-disable-next-line @typescript-eslint/no-this-alias const suite = this; this.timeout(500000); before(async function() { - chainConfig = config.chains[chain][network] as IUtxoNetworkConfig; + chainConfig = Config.get().chains[chain][network] as IUtxoNetworkConfig; creds = chainConfig.rpc; rpc = new AsyncRPC(creds.username, creds.password, creds.host, creds.port); await intBeforeHelper(); diff --git a/packages/bitcore-node/test/integration/wallet-benchmark.test.ts b/packages/bitcore-node/test/integration/wallet-benchmark.test.ts index 697e8c0a49a..8c5d0268670 100644 --- a/packages/bitcore-node/test/integration/wallet-benchmark.test.ts +++ b/packages/bitcore-node/test/integration/wallet-benchmark.test.ts @@ -2,7 +2,6 @@ import { Wallet } from '@bitpay-labs/bitcore-client'; import { ParseApiStream } from '@bitpay-labs/bitcore-client'; import { expect } from 'chai'; import * as io from 'socket.io-client'; -import config from '../../src/config'; import { MongoBound } from '../../src/models/base'; import { CoinStorage, ICoin } from '../../src/models/coin'; import { TransactionStorage } from '../../src/models/transaction'; @@ -17,6 +16,7 @@ import { wait } from '../../src/utils'; import { createWallet } from '../benchmark/wallet-benchmark'; import { resetDatabase } from '../helpers'; import { intAfterHelper, intBeforeHelper } from '../helpers/integration'; +import { Config } from '../../src/services/config'; describe('Wallet Benchmark', function() { @@ -25,7 +25,7 @@ describe('Wallet Benchmark', function() { let chainConfig: IUtxoNetworkConfig; let creds: IUtxoNetworkConfig['rpc']; let rpc: AsyncRPC; - + async function checkWalletExists(pubKey, expectedAddress) { // Check the database for the first wallet const dbWallet = await WalletStorage.collection.findOne({ @@ -33,7 +33,7 @@ describe('Wallet Benchmark', function() { network, pubKey }); - + // Verify the addresses match const foundAddresses = await WalletAddressStorage.collection .find({ @@ -46,7 +46,7 @@ describe('Wallet Benchmark', function() { expect(foundAddresses[0].address).to.eq(expectedAddress); return dbWallet; } - + async function getWalletUtxos(wallet: Wallet) { const utxos = new Array>(); return new Promise>>(resolve => @@ -59,16 +59,16 @@ describe('Wallet Benchmark', function() { .on('end', () => resolve(utxos)) ); } - + async function checkWalletUtxos(wallet: Wallet, expectedAddress: string) { const utxos = await getWalletUtxos(wallet); expect(utxos.length).to.eq(1); expect(utxos[0].address).to.eq(expectedAddress); return utxos; } - + async function verifyCoinSpent(coin: MongoBound, spentTxid: string, wallet: IWallet) { - const wallet1Coin = await CoinStorage.collection.findOne({ + const wallet1Coin = await CoinStorage.collection.findOne({ chain: coin.chain, network: coin.network, mintTxid: coin.mintTxid, @@ -84,28 +84,28 @@ describe('Wallet Benchmark', function() { mintTxid: txid, address }); - + expect(broadcastedOutput!.address).to.eq(address); expect(broadcastedOutput!.wallets.length).to.eq(1); expect(broadcastedOutput!.wallets[0].toHexString()).to.eq(receivingWallet!._id!.toHexString()); - + const broadcastedTransaction = await TransactionStorage.collection.findOne({ chain, network, txid }); expect(broadcastedTransaction!.txid).to.eq(txid); expect(broadcastedTransaction!.fee).gt(0); - + const txWallets = broadcastedTransaction!.wallets.map(w => w.toHexString()); expect(txWallets.length).to.eq(2); expect(txWallets).to.include(receivingWallet!._id!.toHexString()); expect(txWallets).to.include(sendingWallet!._id!.toHexString()); } - + // eslint-disable-next-line @typescript-eslint/no-this-alias const suite = this; this.timeout(5000000); let p2pWorker: BitcoinP2PWorker; before(async function() { - chainConfig = config.chains[chain][network] as IUtxoNetworkConfig; + chainConfig = Config.get().chains[chain][network] as IUtxoNetworkConfig; creds = chainConfig.rpc; rpc = new AsyncRPC(creds.username, creds.password, creds.host, creds.port); await intBeforeHelper(); diff --git a/packages/bitcore-node/test/integration/websocket.test.ts b/packages/bitcore-node/test/integration/websocket.test.ts index 043930d4fae..1a14b49bf03 100644 --- a/packages/bitcore-node/test/integration/websocket.test.ts +++ b/packages/bitcore-node/test/integration/websocket.test.ts @@ -1,7 +1,6 @@ import { expect } from 'chai'; import sinon from 'sinon'; import * as io from 'socket.io-client'; -import config from '../../src/config'; import { BitcoinP2PWorker } from '../../src/modules/bitcoin/p2p'; import { AsyncRPC } from '../../src/rpc'; import { Api } from '../../src/services/api'; @@ -15,6 +14,7 @@ import { WalletAddressStorage } from '../../src/models/walletAddress'; import { Socket } from '../../src/services/socket'; import { wait } from '../../src/utils'; import { intAfterHelper, intBeforeHelper } from '../helpers/integration'; +import { Config } from '../../src/services/config'; describe('Websockets', function() { const chain = 'BTC'; @@ -23,12 +23,12 @@ describe('Websockets', function() { let creds: IUtxoNetworkConfig['rpc']; let rpc: AsyncRPC; const { PrivateKey } = BitcoreLib; - + function getSocket() { const socket = io.connect('http://localhost:3000', { transports: ['websocket'] }); return socket; } - + let p2pWorker: BitcoinP2PWorker; let socket = getSocket(); const bwsPrivKey = new PrivateKey(); @@ -43,7 +43,7 @@ describe('Websockets', function() { this.timeout(60000); before(async function() { - chainConfig = config.chains[chain][network] as IUtxoNetworkConfig; + chainConfig = Config.get().chains[chain][network] as IUtxoNetworkConfig; creds = chainConfig.rpc; rpc = new AsyncRPC(creds.username, creds.password, creds.host, creds.port); await intBeforeHelper(); diff --git a/packages/bitcore-node/test/unit/models/transaction.test.ts b/packages/bitcore-node/test/unit/models/transaction.test.ts index c7d60082b03..a432be13388 100644 --- a/packages/bitcore-node/test/unit/models/transaction.test.ts +++ b/packages/bitcore-node/test/unit/models/transaction.test.ts @@ -16,7 +16,7 @@ import { BitcoreLib } from '@bitpay-labs/crypto-wallet-core'; describe('Transaction Model', function() { const { Transaction } = BitcoreLib; - + before(unitBeforeHelper); after(unitAfterHelper); diff --git a/packages/bitcore-node/test/unit/services/p2p.test.ts b/packages/bitcore-node/test/unit/services/p2p.test.ts index 7c48455cedb..6f5f097583d 100644 --- a/packages/bitcore-node/test/unit/services/p2p.test.ts +++ b/packages/bitcore-node/test/unit/services/p2p.test.ts @@ -13,13 +13,13 @@ describe('P2P Service', function() { const sandbox = sinon.createSandbox(); class MockP2PWorker extends BaseP2PWorker { started = false; - + constructor(params) { super(params); this.started = true; } } - + before(unitBeforeHelper); after(unitAfterHelper); @@ -120,7 +120,7 @@ describe('P2P Service', function() { super(params); } } - + const p2p = new MockEVMP2pWorker({ chain: 'ETH', network: 'mainnet', chainConfig: {} }); const converted = await p2p.convertBlock(block as any); expect(converted.convertedTxs.every(tx => { diff --git a/packages/bitcore-node/test/unit/services/worker.test.ts b/packages/bitcore-node/test/unit/services/worker.test.ts index e114a928b1e..3d4a616ae31 100644 --- a/packages/bitcore-node/test/unit/services/worker.test.ts +++ b/packages/bitcore-node/test/unit/services/worker.test.ts @@ -2,10 +2,10 @@ import { expect } from 'chai'; import { EventEmitter } from 'events'; import * as sinon from 'sinon'; import cluster from 'cluster'; -import config from '../../../src/config'; import logger from '../../../src/logger'; import { Worker } from '../../../src/services/worker'; import { unitAfterHelper, unitBeforeHelper } from '../../helpers/unit'; +import { Config } from '../../../src/services/config'; describe('Worker Service', function() { before(unitBeforeHelper); @@ -22,7 +22,7 @@ describe('Worker Service', function() { sandbox = sinon.createSandbox(); clock = sandbox.useFakeTimers({ shouldAdvanceTime: true }); mockWorkers = []; - + // Stub logger methods loggerStubs = { verbose: sandbox.stub(logger, 'verbose'), @@ -30,7 +30,7 @@ describe('Worker Service', function() { error: sandbox.stub(logger, 'error'), warn: sandbox.stub(logger, 'warn') }; - + // Stub cluster module sandbox.stub(cluster, 'isPrimary').value(true); forkStub = sandbox.stub(cluster, 'fork').callsFake(() => { @@ -40,10 +40,9 @@ describe('Worker Service', function() { Promise.resolve().then(() => mockWorker.emit('listening')); return mockWorker; }); - - // Mock config.numWorkers - sandbox.stub(config, 'numWorkers').value(3); - + + Config.updateConfig({ numWorkers: 3 }); + // Clear the Worker service's internal state (Worker as any).workers = []; (Worker as any).shuttingDown = false; @@ -64,30 +63,30 @@ describe('Worker Service', function() { it('should restart worker after 5 seconds on abnormal exit (non-zero code)', async () => { await Worker.start(); - + const initialWorkerCount = forkStub.callCount; expect(initialWorkerCount).to.equal(3); - + const crashedWorker = mockWorkers[0]; const crashedPid = crashedWorker.process.pid; - + // Simulate worker crash crashedWorker.emit('exit', 1, null); - + // Verify immediate state expect(loggerStubs.error.calledWith(sinon.match(`Worker ${crashedPid} crashed`))).to.be.true; expect(loggerStubs.info.calledWith(sinon.match('Scheduling worker 0 restart in 5 seconds'))).to.be.true; expect(forkStub.callCount).to.equal(initialWorkerCount); // Not restarted yet - + // Advance time but not quite 5 seconds clock.tick(4999); expect(forkStub.callCount).to.equal(initialWorkerCount); // Still not restarted - + // Advance to exactly 5 seconds clock.tick(1); expect(forkStub.callCount).to.equal(initialWorkerCount + 1); // Now restarted expect(loggerStubs.warn.calledWith(sinon.match('Restarting worker 0 (restart #1)'))).to.be.true; - + // Verify the new worker has workerId 0 const restartedWorker = mockWorkers[mockWorkers.length - 1]; restartedWorker.emit('listening'); @@ -96,16 +95,16 @@ describe('Worker Service', function() { it('should restart on signal-terminated exit', async () => { await Worker.start(); - + const initialWorkerCount = forkStub.callCount; const crashedWorker = mockWorkers[0]; - + // Simulate worker killed by signal crashedWorker.emit('exit', null, 'SIGKILL'); - + expect(loggerStubs.error.calledWith(sinon.match('crashed'))).to.be.true; expect(loggerStubs.info.calledWith(sinon.match('Scheduling worker 0 restart in 5 seconds'))).to.be.true; - + // Advance 5 seconds clock.tick(5000); expect(forkStub.callCount).to.equal(initialWorkerCount + 1); @@ -113,18 +112,18 @@ describe('Worker Service', function() { it('should NOT restart on graceful exit (code 0)', async () => { await Worker.start(); - + const initialWorkerCount = forkStub.callCount; const exitedWorker = mockWorkers[0]; - + // Simulate graceful exit exitedWorker.emit('exit', 0, null); - + expect(loggerStubs.info.calledWith(sinon.match('stopped gracefully'))).to.be.true; - + // Advance time well past 5 seconds clock.tick(10000); - + // Should NOT have restarted expect(forkStub.callCount).to.equal(initialWorkerCount); expect(loggerStubs.info.calledWith(sinon.match('Scheduling'))).to.be.false; @@ -132,20 +131,20 @@ describe('Worker Service', function() { it('should NOT restart during shutdown', async () => { await Worker.start(); - + const initialWorkerCount = forkStub.callCount; - + // Initiate shutdown await Worker.stop(); expect((Worker as any).shuttingDown).to.be.true; - + // Simulate worker crash after shutdown initiated const crashedWorker = mockWorkers[0]; crashedWorker.emit('exit', 1, null); - + // Advance time clock.tick(5000); - + // Should NOT have restarted expect(forkStub.callCount).to.equal(initialWorkerCount); expect(loggerStubs.info.calledWith(sinon.match('Not restarting worker'))).to.be.false; @@ -153,19 +152,19 @@ describe('Worker Service', function() { it('should increment restart count correctly', async () => { await Worker.start(); - + // First crash mockWorkers[0].emit('exit', 1, null); clock.tick(5000); expect(loggerStubs.warn.calledWith(sinon.match('restart #1'))).to.be.true; - + // Get the restarted worker and crash it again const restartedWorker1 = mockWorkers[mockWorkers.length - 1]; restartedWorker1.emit('listening'); restartedWorker1.emit('exit', 1, null); clock.tick(5000); expect(loggerStubs.warn.calledWith(sinon.match('restart #2'))).to.be.true; - + // Crash again const restartedWorker2 = mockWorkers[mockWorkers.length - 1]; restartedWorker2.emit('listening'); @@ -176,17 +175,17 @@ describe('Worker Service', function() { it('should preserve worker ID across restarts', async () => { await Worker.start(); - + // Crash worker 1 (middle worker) const crashedWorker = mockWorkers[1]; crashedWorker.emit('exit', 1, null); - + // Advance timer clock.tick(5000); - + // Verify restart message mentions worker 1, not worker 3 expect(loggerStubs.warn.calledWith(sinon.match('Restarting worker 1'))).to.be.true; - + // Trigger listening event const restartedWorker = mockWorkers[mockWorkers.length - 1]; restartedWorker.emit('listening'); @@ -195,24 +194,24 @@ describe('Worker Service', function() { it('should restart multiple workers independently', async () => { await Worker.start(); - + const initialWorkerCount = forkStub.callCount; - + // Crash worker 0 at t=0 mockWorkers[0].emit('exit', 1, null); - + // Crash worker 2 at t=2000ms clock.tick(2000); mockWorkers[2].emit('exit', 1, null); - + // Advance to t=5000ms - worker 0 should restart clock.tick(3000); expect(forkStub.callCount).to.equal(initialWorkerCount + 1); expect(loggerStubs.warn.calledWith(sinon.match('Restarting worker 0'))).to.be.true; - + // Worker 2 shouldn't have restarted yet expect(loggerStubs.warn.calledWith(sinon.match('Restarting worker 2'))).to.be.false; - + // Advance to t=7000ms - worker 2 should restart clock.tick(2000); expect(forkStub.callCount).to.equal(initialWorkerCount + 2); @@ -221,76 +220,76 @@ describe('Worker Service', function() { it('should re-attach event handlers on restart', async () => { await Worker.start(); - + // Crash a worker mockWorkers[0].emit('exit', 1, null); clock.tick(5000); - + // Get the restarted worker const restartedWorker = mockWorkers[mockWorkers.length - 1]; - + // Verify it has event listeners expect(restartedWorker.listenerCount('exit')).to.be.greaterThan(0); expect(restartedWorker.listenerCount('message')).to.be.greaterThan(0); expect(restartedWorker.listenerCount('listening')).to.be.greaterThan(0); - + // Test that exit handler still works restartedWorker.emit('listening'); restartedWorker.emit('exit', 1, null); clock.tick(5000); - + // Should have restarted again expect(loggerStubs.warn.calledWith(sinon.match('restart #2'))).to.be.true; }); it('should log restart information correctly', async () => { await Worker.start(); - + const crashedWorker = mockWorkers[0]; const crashedPid = crashedWorker.process.pid; - + // Crash the worker crashedWorker.emit('exit', 1, null); - + // Verify crash log expect(loggerStubs.error.calledWith(sinon.match(`Worker ${crashedPid} crashed (code: 1, signal: null)`))).to.be.true; - + // Verify scheduling log expect(loggerStubs.info.calledWith('Scheduling worker 0 restart in 5 seconds...')).to.be.true; - + // Advance timer clock.tick(5000); - + // Verify restart log expect(loggerStubs.warn.calledWith('Restarting worker 0 (restart #1)')).to.be.true; - + // Emit listening event const restartedWorker = mockWorkers[mockWorkers.length - 1]; const newPid = restartedWorker.process.pid; restartedWorker.emit('listening'); - + // Verify success log expect(loggerStubs.info.calledWith(`Worker 0 successfully restarted (pid: ${newPid})`)).to.be.true; }); it('should disconnect all workers on stop()', async () => { await Worker.start(); - + const worker0 = mockWorkers[0]; const worker1 = mockWorkers[1]; const worker2 = mockWorkers[2]; - + // Call stop await Worker.stop(); - + // Verify shuttingDown flag expect((Worker as any).shuttingDown).to.be.true; - + // Verify disconnect called on all workers expect(worker0.disconnect.called).to.be.true; expect(worker1.disconnect.called).to.be.true; expect(worker2.disconnect.called).to.be.true; - + // Verify no restarts happen after stop worker0.emit('exit', 1, null); clock.tick(5000); @@ -299,56 +298,56 @@ describe('Worker Service', function() { it('should remove worker from array on exit', async () => { await Worker.start(); - + // Initial count expect(Worker.workerCount()).to.equal(3); - + // Crash a worker mockWorkers[1].emit('exit', 1, null); - + // Should immediately be removed expect(Worker.workerCount()).to.equal(2); - + // Advance timer to restart clock.tick(5000); - + // After listening event, count should be back to 3 const restartedWorker = mockWorkers[mockWorkers.length - 1]; restartedWorker.emit('listening'); - + expect(Worker.workerCount()).to.equal(3); }); it('should restart on OOM (exit code 137)', async () => { await Worker.start(); - + const initialWorkerCount = forkStub.callCount; - + // Simulate OOM mockWorkers[0].emit('exit', 137, null); - + expect(loggerStubs.error.calledWith(sinon.match('crashed'))).to.be.true; - + // Advance 5 seconds clock.tick(5000); - + expect(forkStub.callCount).to.equal(initialWorkerCount + 1); expect(loggerStubs.warn.calledWith(sinon.match('Restarting worker 0'))).to.be.true; }); it('should restart on uncaught exception (exit code 1)', async () => { await Worker.start(); - + const initialWorkerCount = forkStub.callCount; - + // Simulate uncaught exception mockWorkers[0].emit('exit', 1, null); - + expect(loggerStubs.error.calledWith(sinon.match('crashed'))).to.be.true; - + // Advance 5 seconds clock.tick(5000); - + expect(forkStub.callCount).to.equal(initialWorkerCount + 1); expect(loggerStubs.warn.calledWith(sinon.match('Restarting worker 0'))).to.be.true; }); diff --git a/packages/bitcore-node/test/verification/rpc-verify.ts b/packages/bitcore-node/test/verification/rpc-verify.ts index b64fa00d030..c19bf5cf763 100755 --- a/packages/bitcore-node/test/verification/rpc-verify.ts +++ b/packages/bitcore-node/test/verification/rpc-verify.ts @@ -1,6 +1,5 @@ #!/usr/bin/env node import { expect } from 'chai'; -import config from '../../src/config'; import logger from '../../src/logger'; import { BitcoinBlockStorage, IBtcBlock } from '../../src/models/block'; import { CoinStorage } from '../../src/models/coin'; @@ -11,6 +10,7 @@ import { AsyncRPC } from '../../src/rpc'; import { Storage } from '../../src/services/storage'; import { ChainNetwork } from '../../src/types/ChainNetwork'; import { IUtxoNetworkConfig } from '../../src/types/Config'; +import { Config } from '../../src/services/config'; const SATOSHI = 100000000.0; @@ -235,7 +235,7 @@ if (require.main === module) chain: process.env.CHAIN || 'BTC', network: process.env.NETWORK || 'testnet' }; - const creds = (config.chains[info.chain][info.network] as IUtxoNetworkConfig).rpc; + const creds = (Config.get().chains[info.chain][info.network] as IUtxoNetworkConfig).rpc; await Storage.start({}); logger.info('verifying blocks');