diff --git a/backend/eslint-local-rules/index.js b/backend/eslint-local-rules/index.js index 52e2d1b79..0a4605dd2 100644 --- a/backend/eslint-local-rules/index.js +++ b/backend/eslint-local-rules/index.js @@ -21,6 +21,8 @@ module.exports = { messages: { unhandled: 'await of non-@asyncSafe callee in @asyncSafe context; use try/catch or annotate callee (@asyncSafe) or context (@asyncUnsafe)', + unhandledVoid: + 'void of non-@asyncSafe callee; annotate callee with @asyncSafe or handle the promise properly', }, }, @@ -308,8 +310,8 @@ module.exports = { return fnHasTag(m, opt.safeTag); } - function calleeIsAnnotatedSafe(awaitNode) { - const arg = unwrapChain(awaitNode.argument); + function calleeIsAnnotatedSafe(callExpr) { + const arg = unwrapChain(callExpr); if (!arg) return false; if (arg.type === 'CallExpression') { @@ -368,7 +370,7 @@ module.exports = { if (inTryBlock(node) || isHandledAwaitArg(node)) return; // callee carries @asyncSafe? → ok anywhere - if (calleeIsAnnotatedSafe(node)) return; + if (calleeIsAnnotatedSafe(node.argument)) return; // context explicitly @asyncUnsafe? → ok to bubble if (contextIsAnnotatedUnsafe(node)) return; @@ -376,6 +378,20 @@ module.exports = { // default: context is safe, callee is unsafe → error context.report({ node, messageId: 'unhandled' }); }, + + // void someAsyncCall() — only allowed if callee is @asyncSafe + UnaryExpression(node) { + if (node.operator !== 'void') return; + + const arg = unwrapChain(node.argument); + if (!arg || arg.type !== 'CallExpression') return; + + // callee carries @asyncSafe? → ok + if (calleeIsAnnotatedSafe(node.argument)) return; + + // void of non-safe callee → error + context.report({ node, messageId: 'unhandledVoid' }); + }, }; }, }, diff --git a/backend/src/api/backend-info.ts b/backend/src/api/backend-info.ts index 7c8fbb47f..af22f45b2 100644 --- a/backend/src/api/backend-info.ts +++ b/backend/src/api/backend-info.ts @@ -43,6 +43,7 @@ class BackendInfo { void this.$updateCoreVersion(); // starting immediately } + /** @asyncSafe */ private async $updateCoreVersion(): Promise { try { const networkInfo = await bitcoinClient.getNetworkInfo(); diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 5bbe10c8b..f4b0b400c 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -106,6 +106,7 @@ class FailoverRouter { } // start polling hosts to measure availability & rtt + /** @asyncSafe */ private async pollHosts(): Promise { if (this.pollTimer) { clearTimeout(this.pollTimer); diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 6e0a6d948..2abb546f9 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -660,7 +660,7 @@ class Blocks { /** * [INDEXING] Index transaction classification flags for Goggles * - * @asyncUnsafe + * @asyncSafe */ public async $classifyBlocks(): Promise { if (this.classifyingBlocks) { @@ -1615,6 +1615,7 @@ class Blocks { } } + /** @asyncSafe */ public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise { try { const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters); diff --git a/backend/src/api/chain-tips.ts b/backend/src/api/chain-tips.ts index 398cefcc7..5062ca1a0 100644 --- a/backend/src/api/chain-tips.ts +++ b/backend/src/api/chain-tips.ts @@ -136,6 +136,7 @@ class ChainTips { } } + /** @asyncSafe */ private async $indexOrphanedBlocks(): Promise { if (this.indexingOrphanedBlocks) { return; diff --git a/backend/src/api/disk-cache.ts b/backend/src/api/disk-cache.ts index c417e16a5..92d52b93f 100644 --- a/backend/src/api/disk-cache.ts +++ b/backend/src/api/disk-cache.ts @@ -38,6 +38,7 @@ class DiskCache { }); } + /** @asyncSafe */ async $saveCacheToDisk(sync: boolean = false): Promise { if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) { return; diff --git a/backend/src/api/redis-cache.ts b/backend/src/api/redis-cache.ts index 50b1ea773..d0b6f18bd 100644 --- a/backend/src/api/redis-cache.ts +++ b/backend/src/api/redis-cache.ts @@ -42,6 +42,7 @@ class RedisCache { } } + /* @asyncSafe */ private async $ensureConnected(): Promise { if (!this.connected && config.REDIS.ENABLED) { try { @@ -95,6 +96,7 @@ class RedisCache { await this.$flushRbfQueues(); } + /** @asyncSafe */ async $updateBlocks(blocks: BlockExtended[]): Promise { if (!config.REDIS.ENABLED) { return; diff --git a/backend/src/api/services/acceleration.ts b/backend/src/api/services/acceleration.ts index f5123bbda..02d3482f0 100644 --- a/backend/src/api/services/acceleration.ts +++ b/backend/src/api/services/acceleration.ts @@ -240,6 +240,7 @@ class AccelerationApi { } } + /** @asyncSafe */ public async connectWebsocket(): Promise { if (this.startedWebsocketLoop) { return; diff --git a/backend/src/api/services/stratum.ts b/backend/src/api/services/stratum.ts index 6bf459909..57c5d3f8d 100644 --- a/backend/src/api/services/stratum.ts +++ b/backend/src/api/services/stratum.ts @@ -59,6 +59,7 @@ class StratumApi { } } + /** @asyncSafe */ public async connectWebsocket(): Promise { if (!config.STRATUM.ENABLED) { return; diff --git a/backend/src/api/services/wallets.ts b/backend/src/api/services/wallets.ts index 6e41b35ba..d1ffa1f27 100644 --- a/backend/src/api/services/wallets.ts +++ b/backend/src/api/services/wallets.ts @@ -60,6 +60,7 @@ class WalletApi { } } + /** @asyncSafe */ private async $loadCache(): Promise { try { const cacheData = await fsPromises.readFile(WalletApi.FILE_NAME, 'utf8'); @@ -148,6 +149,7 @@ class WalletApi { } // resync wallet addresses from the services backend + /** @asyncSafe */ async $syncWallets(): Promise { if (!config.WALLETS.ENABLED || this.syncing) { return; diff --git a/backend/src/database.ts b/backend/src/database.ts index 91d9f4e74..6e853c6e1 100644 --- a/backend/src/database.ts +++ b/backend/src/database.ts @@ -182,7 +182,8 @@ import { execSync } from 'child_process'; if (this.pool === null) { this.pool = createPool(this.poolConfig); this.pool.on('connection', function (newConnection: PoolConnection) { - void newConnection.query(`SET time_zone='+00:00'`); + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- callback API, not a promise despite types + newConnection.query(`SET time_zone='+00:00'`); }); } return this.pool; diff --git a/backend/src/index.ts b/backend/src/index.ts index c41a2a204..ae119bfbc 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -96,6 +96,7 @@ class Server { } } + /** @asyncSafe */ async startServer(worker = false): Promise { logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`); @@ -237,6 +238,7 @@ class Server { void poolsUpdater.$startService(); } + /** @asyncSafe */ async runMainUpdateLoop(): Promise { const start = Date.now(); try { diff --git a/backend/src/indexer.ts b/backend/src/indexer.ts index 66393e0b9..84fe98a52 100644 --- a/backend/src/indexer.ts +++ b/backend/src/indexer.ts @@ -127,6 +127,8 @@ class Indexer { * Runs a single task immediately * * (use `scheduleSingleTask` instead to queue a task to run after some timeout) + * + * @asyncSafe */ public async runSingleTask(task: TaskName): Promise { if (!Common.indexingEnabled() || this.tasksRunning[task]) { @@ -165,7 +167,7 @@ class Indexer { this.tasksRunning[task] = false; } - /** @asyncUnsafe */ + /** @asyncSafe */ public async $run(): Promise { if (!Common.indexingEnabled() || this.runIndexer === false || this.indexerRunning === true || mempool.hasPriority() diff --git a/backend/src/repositories/AccelerationRepository.ts b/backend/src/repositories/AccelerationRepository.ts index 345a3b5f3..39e002df2 100644 --- a/backend/src/repositories/AccelerationRepository.ts +++ b/backend/src/repositories/AccelerationRepository.ts @@ -31,6 +31,7 @@ export interface PublicAcceleration { class AccelerationRepository { private bidBoostV2Activated = 831580; + /** @asyncSafe */ public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number, accelerationData: Acceleration[]): Promise { const accelerationMap: { [txid: string]: Acceleration } = {}; for (const acc of accelerationData) { diff --git a/backend/src/tasks/lightning/network-sync.service.ts b/backend/src/tasks/lightning/network-sync.service.ts index 570aba0cf..649b70a35 100644 --- a/backend/src/tasks/lightning/network-sync.service.ts +++ b/backend/src/tasks/lightning/network-sync.service.ts @@ -112,7 +112,9 @@ class NetworkSyncService { await nodesApi.$setNodesInactive(graphNodesPubkeys); if (config.MAXMIND.ENABLED) { - void $lookupNodeLocation(); + $lookupNodeLocation().catch((e) => { + logger.err(`Error in $lookupNodeLocation: ${e instanceof Error ? e.message : e}`); + }); } } diff --git a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts index 292926760..26652b356 100644 --- a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts +++ b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts @@ -58,7 +58,9 @@ class FundingTxFetcher { elapsedSeconds = Math.round((new Date().getTime() / 1000) - cacheTimer); if (elapsedSeconds > 60) { logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`, logger.tags.ln); - void fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)); + fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)).catch((e) => { + logger.err(`Error saving funding txs cache to disk: ${e instanceof Error ? e.message : e}`, logger.tags.ln); + }); cacheTimer = new Date().getTime() / 1000; } } @@ -66,7 +68,9 @@ class FundingTxFetcher { if (this.channelNewlyProcessed > 0) { logger.info(`Indexed ${this.channelNewlyProcessed} additional channels funding tx`, logger.tags.ln); logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`, logger.tags.ln); - void fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)); + fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)).catch((e) => { + logger.err(`Error saving funding txs cache to disk: ${e instanceof Error ? e.message : e}`, logger.tags.ln); + }); } this.running = false; diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts index 760b0a516..a3933b7db 100644 --- a/backend/src/tasks/pools-updater.ts +++ b/backend/src/tasks/pools-updater.ts @@ -19,6 +19,7 @@ class PoolsUpdater { poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL; treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL; + /** @asyncSafe */ public async $startService(): Promise { while ('Bitcoin is still alive') { try { diff --git a/backend/src/tasks/price-updater.ts b/backend/src/tasks/price-updater.ts index e318452b9..34646e30f 100644 --- a/backend/src/tasks/price-updater.ts +++ b/backend/src/tasks/price-updater.ts @@ -135,6 +135,7 @@ class PriceUpdater { this.latestGoodPrices = JSON.parse(JSON.stringify(this.latestPrices)); } + /** @asyncSafe */ public async $run(): Promise { if (['testnet', 'signet', 'testnet4', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Coins have no value on testnet/signet, so we want to always show 0 diff --git a/backend/src/utils/p-limit.ts b/backend/src/utils/p-limit.ts index dc5448ff2..c62e2831e 100644 --- a/backend/src/utils/p-limit.ts +++ b/backend/src/utils/p-limit.ts @@ -21,6 +21,8 @@ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFT OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import logger from "../logger"; + /* How it works: `this._head` is an instance of `Node` which keeps track of its current value and nests @@ -143,7 +145,7 @@ export default function pLimit(concurrency: number): LimitFunction { const enqueue = (fn, resolve, args) => { queue.enqueue(run.bind(undefined, fn, resolve, args)); - void ( + ( /** @asyncUnsafe */ async () => { // This function needs to wait until the next microtask before comparing @@ -155,7 +157,9 @@ export default function pLimit(concurrency: number): LimitFunction { if (activeCount < concurrency && queue.size > 0) { queue.dequeue()(); } - })(); + })().catch((e) => { + logger.err(`Error in pLimit enqueue: ${e instanceof Error ? e.message : e}`); + }); }; const generator = (fn, ...args) =>