diff --git a/backend/.eslintignore b/backend/.eslintignore index 76add878f..374800d31 100644 --- a/backend/.eslintignore +++ b/backend/.eslintignore @@ -1,2 +1,4 @@ node_modules -dist \ No newline at end of file +dist +eslint-local-rules +.eslintrc.js \ No newline at end of file diff --git a/backend/.eslintrc b/backend/.eslintrc.js similarity index 64% rename from backend/.eslintrc rename to backend/.eslintrc.js index a9b16ef9d..15bc17cd9 100644 --- a/backend/.eslintrc +++ b/backend/.eslintrc.js @@ -1,8 +1,13 @@ -{ +module.exports = { "root": true, "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json", + "tsconfigRootDir": __dirname + }, "plugins": [ - "@typescript-eslint" + "@typescript-eslint", + "local-rules" ], "extends": [ "eslint:recommended", @@ -10,6 +15,16 @@ "plugin:@typescript-eslint/recommended", "prettier" ], + "ignorePatterns": ["dist", "eslint-local-rules", ".eslintrc.js", "testSetup*.ts", "jest.integration.*.ts", "__tests__", "*.config.ts"], + "overrides": [ + { + "files": ["src/__integration_tests__/**/*"], + "rules": { + "@typescript-eslint/no-floating-promises": "off", + "local-rules/no-unhandled-await": "off" + } + } + ], "rules": { "@typescript-eslint/ban-ts-comment": 1, "@typescript-eslint/ban-types": 1, @@ -21,6 +36,8 @@ "@typescript-eslint/no-var-requires": 1, "@typescript-eslint/explicit-function-return-type": 1, "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-floating-promises": "error", + "local-rules/no-unhandled-await": "error", "no-console": 1, "no-constant-condition": 1, "no-dupe-else-if": 1, diff --git a/backend/eslint-local-rules/index.js b/backend/eslint-local-rules/index.js new file mode 100644 index 000000000..0a4605dd2 --- /dev/null +++ b/backend/eslint-local-rules/index.js @@ -0,0 +1,398 @@ +'use strict'; + +module.exports = { + 'no-unhandled-await': { + meta: { + type: 'problem', + docs: { + description: 'forbid unhandled await unless callee is @asyncSafe, context is @asyncUnsafe, or rejection is explicitly handled', + }, + schema: [{ + type: 'object', + properties: { + safeTag: { type: 'string' }, // jsdoc tag that marks a callee safe (default '@asyncSafe') + unsafeTag: { type: 'string' }, // comment/jsdoc that marks a context unsafe (default '@asyncUnsafe') + allowAllSettled: { type: 'boolean' }, + allowCatchMethod: { type: 'boolean' }, + allowThenWithTwoArgs:{ type: 'boolean' }, + }, + additionalProperties: false, + }], + 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', + }, + }, + + create(context) { + const src = context.getSourceCode(); + const opt = Object.assign( + { + safeTag: '@asyncSafe', + unsafeTag: '@asyncUnsafe', + allowAllSettled: true, + allowCatchMethod: true, + allowThenWithTwoArgs: true, + }, + (context.options && context.options[0]) || {} + ); + + // optional typescript API (for cross-file/class resolution) + let ts = null, services = null, checker = null, es2ts = null; + try { + // eslint will only populate parserServices if @typescript-eslint/parser + parserOptions.project are set + services = context.parserServices || null; + // @ts-ignore + if (services && (services.program || services.esTreeNodeToTSNodeMap)) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + ts = require('typescript'); + // @ts-ignore + const program = services.program; + // @ts-ignore + es2ts = services.esTreeNodeToTSNodeMap; + checker = program?.getTypeChecker?.(); + } + } catch { /* noop */ } + + const hasRange = (n) => n && Array.isArray(n.range); + const inside = (n, o) => hasRange(n) && hasRange(o) && n.range[0] >= o.range[0] && n.range[1] <= o.range[1]; + const before = (a, b) => hasRange(a) && hasRange(b) && a.range[0] < b.range[0]; + + const isFnNode = (n) => + n && + (n.type === 'FunctionDeclaration' || + n.type === 'FunctionExpression' || + n.type === 'ArrowFunctionExpression' || + n.type === 'MethodDefinition'); + + const isCommentWith = (c, tag) => typeof c?.value === 'string' && c.value.includes(tag); + + // --- jsdoc tag utils ---------------------------------------------------- + const stripAt = (s) => (s || '').replace(/^@/, ''); + + function tsNodeHasJsDocTag(tsNode, tag) { + if (!ts || !tsNode) return false; + try { + const want = stripAt(tag); + const tags = ts.getJSDocTags(tsNode) || []; + return tags.some((t) => { + const n = t.tagName && (t.tagName.escapedText || t.tagName.getText?.()); + return String(n) === want; + }); + } catch { return false; } + } + + function leadingCommentsHaveTag(node, tag) { + if (!node) return false; + const lead = src.getCommentsBefore(node) || []; + return lead.some((c) => isCommentWith(c, tag)); + } + + function isExportWrapper(node) { + return node?.type === 'ExportNamedDeclaration' || node?.type === 'ExportDefaultDeclaration'; + } + + // does a given function *definition* carry tag in its leading comments? + function fnHasTag(fnNode, tag) { + if (!fnNode) return false; + + // 1) method definitions: jsdoc sits on the MethodDefinition + if (fnNode.type === 'MethodDefinition') { + return leadingCommentsHaveTag(fnNode, tag); + } + + // 2) function declarations (also handle `export` wrappers) + if (fnNode.type === 'FunctionDeclaration') { + if (leadingCommentsHaveTag(fnNode, tag)) return true; + const p = fnNode.parent; + if (isExportWrapper(p) && leadingCommentsHaveTag(p, tag)) return true; + const gp = p && p.parent; + if (isExportWrapper(gp) && leadingCommentsHaveTag(gp, tag)) return true; // belt & suspenders + return false; + } + + // 3) function/arrow expressions + if (fnNode.type === 'FunctionExpression' || fnNode.type === 'ArrowFunctionExpression') { + // tag directly on the expression + if (leadingCommentsHaveTag(fnNode, tag)) return true; + + const p = fnNode.parent; + + // tag on class members that wrap the fn expr (class fields or methods-as-values) + if ( + p?.type === 'MethodDefinition' || + p?.type === 'PropertyDefinition' || // ts/estree: class field + p?.type === 'ClassProperty' // older @typescript-eslint + ) { + if (leadingCommentsHaveTag(p, tag)) return true; + } + + // tag on a variable declarator (const fn = async () => {}) + if (p?.type === 'VariableDeclarator') { + if (leadingCommentsHaveTag(p, tag)) return true; + if (p.parent && leadingCommentsHaveTag(p.parent, tag)) return true; // VariableDeclaration + // handle: export const fn = async () => {} + const exp = p.parent && p.parent.parent; + if (isExportWrapper(exp) && leadingCommentsHaveTag(exp, tag)) return true; + } + + // handle: export default (async () => {...}) or export default (async function(){}) + if (isExportWrapper(p) && leadingCommentsHaveTag(p, tag)) return true; + } + + return false; + } + + + // nearest class body ancestor (if any) + function nearestClassBody() { + const anc = context.getAncestors(); + for (let i = anc.length - 1; i >= 0; i--) { + if (anc[i]?.type === 'ClassBody') return anc[i]; + } + return null; + } + + // within the same class, find a method by name + function findMethodInCurrentClass(propertyName) { + const body = nearestClassBody(); + if (!body) return null; + for (const el of body.body || []) { + if (el?.type === 'MethodDefinition') { + // only handle simple identifiers (not computed) rn + if (el.key?.type === 'Identifier' && el.key.name === propertyName) return el; + } + } + return null; + } + + // nearest function ancestor + function nearestFn() { + const anc = context.getAncestors(); + for (let i = anc.length - 1; i >= 0; i--) if (isFnNode(anc[i])) return anc[i]; + return null; + } + + // nearest block or program ancestor + function nearestBlockOrProgram() { + const anc = context.getAncestors(); + for (let i = anc.length - 1; i >= 0; i--) { + const a = anc[i]; + if (a?.type === 'BlockStatement' || a?.type === 'Program') return a; + } + return null; + } + + // context is @asyncUnsafe if the function has the tag OR there is a tagged comment earlier in the same block/program + function contextIsAnnotatedUnsafe(node) { + const fn = nearestFn(); + if (fnHasTag(fn, opt.unsafeTag)) return true; + + const blk = nearestBlockOrProgram(); + if (!blk) return false; + const lead = src.getCommentsBefore(node) || []; + return lead.some((c) => isCommentWith(c, opt.unsafeTag) && inside(c, blk) && before(c, node)); + } + + // in try { ... } ? + function inTryBlock(node) { + const anc = context.getAncestors(); + for (let i = anc.length - 1; i >= 0; i--) { + const a = anc[i]; + if (a?.type === 'TryStatement' && a.block && inside(node, a.block)) return true; + } + return false; + } + + const unwrapChain = (e) => (e && e.type === 'ChainExpression' ? e.expression : e); + + function isHandledAwaitArg(node) { + const arg = unwrapChain(node.argument); + if (!arg) return false; + + // await Promise.allSettled(...) + if ( + opt.allowAllSettled && + arg.type === 'CallExpression' && + arg.callee?.type === 'MemberExpression' && + arg.callee.object?.type === 'Identifier' && + arg.callee.object.name === 'Promise' && + arg.callee.property?.type === 'Identifier' && + arg.callee.property.name === 'allSettled' + ) return true; + + // await p.catch(...) + if ( + opt.allowCatchMethod && + arg.type === 'CallExpression' && + arg.callee?.type === 'MemberExpression' && + arg.callee.property?.type === 'Identifier' && + arg.callee.property.name === 'catch' + ) return true; + + // await p.then(onFulfilled, onRejected) + if ( + opt.allowThenWithTwoArgs && + arg.type === 'CallExpression' && + arg.callee?.type === 'MemberExpression' && + arg.callee.property?.type === 'Identifier' && + arg.callee.property.name === 'then' && + Array.isArray(arg.arguments) && + arg.arguments.length >= 2 + ) return true; + + return false; + } + + // resolve identifier → local function def in scope (best-effort) + function resolveFnFromIdentifier(id) { + const name = id?.name; + if (!name) return null; + let scope = context.getScope(); + while (scope) { + const v = (scope.set && scope.set.get(name)) || scope.variables?.find((vv) => vv.name === name); + if (v && v.defs && v.defs.length) { + for (const d of v.defs) { + const dn = d.node; + if (!dn) continue; + if (dn.type === 'FunctionDeclaration') return dn; + if (dn.type === 'VariableDeclarator') { + const init = dn.init; + if (init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) { + return init; + } + } + } + } + scope = scope.upper; + } + return null; + } + + // typescript-powered resolution: member call callee → ts declarations → jsdoc tags + function tsMemberIsAnnotatedSafe(memberExpr) { + if (!ts || !checker || !es2ts) return false; + const prop = memberExpr.property; + if (!prop || prop.type !== 'Identifier') return false; // skip computed/strings rn + + try { + const tsObj = es2ts.get(unwrapChain(memberExpr.object)); + if (!tsObj) return false; + let type = checker.getTypeAtLocation(tsObj); + if (!type) return false; + // normalize to apparent type (unions, etc.) + const apparent = checker.getApparentType ? checker.getApparentType(type) : type; + const name = prop.name; + + // ts <5 vs >=5 api differences + const sym = (apparent.getProperty && apparent.getProperty(name)) || + (checker.getPropertyOfType && checker.getPropertyOfType(apparent, name)); + if (!sym || !Array.isArray(sym.declarations)) return false; + + for (const decl of sym.declarations) { + // method, function, property with function type — accept any with @asyncSafe + if (tsNodeHasJsDocTag(decl, opt.safeTag)) return true; + // for class methods, also check the parent (sometimes the tag is on the signature) + if (decl.parent && tsNodeHasJsDocTag(decl.parent, opt.safeTag)) return true; + } + } catch { /* ignore */ } + return false; + } + + // estree-only: this.method() inside same class + function thisMethodIsAnnotatedSafe(memberExpr) { + if (memberExpr.object?.type !== 'ThisExpression') return false; + const prop = memberExpr.property; + if (!prop || prop.type !== 'Identifier') return false; + const m = findMethodInCurrentClass(prop.name); + return fnHasTag(m, opt.safeTag); + } + + function calleeIsAnnotatedSafe(callExpr) { + const arg = unwrapChain(callExpr); + if (!arg) return false; + + if (arg.type === 'CallExpression') { + const c = unwrapChain(arg.callee); + if (!c) return false; + + // direct identifier call + if (c.type === 'Identifier') { + const def = resolveFnFromIdentifier(c); + if (fnHasTag(def, opt.safeTag)) return true; + + // ts fallback for imported funcs + if (ts && checker && es2ts) { + try { + const tsCallee = es2ts.get(c); + const sym = checker.getSymbolAtLocation?.(tsCallee); + const decls = sym?.declarations || []; + for (const d of decls) { + if (tsNodeHasJsDocTag(d, opt.safeTag) || (d.parent && tsNodeHasJsDocTag(d.parent, opt.safeTag))) { + return true; + } + } + } catch { /* noop */ } + } + return false; + } + + // member call: this.m(), obj.m() + if (c.type === 'MemberExpression') { + // 1) easy path: this.method inside same class + if (thisMethodIsAnnotatedSafe(c)) return true; + + // 2) ts-powered cross-file/class/instance resolution + if (tsMemberIsAnnotatedSafe(c)) return true; + + return false; + } + + // function expressions / arrow directly inline + if (c.type === 'FunctionExpression' || c.type === 'ArrowFunctionExpression') { + return fnHasTag(c, opt.safeTag); + } + + // dynamic/new/etc → treat as unsafe + return false; + } + + // awaiting a non-call promise value → treat as unsafe + return false; + } + + // --- main --------------------------------------------------------------- + return { + AwaitExpression(node) { + // handled patterns → ok + if (inTryBlock(node) || isHandledAwaitArg(node)) return; + + // callee carries @asyncSafe? → ok anywhere + if (calleeIsAnnotatedSafe(node.argument)) return; + + // context explicitly @asyncUnsafe? → ok to bubble + if (contextIsAnnotatedUnsafe(node)) return; + + // 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/package-lock.json b/backend/package-lock.json index c3b0a5d9f..b0175ba96 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -34,6 +34,7 @@ "@typescript-eslint/parser": "^5.55.0", "eslint": "^8.36.0", "eslint-config-prettier": "^8.8.0", + "eslint-plugin-local-rules": "^3.0.2", "jest": "^30.0.0", "prettier": "^3.0.0", "ts-jest": "^29.4.5", @@ -87,6 +88,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1449,6 +1451,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -1720,7 +1723,8 @@ "node_modules/@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "peer": true }, "node_modules/@types/qs": { "version": "6.9.7", @@ -1855,6 +1859,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.57.0.tgz", "integrity": "sha512-orrduvpWYkgLCyAdNtR1QIWovcNZlEm6yL8nwH/eTxWLd8gsP+25pdLHYzL2QdkqrieaDwLpytHqycncv0woUQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.57.0", "@typescript-eslint/types": "5.57.0", @@ -2363,6 +2368,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2778,6 +2784,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -3371,6 +3378,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.37.0.tgz", "integrity": "sha512-NU3Ps9nI05GUoVMxcZx1J8CNR6xOvUT4jAUMH5+z8lpp3aEdPVCImKw6PWG4PY+Vfkpr+jvMpxs/qoE7wq0sPw==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", @@ -3435,6 +3443,13 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-local-rules": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-local-rules/-/eslint-plugin-local-rules-3.0.2.tgz", + "integrity": "sha512-IWME7GIYHXogTkFsToLdBCQVJ0U4kbSuVyDT+nKoR4UgtnVrrVeNWuAZkdEu1nxkvi9nsPccGehEEF6dgA28IQ==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -4546,6 +4561,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -6810,6 +6826,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -6925,6 +6942,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7294,6 +7312,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, + "peer": true, "requires": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -8264,6 +8283,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "peer": true, "requires": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -8515,7 +8535,8 @@ "@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "peer": true }, "@types/qs": { "version": "6.9.7", @@ -8624,6 +8645,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.57.0.tgz", "integrity": "sha512-orrduvpWYkgLCyAdNtR1QIWovcNZlEm6yL8nwH/eTxWLd8gsP+25pdLHYzL2QdkqrieaDwLpytHqycncv0woUQ==", "dev": true, + "peer": true, "requires": { "@typescript-eslint/scope-manager": "5.57.0", "@typescript-eslint/types": "5.57.0", @@ -8907,7 +8929,8 @@ "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -9204,6 +9227,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", "dev": true, + "peer": true, "requires": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -9611,6 +9635,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.37.0.tgz", "integrity": "sha512-NU3Ps9nI05GUoVMxcZx1J8CNR6xOvUT4jAUMH5+z8lpp3aEdPVCImKw6PWG4PY+Vfkpr+jvMpxs/qoE7wq0sPw==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", @@ -9700,6 +9725,12 @@ "dev": true, "requires": {} }, + "eslint-plugin-local-rules": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-local-rules/-/eslint-plugin-local-rules-3.0.2.tgz", + "integrity": "sha512-IWME7GIYHXogTkFsToLdBCQVJ0U4kbSuVyDT+nKoR4UgtnVrrVeNWuAZkdEu1nxkvi9nsPccGehEEF6dgA28IQ==", + "dev": true + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -10440,6 +10471,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, + "peer": true, "requires": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -11980,6 +12012,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, + "peer": true, "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12049,7 +12082,8 @@ "typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "peer": true }, "uglify-js": { "version": "3.19.3", diff --git a/backend/package.json b/backend/package.json index c719c7148..b723ff3af 100644 --- a/backend/package.json +++ b/backend/package.json @@ -65,6 +65,7 @@ "@typescript-eslint/parser": "^5.55.0", "eslint": "^8.36.0", "eslint-config-prettier": "^8.8.0", + "eslint-plugin-local-rules": "^3.0.2", "jest": "^30.0.0", "prettier": "^3.0.0", "ts-jest": "^29.4.5", diff --git a/backend/src/api/acceleration/acceleration.routes.ts b/backend/src/api/acceleration/acceleration.routes.ts index dc2ce697b..8be767287 100644 --- a/backend/src/api/acceleration/acceleration.routes.ts +++ b/backend/src/api/acceleration/acceleration.routes.ts @@ -24,6 +24,7 @@ class AccelerationRoutes { res.status(200).send(Object.values(accelerations)); } + /** @asyncUnsafe */ private async $getAcceleratorAcceleration(req: Request, res: Response): Promise { if (req.params.txid) { const acceleration = await AccelerationRepository.$getAccelerationInfoForTxid(req.params.txid); @@ -37,6 +38,7 @@ class AccelerationRoutes { } } + /** @asyncUnsafe */ private async $getAcceleratorAccelerationsHistory(req: Request, res: Response): Promise { const history = await AccelerationRepository.$getAccelerationInfo(null, req.query.blockHeight ? parseInt(req.query.blockHeight as string, 10) : null); res.status(200).send(history.map(accel => ({ diff --git a/backend/src/api/backend-info.ts b/backend/src/api/backend-info.ts index 4a907a067..af22f45b2 100644 --- a/backend/src/api/backend-info.ts +++ b/backend/src/api/backend-info.ts @@ -34,11 +34,16 @@ class BackendInfo { }; this.timer = setInterval(async () => { - await this.$updateCoreVersion(); + try { + await this.$updateCoreVersion(); + } catch (e) { + logger.err(`Exception in $updateCoreVersion. Reason: ${(e instanceof Error ? e.message : e)}`); + } }, 10 * 60 * 1000); // every 10 minutes - this.$updateCoreVersion(); // starting immediately + void this.$updateCoreVersion(); // starting immediately } + /** @asyncSafe */ private async $updateCoreVersion(): Promise { try { const networkInfo = await bitcoinClient.getNetworkInfo(); diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 90543f03f..85d548f8e 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -112,6 +112,7 @@ class BitcoinApi implements AbstractBitcoinApi { .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx); } + /** @asyncUnsafe */ async $getTxsForBlock(hash: string, fallbackToCore = false): Promise { const verboseBlock: IBitcoinApi.VerboseBlock = await this.bitcoindClient.getBlock(hash, 2); const transactions: IEsploraApi.Transaction[] = []; @@ -219,6 +220,7 @@ class BitcoinApi implements AbstractBitcoinApi { return this.bitcoindClient.submitPackage(rawTransactions, maxfeerate ?? undefined, maxburnamount ?? undefined); } + /** @asyncUnsafe */ async $getOutspend(txId: string, vout: number): Promise { const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); return { @@ -229,6 +231,7 @@ class BitcoinApi implements AbstractBitcoinApi { }; } + /** @asyncUnsafe */ async $getOutspends(txId: string): Promise { const outSpends: IEsploraApi.Outspend[] = []; const tx = await this.$getRawTransaction(txId, true, false); @@ -247,6 +250,7 @@ class BitcoinApi implements AbstractBitcoinApi { return outSpends; } + /** @asyncUnsafe */ async $getBatchedOutspends(txId: string[]): Promise { const outspends: IEsploraApi.Outspend[][] = []; for (const tx of txId) { @@ -260,6 +264,7 @@ class BitcoinApi implements AbstractBitcoinApi { return this.$getBatchedOutspends(txId); } + /** @asyncUnsafe */ async $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise { const outspends: IEsploraApi.Outspend[] = []; for (const outpoint of outpoints) { @@ -269,6 +274,7 @@ class BitcoinApi implements AbstractBitcoinApi { return outspends; } + /** @asyncUnsafe */ async $getCoinbaseTx(blockhash: string): Promise { const txids = await this.$getTxIdsForBlock(blockhash); return this.$getRawTransaction(txids[0]); @@ -283,6 +289,7 @@ class BitcoinApi implements AbstractBitcoinApi { return this.bitcoindClient.getNetworkHashPs(120, blockHeight); } + /** @asyncUnsafe */ protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean, lazyPrevouts = false, allowMissingPrevouts = false): Promise { let esploraTransaction: IEsploraApi.Transaction = { txid: transaction.txid, @@ -367,6 +374,7 @@ class BitcoinApi implements AbstractBitcoinApi { } } + /** @asyncUnsafe */ private async $appendMempoolFeeData(transaction: IEsploraApi.Transaction): Promise { if (transaction.fee) { return transaction; @@ -384,6 +392,7 @@ class BitcoinApi implements AbstractBitcoinApi { return transaction; } + /** @asyncUnsafe */ protected async $addPrevouts(transaction: TransactionExtended): Promise { let addedPrevouts = false; for (const vin of transaction.vin) { @@ -423,6 +432,7 @@ class BitcoinApi implements AbstractBitcoinApi { } + /** @asyncUnsafe */ private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean, lazyPrevouts: boolean): Promise { if (transaction.vin[0].is_coinbase) { transaction.fee = 0; diff --git a/backend/src/api/bitcoin/electrum-api.ts b/backend/src/api/bitcoin/electrum-api.ts index 9e8e17705..aa9765420 100644 --- a/backend/src/api/bitcoin/electrum-api.ts +++ b/backend/src/api/bitcoin/electrum-api.ts @@ -40,6 +40,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi { }); } + /** @asyncUnsafe */ async $getAddress(address: string): Promise { const addressInfo = await this.bitcoindClient.validateAddress(address); if (!addressInfo || !addressInfo.isvalid) { @@ -91,6 +92,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi { } } + /** @asyncUnsafe */ async $getAddressTransactions(address: string, lastSeenTxId: string): Promise { const addressInfo = await this.bitcoindClient.validateAddress(address); if (!addressInfo || !addressInfo.isvalid) { @@ -160,6 +162,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi { } } + /** @asyncUnsafe */ async $getAddressUtxos(address: string): Promise { const addressInfo = await this.bitcoindClient.validateAddress(address); if (!addressInfo || !addressInfo.isvalid) { @@ -206,6 +209,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi { } } + /** @asyncUnsafe */ async $getScriptHashUtxos(scripthash: string): Promise { const utxos = await this.$getScriptHashUnspent(scripthash); const result: IEsploraApi.UTXO[] = []; @@ -244,6 +248,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi { return this.electrumClient.blockchainScripthash_listunspent(scriptHash); } + /** @asyncUnsafe */ async $getTransactionMerkleProof(txId: string): Promise { const tx = await this.$getRawTransaction(txId); return this.electrumClient.blockchainTransaction_getMerkle(txId, tx.status.block_height); diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 475aae8b4..f4b0b400c 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -101,11 +101,12 @@ class FailoverRouter { }); if (this.multihost) { - this.pollHosts(); + void this.pollHosts(); } } // start polling hosts to measure availability & rtt + /** @asyncSafe */ private async pollHosts(): Promise { if (this.pollTimer) { clearTimeout(this.pollTimer); @@ -195,7 +196,7 @@ class FailoverRouter { const elapsed = Date.now() - start; - this.pollTimer = setTimeout(() => { this.pollHosts(); }, Math.max(1, this.pollInterval - elapsed)); + this.pollTimer = setTimeout(() => { void this.pollHosts(); }, Math.max(1, this.pollInterval - elapsed)); } private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string { @@ -446,6 +447,7 @@ class ElectrsApi implements AbstractBitcoinApi { return this.failoverRouter.$get('/blocks/tip/hash'); } + /** @asyncUnsafe */ async $getTxIdsForBlock(hash: string, fallbackToCore = false): Promise { try { const txids = await this.failoverRouter.$get('/block/' + hash + '/txids'); @@ -462,6 +464,7 @@ class ElectrsApi implements AbstractBitcoinApi { } } + /** @asyncUnsafe */ async $getTxsForBlock(hash: string, fallbackToCore = false): Promise { try { const txs = await this.failoverRouter.$get('/internal/block/' + hash + '/txs'); @@ -555,6 +558,7 @@ class ElectrsApi implements AbstractBitcoinApi { return this.failoverRouter.$post('/internal/txs/outspends/by-outpoint', outpoints.map(out => `${out.txid}:${out.vout}`), 'json'); } + /** @asyncUnsafe */ async $getCoinbaseTx(blockhash: string): Promise { const txid = await this.failoverRouter.$get(`/block/${blockhash}/txid/0`); return this.failoverRouter.$get('/tx/' + txid); diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index f621868f2..2abb546f9 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -85,6 +85,8 @@ class Blocks { * @param quiet - don't print non-essential logs * @param addMempoolData - calculate sigops etc * @returns Promise + * + * @asyncUnsafe */ private async $getTransactionsExtended( blockHash: string, @@ -245,6 +247,8 @@ class Blocks { * @param block * @param transactions * @returns BlockExtended + * + * @asyncUnsafe */ private async $getBlockExtended(block: IEsploraApi.Block, transactions: TransactionExtended[]): Promise { const coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]); @@ -449,6 +453,8 @@ class Blocks { * Try to find which miner found the block * @param txMinerInfo * @returns + * + * @asyncUnsafe */ private async $findBlockMiner(txMinerInfo: TransactionMinerInfo | undefined): Promise { if (txMinerInfo === undefined || txMinerInfo.vout.length < 1) { @@ -547,6 +553,7 @@ class Blocks { } } + /** @asyncUnsafe */ public async $indexBlockSummary(hash: string, height: number, stale?: boolean): Promise { if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(hash, stale)).map(tx => transactionUtils.extendMempoolTransaction(tx)); @@ -610,6 +617,8 @@ class Blocks { /** * [INDEXING] Index expected fees & weight for all audited blocks + * + * @asyncUnsafe */ public async $generateAuditStats(): Promise { const blockIds = await BlocksAuditsRepository.$getBlocksWithoutSummaries(); @@ -650,6 +659,8 @@ class Blocks { /** * [INDEXING] Index transaction classification flags for Goggles + * + * @asyncSafe */ public async $classifyBlocks(): Promise { if (this.classifyingBlocks) { @@ -830,6 +841,7 @@ class Blocks { /** * [INDEXING] Index all blocks metadata for the mining dashboard + * @asyncSafe */ public async $generateBlockDatabase(): Promise { try { @@ -907,6 +919,7 @@ class Blocks { return await BlocksRepository.$validateChain(); } + /** @asyncUnsafe */ public async $updateBlocks(): Promise { // warn if this run stalls the main loop for more than 2 minutes const timer = this.startTimer(); @@ -1035,7 +1048,7 @@ class Blocks { this.updateTimerProgress(timer, `saved block summary for ${this.currentBlockHeight}`); } if (config.MEMPOOL.CPFP_INDEXING) { - this.$saveCpfp(blockExtended.id, this.currentBlockHeight, cpfpSummary); + void this.$saveCpfp(blockExtended.id, this.currentBlockHeight, cpfpSummary); this.updateTimerProgress(timer, `saved cpfp for ${this.currentBlockHeight}`); } } @@ -1103,7 +1116,7 @@ class Blocks { this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions)); } if (config.MEMPOOL.CACHE_ENABLED && !memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) { - diskCache.$saveCacheToDisk(); + void diskCache.$saveCacheToDisk(); } // Update Redis cache @@ -1146,6 +1159,7 @@ class Blocks { } } + /** @asyncUnsafe */ private async updateQuarterEpochBlockTime(): Promise { if (this.currentBlockHeight >= 503) { try { @@ -1159,6 +1173,10 @@ class Blocks { } } + /** + * Index a block if it's missing from the database. Returns the block after indexing + * @asyncUnsafe + */ public async $indexBlockByHeight(height: number, skipDb = false): Promise { if (Common.indexingEnabled() && !skipDb) { const dbBlock = await blocksRepository.$getBlockByHeight(height); @@ -1171,6 +1189,7 @@ class Blocks { return this.$indexBlock(hash); } + /** @asyncUnsafe */ private async $handleReorgs(blockExtended: BlockExtended, timer: any): Promise { let forkTail = blockExtended; let currentlyIndexed = await blocksRepository.$getBlockByHeight(forkTail.height - 1); @@ -1242,6 +1261,8 @@ class Blocks { /** * Index a block if it's missing from the database. Returns the block after indexing + * + * @asyncUnsafe */ public async $indexBlock(hash: string, block?: IEsploraApi.Block, skipDb = false): Promise { if (Common.indexingEnabled() && !skipDb) { @@ -1271,6 +1292,7 @@ class Blocks { /** * Get one block by its hash + * @asyncUnsafe */ public async $getBlock(hash: string, skipMemoryCache: boolean = false): Promise { // Check the memory cache @@ -1290,6 +1312,7 @@ class Blocks { return await this.$indexBlock(hash); } + /** @asyncUnsafe */ public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false, skipDBLookup = false, cpfpSummary?: CpfpSummary, blockHeight?: number): Promise { @@ -1358,6 +1381,7 @@ class Blocks { return summary.transactions; } + /** @asyncUnsafe */ public async $getSingleTxFromSummary(hash: string, txid: string): Promise { const txs = await this.$getStrippedBlockTransactions(hash); return txs.find(tx => tx.txid === txid) || null; @@ -1374,6 +1398,7 @@ class Blocks { * @param fromHeight * @param limit * @returns + * @asyncUnsafe */ public async $getBlocks(fromHeight?: number, limit: number = 15): Promise { let currentHeight = fromHeight !== undefined ? fromHeight : this.currentBlockHeight; @@ -1408,6 +1433,7 @@ class Blocks { * * @param fromHeight * @param toHeight + * @asyncUnsafe */ public async $getBlocksBetweenHeight(fromHeight: number, toHeight: number): Promise { if (!Common.indexingEnabled()) { @@ -1556,6 +1582,7 @@ class Blocks { return this.currentBlockHeight; } +/** @asyncUnsafe */ public async $indexCPFP(hash: string, height: number, txs?: MempoolTransactionExtended[], stale?: boolean): Promise { let transactions = txs; if (!transactions) { @@ -1588,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 b42a83c7e..5062ca1a0 100644 --- a/backend/src/api/chain-tips.ts +++ b/backend/src/api/chain-tips.ts @@ -39,6 +39,7 @@ class ChainTips { private staleTipsCacheSize = 50; private maxIndexingQueueSize = 100; + /** @asyncSafe */ public async updateOrphanedBlocks(): Promise { try { this.chainTips = await bitcoinClient.getChainTips(); @@ -135,6 +136,7 @@ class ChainTips { } } + /** @asyncSafe */ private async $indexOrphanedBlocks(): Promise { if (this.indexingOrphanedBlocks) { return; @@ -146,13 +148,13 @@ class ChainTips { if (!block && !blockhash) { continue; } - if (blockhash && !block) { - block = await bitcoinCoreApi.$getBlock(blockhash); - } - if (!block) { - continue; - } try { + if (blockhash && !block) { + block = await bitcoinCoreApi.$getBlock(blockhash); + } + if (!block) { + continue; + } let staleBlock: BlockExtended | undefined; const alreadyIndexed = await BlocksSummariesRepository.$isSummaryIndexed(block.id); const needToCache = Object.keys(this.staleTips).length < this.staleTipsCacheSize || block.height > Object.keys(this.staleTips).map(Number).sort((a, b) => b - a)[this.staleTipsCacheSize - 1]; @@ -178,7 +180,7 @@ class ChainTips { this.trimStaleTipsCache(); } } catch (e) { - logger.err(`Failed to index orphaned block ${block.id} at height ${block.height}. Reason: ${e instanceof Error ? e.message : e}`); + logger.err(`Failed to index orphaned block ${block?.id} at height ${block?.height}. Reason: ${e instanceof Error ? e.message : e}`); } } this.indexingOrphanedBlocks = false; diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index e04feb12d..2538d7bd1 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -802,6 +802,7 @@ export class Common { return txs.map(Common.stripTransaction); } + /** @asyncSafe */ static sleep$(ms: number): Promise { return new Promise((resolve) => { setTimeout(() => { diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 07c67d67e..b2613cad8 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -28,6 +28,7 @@ class DatabaseMigration { /** * Entry point + * @asyncUnsafe */ public async $initializeOrMigrateDatabase(): Promise { logger.debug('MIGRATIONS: Running migrations'); @@ -100,6 +101,7 @@ class DatabaseMigration { /** * Create all missing tables + * @asyncUnsafe */ private async $createMissingTablesAndIndexes(databaseSchemaVersion: number) { await this.$setStatisticsAddedIndexedFlag(databaseSchemaVersion); @@ -1245,6 +1247,7 @@ class DatabaseMigration { /** * Check if 'table' exists in the database + * @asyncUnsafe */ private async $checkIfTableExists(table: string): Promise { const query = `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${config.DATABASE.DATABASE}' AND TABLE_NAME = '${table}'`; @@ -1254,6 +1257,7 @@ class DatabaseMigration { /** * Get current database version + * @asyncUnsafe */ private async $getSchemaVersionFromDatabase(): Promise { const query = `SELECT number FROM state WHERE name = 'schema_version';`; @@ -1263,6 +1267,7 @@ class DatabaseMigration { /** * Create the `state` table + * @asyncUnsafe */ private async $createMigrationStateTable(): Promise { const query = `CREATE TABLE IF NOT EXISTS state ( @@ -1280,6 +1285,7 @@ class DatabaseMigration { /** * We actually execute the migrations queries here + * @asyncUnsafe */ private async $migrateTableSchemaFromVersion(version: number): Promise { const transactionQueries: string[] = []; @@ -1346,11 +1352,13 @@ class DatabaseMigration { /** * Save the schema version in the database + * @asyncUnsafe */ private getUpdateToLatestSchemaVersionQuery(): string { return `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version';`; } + /** @asyncUnsafe */ private async updateToSchemaVersion(version): Promise { await this.$executeQuery(`UPDATE state SET number = ${version} WHERE name = 'schema_version';`); } @@ -1769,6 +1777,7 @@ class DatabaseMigration { ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } + /** @asyncUnsafe */ public async $blocksReindexingTruncate(): Promise { logger.warn(`Truncating pools, blocks, hashrates and difficulty_adjustments tables for re-indexing (using '--reindex-blocks'). You can cancel this command within 5 seconds`); await Common.sleep$(5000); diff --git a/backend/src/api/disk-cache.ts b/backend/src/api/disk-cache.ts index c6106429d..92d52b93f 100644 --- a/backend/src/api/disk-cache.ts +++ b/backend/src/api/disk-cache.ts @@ -33,11 +33,12 @@ class DiskCache { return; } process.on('SIGINT', (e) => { - this.$saveCacheToDisk(true); + void this.$saveCacheToDisk(true); process.exit(0); }); } + /** @asyncSafe */ async $saveCacheToDisk(sync: boolean = false): Promise { if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) { return; @@ -174,6 +175,7 @@ class DiskCache { } } + /** @asyncSafe */ async $loadMempoolCache(): Promise { if (!config.MEMPOOL.CACHE_ENABLED || !fs.existsSync(DiskCache.FILE_NAME)) { return; diff --git a/backend/src/api/explorer/channels.api.ts b/backend/src/api/explorer/channels.api.ts index 943ef0a12..7d691e469 100644 --- a/backend/src/api/explorer/channels.api.ts +++ b/backend/src/api/explorer/channels.api.ts @@ -298,6 +298,7 @@ class ChannelsApi { } } + /** @asyncSafe */ public async $getChannelByClosingId(transactionId: string): Promise { try { const query = ` @@ -338,6 +339,7 @@ class ChannelsApi { } } + /** @asyncSafe */ public async $updateClosingInfo(channelInfo: { id: string, node1_closing_balance: number, node2_closing_balance: number, closed_by: string | null, closing_fee: number, outputs: ILightningApi.ForensicOutput[]}): Promise { try { const query = ` @@ -363,6 +365,7 @@ class ChannelsApi { } } + /** @asyncSafe */ public async $updateOpeningInfo(channelInfo: { id: string, node1_funding_balance: number, node2_funding_balance: number, funding_ratio: number, single_funded: boolean | void }): Promise { try { const query = ` @@ -578,6 +581,7 @@ class ChannelsApi { /** * Save or update a channel present in the graph + * @asyncUnsafe */ public async $saveChannel(channel: ILightningApi.Channel, status = 1): Promise { if (!channel.chan_point?.length) { @@ -717,6 +721,7 @@ class ChannelsApi { } } + /** @asyncSafe */ public async $getLatestChannelUpdateForNode(publicKey: string): Promise { try { const query = ` diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index eba6ff516..c7bf86cdf 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -143,6 +143,7 @@ class NodesApi { } } + /** @asyncUnsafe */ public async $getActiveChannelsStats(node_public_key: string): Promise { const query = ` SELECT count(short_id) as active_channel_count, sum(capacity) as capacity @@ -658,6 +659,7 @@ class NodesApi { /** * Save or update a node present in the graph + * @asyncSafe */ public async $saveNode(node: ILightningApi.Node): Promise { try { @@ -727,6 +729,7 @@ class NodesApi { /** * Set all nodes not in `nodesPubkeys` as inactive (status = 0) + * @asyncSafe */ public async $setNodesInactive(graphNodesPubkeys: string[]): Promise { if (graphNodesPubkeys.length === 0) { diff --git a/backend/src/api/lightning/clightning/clightning-client.ts b/backend/src/api/lightning/clightning/clightning-client.ts index 28fc8e9f3..a0da26c0c 100644 --- a/backend/src/api/lightning/clightning/clightning-client.ts +++ b/backend/src/api/lightning/clightning/clightning-client.ts @@ -249,6 +249,7 @@ export default class CLightningClient extends EventEmitter implements AbstractLi })); } + /** @asyncUnsafe */ async $getNetworkGraph(): Promise { const listnodes: any[] = await this.call('listnodes'); const listchannels: any[] = await this.call('listchannels'); diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts index 0db60f649..6fe93df93 100644 --- a/backend/src/api/lightning/clightning/clightning-convert.ts +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -156,6 +156,7 @@ export function convertNode(clNode: any): ILightningApi.Node { /** * Convert clightning "listchannels" response to lnd "describegraph.edges" format + * @asyncUnsafe */ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): Promise { logger.debug(`Converting clightning nodes and channels to lnd graph format`, logger.tags.ln); @@ -212,6 +213,7 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P /** * Convert two clightning "getchannels" entries into a full a lnd "describegraph.edges" format * In this case, clightning knows the channel policy for both nodes + * @asyncUnsafe */ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise { const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0); @@ -238,6 +240,7 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise { const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id); diff --git a/backend/src/api/lightning/lnd/lnd-api.ts b/backend/src/api/lightning/lnd/lnd-api.ts index eb48b5f96..5c3495032 100644 --- a/backend/src/api/lightning/lnd/lnd-api.ts +++ b/backend/src/api/lightning/lnd/lnd-api.ts @@ -40,6 +40,7 @@ class LndApi implements AbstractLightningApi { .then((response) => response.data); } + /** @asyncUnsafe */ async $getNetworkGraph(): Promise { const graph = await axios.get(config.LND.REST_API_URL + '/v1/graph', this.axiosConfig) .then((response) => response.data); diff --git a/backend/src/api/liquid/elements-parser.ts b/backend/src/api/liquid/elements-parser.ts index a07f94245..090df023c 100644 --- a/backend/src/api/liquid/elements-parser.ts +++ b/backend/src/api/liquid/elements-parser.ts @@ -36,6 +36,7 @@ class ElementsParser { } } + /** @asyncUnsafe */ protected async $parseBlock(block: IBitcoinApi.Block) { for (const tx of block.tx) { await this.$parseInputs(tx, block); @@ -43,6 +44,7 @@ class ElementsParser { } } + /** @asyncUnsafe */ protected async $parseInputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) { for (const [index, input] of tx.vin.entries()) { if (input.is_pegin) { @@ -51,6 +53,7 @@ class ElementsParser { } } + /** @asyncUnsafe */ protected async $parsePegIn(input: IBitcoinApi.Vin, vindex: number, txid: string, block: IBitcoinApi.Block) { const bitcoinTx: IBitcoinApi.Transaction = await bitcoinSecondClient.getRawTransaction(input.txid, true); const bitcoinBlock: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(bitcoinTx.blockhash); @@ -60,6 +63,7 @@ class ElementsParser { outputAddress, bitcoinTx.txid, prevout.n, bitcoinBlock.height, bitcoinBlock.time, 1); } + /** @asyncUnsafe */ protected async $parseOutputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) { for (const output of tx.vout) { if (output.scriptPubKey.pegout_chain) { @@ -74,6 +78,7 @@ class ElementsParser { } } + /** @asyncUnsafe */ protected async $savePegToDatabase(height: number, blockTime: number, amount: number, txid: string, txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, bitcoinblock: number, bitcoinBlockTime: number, final_tx: number): Promise { const query = `INSERT IGNORE INTO elements_pegs( @@ -102,12 +107,14 @@ class ElementsParser { } } + /** @asyncUnsafe */ protected async $getLatestBlockHeightFromDatabase(): Promise { const query = `SELECT number FROM state WHERE name = 'last_elements_block'`; const [rows] = await DB.query(query); return rows[0]['number']; } + /** @asyncUnsafe */ protected async $saveLatestBlockToDatabase(blockHeight: number) { const query = `UPDATE state SET number = ? WHERE name = 'last_elements_block'`; await DB.query(query, [blockHeight]); @@ -205,6 +212,7 @@ class ElementsParser { } // Get the UTXOs that need to be scanned in block height (UTXOs that were last updated in the block height - 1) + /** @asyncUnsafe */ protected async $getFederationUtxosToScan(height: number) { const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, timelock, expiredAt FROM federation_txos WHERE lastblockupdate = ? AND unspent = 1`; const [rows] = await DB.query(query, [height - 1]); @@ -212,6 +220,7 @@ class ElementsParser { } // Returns the UTXOs that are spent as of tip and need to be scanned + /** @asyncUnsafe */ protected async $getFederationUtxosToParse(utxos: any[]): Promise { const spentAsTip: any[] = []; const unspentAsTip: any[] = []; @@ -224,6 +233,7 @@ class ElementsParser { return {spentAsTip, unspentAsTip}; } + /** @asyncUnsafe */ protected async $parseBitcoinBlock(block: IBitcoinApi.Block, spentAsTip: any[], unspentAsTip: any[], confirmedTip: number, redeemAddressesData: any[] = []) { const redeemAddresses: string[] = redeemAddressesData.map(redeemAddress => redeemAddress.bitcoinaddress); for (const tx of block.tx) { @@ -315,12 +325,14 @@ class ElementsParser { } } + /** @asyncUnsafe */ protected async $saveLastBlockAuditToDatabase(blockHeight: number) { const query = `UPDATE state SET number = ? WHERE name = 'last_bitcoin_block_audit'`; await DB.query(query, [blockHeight]); } // Get the bitcoin block where the audit process was last updated + /** @asyncUnsafe */ protected async $getAuditProgress(): Promise { const lastblockaudit = await this.$getLastBlockAudit(); const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState(); @@ -331,6 +343,7 @@ class ElementsParser { } // Get the bitcoin blocks remaining to be synced + /** @asyncUnsafe */ protected async $getBitcoinBlockchainState(): Promise { const result = await bitcoinSecondClient.getBlockchainInfo(); return { @@ -339,12 +352,14 @@ class ElementsParser { }; } + /** @asyncUnsafe */ protected async $getLastBlockAudit(): Promise { const query = `SELECT number FROM state WHERE name = 'last_bitcoin_block_audit'`; const [rows] = await DB.query(query); return rows[0]['number']; } + /** @asyncUnsafe */ protected async $getRedeemAddressesToScan(): Promise { const query = `SELECT datetime, amount, bitcoinaddress FROM elements_pegs where amount < 0 AND bitcoinaddress != '' AND bitcointxid = '';`; const [rows]: any[] = await DB.query(query); @@ -357,6 +372,7 @@ class ElementsParser { ///////////// DATA QUERY ////////////// + /** @asyncUnsafe */ public async $getAuditStatus(): Promise { const lastBlockAudit = await this.$getLastBlockAudit(); const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState(); @@ -368,12 +384,14 @@ class ElementsParser { }; } + /** @asyncUnsafe */ public async $getPegDataByMonth(): Promise { const query = `SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y-%m-01') AS date FROM elements_pegs GROUP BY DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y%m')`; const [rows] = await DB.query(query); return rows; } + /** @asyncUnsafe */ public async $getFederationReservesByMonth(): Promise { const query = ` SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(blocktime), '%Y-%m-01') AS date FROM federation_txos @@ -390,6 +408,7 @@ class ElementsParser { } // Get the current L-BTC pegs and the last Liquid block it was updated + /** @asyncUnsafe */ public async $getCurrentLbtcSupply(): Promise { const [rows] = await DB.query(`SELECT SUM(amount) AS LBTC_supply FROM elements_pegs;`); const lastblockupdate = await this.$getLatestBlockHeightFromDatabase(); @@ -402,6 +421,7 @@ class ElementsParser { } // Get the current reserves of the federation and the last Bitcoin block it was updated + /** @asyncUnsafe */ public async $getCurrentFederationReserves(): Promise { const [rows] = await DB.query(`SELECT SUM(amount) AS total_balance FROM federation_txos WHERE unspent = 1 AND expiredAt = 0;`); const lastblockaudit = await this.$getLastBlockAudit(); @@ -414,6 +434,7 @@ class ElementsParser { } // Get all of the federation addresses, most balances first + /** @asyncUnsafe */ public async $getFederationAddresses(): Promise { const query = `SELECT bitcoinaddress, SUM(amount) AS balance FROM federation_txos WHERE unspent = 1 AND expiredAt = 0 GROUP BY bitcoinaddress ORDER BY balance DESC;`; const [rows] = await DB.query(query); @@ -421,6 +442,7 @@ class ElementsParser { } // Get all of the UTXOs held by the federation, most recent first + /** @asyncUnsafe */ public async $getFederationUtxos(): Promise { const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, pegtxid, pegindex, pegblocktime, timelock, expiredAt FROM federation_txos WHERE unspent = 1 AND expiredAt = 0 ORDER BY blocktime DESC;`; const [rows] = await DB.query(query); @@ -428,6 +450,7 @@ class ElementsParser { } // Get expired UTXOs, most recent first + /** @asyncUnsafe */ public async $getExpiredUtxos(): Promise { const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, pegtxid, pegindex, pegblocktime, timelock, expiredAt FROM federation_txos WHERE unspent = 1 AND expiredAt > 0 ORDER BY blocktime DESC;`; const [rows]: any[] = await DB.query(query); @@ -439,6 +462,7 @@ class ElementsParser { } // Get utxos that were spent using emergency keys + /** @asyncUnsafe */ public async $getEmergencySpentUtxos(): Promise { const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, pegtxid, pegindex, pegblocktime, timelock, expiredAt FROM federation_txos WHERE emergencyKey = 1 ORDER BY blocktime DESC;`; const [rows] = await DB.query(query); @@ -446,6 +470,7 @@ class ElementsParser { } // Get the total number of federation addresses + /** @asyncUnsafe */ public async $getFederationAddressesNumber(): Promise { const query = `SELECT COUNT(DISTINCT bitcoinaddress) AS address_count FROM federation_txos WHERE unspent = 1 AND expiredAt = 0;`; const [rows] = await DB.query(query); @@ -453,6 +478,7 @@ class ElementsParser { } // Get the total number of federation utxos + /** @asyncUnsafe */ public async $getFederationUtxosNumber(): Promise { const query = `SELECT COUNT(*) AS utxo_count FROM federation_txos WHERE unspent = 1 AND expiredAt = 0;`; const [rows] = await DB.query(query); @@ -460,6 +486,7 @@ class ElementsParser { } // Get the total number of emergency spent utxos and their total amount + /** @asyncUnsafe */ public async $getEmergencySpentUtxosStats(): Promise { const query = `SELECT COUNT(*) AS utxo_count, SUM(amount) AS total_amount FROM federation_txos WHERE emergencyKey = 1;`; const [rows] = await DB.query(query); @@ -467,6 +494,7 @@ class ElementsParser { } // Get recent pegs in / out + /** @asyncUnsafe */ public async $getPegsList(count: number = 0): Promise { const query = `SELECT txid, txindex, amount, bitcoinaddress, bitcointxid, bitcoinindex, datetime AS blocktime FROM elements_pegs ORDER BY block DESC LIMIT 15 OFFSET ?;`; const [rows] = await DB.query(query, [count]); @@ -474,6 +502,7 @@ class ElementsParser { } // Get all peg in / out from the last month + /** @asyncUnsafe */ public async $getPegsVolumeDaily(): Promise { const pegInQuery = await DB.query(`SELECT SUM(amount) AS volume, COUNT(*) AS number FROM elements_pegs WHERE amount > 0 and datetime > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -1, CURRENT_TIMESTAMP()));`); const pegOutQuery = await DB.query(`SELECT SUM(amount) AS volume, COUNT(*) AS number FROM elements_pegs WHERE amount < 0 and datetime > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -1, CURRENT_TIMESTAMP()));`); @@ -484,6 +513,7 @@ class ElementsParser { } // Get the total pegs number + /** @asyncUnsafe */ public async $getPegsCount(): Promise { const [rows] = await DB.query(`SELECT COUNT(*) AS pegs_count FROM elements_pegs;`); return rows[0]; diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 7a87091aa..43bf05eec 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -45,6 +45,7 @@ class MempoolBlocks { return this.mempoolBlockDeltas; } + /** @asyncUnsafe */ public async updatePools$(): Promise { if (['mainnet', 'testnet', 'signet', 'testnet4', 'regtest'].includes(config.MEMPOOL.NETWORK) === false) { this.pools = {}; @@ -98,6 +99,7 @@ class MempoolBlocks { return mempoolBlockDeltas; } + /** @asyncSafe */ public async $makeBlockTemplates(transactions: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise { const start = Date.now(); @@ -172,6 +174,7 @@ class MempoolBlocks { return this.mempoolBlocks; } + /** @asyncSafe */ public async $updateBlockTemplates(transactions: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], candidates: GbtCandidates | undefined, accelerationDelta: string[] = [], saveResults: boolean = false, useAccelerations: boolean = false): Promise { if (!this.txSelectionWorker) { // need to reset the worker @@ -228,11 +231,13 @@ class MempoolBlocks { } } + /** @asyncSafe */ private resetRustGbt(): void { this.rustInitialized = false; this.rustGbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT); } + /** @asyncSafe */ public async $rustMakeBlockTemplates(txids: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise { const start = Date.now(); @@ -285,10 +290,12 @@ class MempoolBlocks { return this.mempoolBlocks; } + /** @asyncSafe */ public async $oneOffRustBlockTemplates(transactions: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, useAccelerations: boolean, accelerationPool?: number): Promise { return this.$rustMakeBlockTemplates(transactions, newMempool, candidates, false, useAccelerations, accelerationPool); } + /** @asyncSafe */ public async $rustUpdateBlockTemplates(transactions: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], candidates: GbtCandidates | undefined, useAccelerations: boolean, accelerationPool?: number): Promise { // GBT optimization requires that uids never get too sparse // as a sanity check, we should also explicitly prevent uint32 uid overflow diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index ac9b4ba52..d83347fd6 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -122,6 +122,7 @@ class Mempool { return this.spendMap.get(`${txid}:${index}`); } + /** @asyncUnsafe */ public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) { this.mempoolCache = mempoolData; let count = 0; @@ -203,6 +204,7 @@ class Mempool { return this.mempoolCandidates; } + /** @asyncUnsafe */ public async $updateMemPoolInfo() { this.mempoolInfo = await this.$getMempoolInfo(); } @@ -232,6 +234,7 @@ class Mempool { return txTimes; } + /** @asyncUnsafe */ public async $updateMempool(transactions: string[], accelerations: Record | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise { logger.debug(`Updating mempool...`); diff --git a/backend/src/api/mining/mining.ts b/backend/src/api/mining/mining.ts index ef58b6cd9..386f22c17 100644 --- a/backend/src/api/mining/mining.ts +++ b/backend/src/api/mining/mining.ts @@ -326,6 +326,7 @@ class Mining { /** * Generate daily hashrate data + * @asyncUnsafe */ public async $generateNetworkHashrateHistory(): Promise { // If a re-index was requested, truncate first @@ -439,6 +440,7 @@ class Mining { /** * Index difficulty adjustments + * @asyncUnsafe */ public async $indexDifficultyAdjustments(): Promise { // If a re-index was requested, truncate first @@ -528,6 +530,8 @@ class Mining { /** * Create a link between blocks and the latest price at when they were mined + * + * @asyncSafe */ public async $indexBlockPrices(): Promise { if (this.blocksPriceIndexingRunning === true) { @@ -598,6 +602,8 @@ class Mining { /** * Index core coinstatsindex + * + * @asyncUnsafe */ public async $indexCoinStatsIndex(): Promise { let timer = new Date().getTime() / 1000; @@ -635,6 +641,7 @@ class Mining { /** * List existing mining pools + * @asyncUnsafe */ public async $listPools(): Promise<{name: string, slug: string, unique_id: number}[] | null> { const [rows] = await database.query(` @@ -701,6 +708,7 @@ class Mining { return blocks[0]; } + /** @asyncUnsafe */ private async getGenesisData(): Promise<{timestamp: number, bits: number, difficulty: number}> { if (this.genesisData == null) { const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0)); diff --git a/backend/src/api/pools-parser.ts b/backend/src/api/pools-parser.ts index b19eb789e..92c8e919d 100644 --- a/backend/src/api/pools-parser.ts +++ b/backend/src/api/pools-parser.ts @@ -33,6 +33,7 @@ class PoolsParser { /** * Populate our db with updated mining pool definition * @param pools + * @asyncUnsafe */ public async migratePoolsJson(): Promise { // We also need to wipe the backend cache to make sure we don't serve blocks with @@ -126,8 +127,8 @@ class PoolsParser { block.extras.pool = reindexedBlock.extras.pool; } // update persistent cache with the reindexed data - diskCache.$saveCacheToDisk(); - redisCache.$updateBlocks(blocks.getBlocks()); + void diskCache.$saveCacheToDisk(); + void redisCache.$updateBlocks(blocks.getBlocks()); } } @@ -159,6 +160,7 @@ class PoolsParser { /** * Manually add the 'unknown pool' + * @asyncSafe */ public async $insertUnknownPool(): Promise { if (!config.DATABASE.ENABLED) { @@ -190,6 +192,7 @@ class PoolsParser { * re-index pool assignment for blocks previously associated with pool * * @param pool local id of existing pool to reindex + * @asyncUnsafe */ private async $reindexBlocksForPool(poolId: number): Promise { let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52 diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index 31c5c5e9c..10db094ab 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -407,6 +407,7 @@ class RbfCache { }; } + /** @asyncSafe */ public async load({ txs, trees, expiring, mempool, spendMap }): Promise { try { txs.forEach(txEntry => { diff --git a/backend/src/api/redis-cache.ts b/backend/src/api/redis-cache.ts index 1caade15b..80a947c12 100644 --- a/backend/src/api/redis-cache.ts +++ b/backend/src/api/redis-cache.ts @@ -37,11 +37,12 @@ class RedisCache { }, database: NetworkDB[config.MEMPOOL.NETWORK], }; - this.$ensureConnected(); - setInterval(() => { this.$ensureConnected(); }, 10000); + void this.$ensureConnected(); + setInterval(() => { void this.$ensureConnected(); }, 10000); } } + /** @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; @@ -127,6 +129,7 @@ class RedisCache { } } + /** @asyncSafe */ async $addTransaction(tx: MempoolTransactionExtended): Promise { if (!config.REDIS.ENABLED) { return; @@ -139,6 +142,7 @@ class RedisCache { } } + /** @asyncSafe */ async $flushTransactions(): Promise { if (!config.REDIS.ENABLED) { return; @@ -178,6 +182,7 @@ class RedisCache { } } + /** @asyncSafe */ async $removeTransactions(transactions: string[]): Promise { if (!config.REDIS.ENABLED) { return; @@ -206,6 +211,7 @@ class RedisCache { } } + /** @asyncSafe */ async $setRbfEntry(type: string, txid: string, value: any): Promise { if (!config.REDIS.ENABLED) { return; @@ -222,6 +228,7 @@ class RedisCache { } } + /** @asyncSafe */ async $removeRbfEntry(type: string, txid: string): Promise { if (!config.REDIS.ENABLED) { return; @@ -238,6 +245,7 @@ class RedisCache { } } + /** @asyncSafe */ private async $flushRbfQueues(): Promise { if (!config.REDIS.ENABLED) { return; @@ -263,6 +271,7 @@ class RedisCache { } } + /** @asyncSafe */ async $getBlocks(): Promise { if (!config.REDIS.ENABLED) { return []; @@ -280,6 +289,7 @@ class RedisCache { } } + /** @asyncSafe */ async $getBlockSummaries(): Promise { if (!config.REDIS.ENABLED) { return []; @@ -297,6 +307,7 @@ class RedisCache { } } + /** @asyncSafe */ async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> { if (!config.REDIS.ENABLED) { return {}; @@ -320,6 +331,7 @@ class RedisCache { return {}; } + /** @asyncSafe */ async $getRbfEntries(type: string): Promise { if (!config.REDIS.ENABLED) { return []; @@ -337,6 +349,7 @@ class RedisCache { } } + /** @asyncUnsafe */ async $loadCache(): Promise { if (!config.REDIS.ENABLED) { return; @@ -385,12 +398,14 @@ class RedisCache { } } + /** @asyncUnsafe */ private async scanKeys(pattern): Promise<{ key: string, value: T }[]> { logger.info(`loading Redis entries for ${pattern}`); let keys: string[] = []; const result: { key: string, value: T }[] = []; const patternLength = pattern.length - 1; let count = 0; + /** @asyncUnsafe */ const processValues = async (keys): Promise => { const values = await this.client.MGET(keys); for (let i = 0; i < values.length; i++) { diff --git a/backend/src/api/services/acceleration.ts b/backend/src/api/services/acceleration.ts index 053da6e82..02d3482f0 100644 --- a/backend/src/api/services/acceleration.ts +++ b/backend/src/api/services/acceleration.ts @@ -5,6 +5,7 @@ import { BlockExtended } from '../../mempool.interfaces'; import axios from 'axios'; import mempool from '../mempool'; import websocketHandler from '../websocket-handler'; +import { Common } from '../common'; type MyAccelerationStatus = 'requested' | 'accelerating' | 'done'; @@ -74,6 +75,7 @@ class AccelerationApi { this.forcePoll = true; } + /** @asyncSafe */ private async $fetchAccelerations(): Promise { try { const response = await axios.get(this.apiPath, { responseType: 'json', timeout: 10000 }); @@ -238,6 +240,7 @@ class AccelerationApi { } } + /** @asyncSafe */ public async connectWebsocket(): Promise { if (this.startedWebsocketLoop) { return; @@ -314,7 +317,7 @@ class AccelerationApi { } } } - await new Promise(resolve => setTimeout(resolve, 5000)); + await Common.sleep$(5000); } } } diff --git a/backend/src/api/services/stratum.ts b/backend/src/api/services/stratum.ts index a8ee64106..57c5d3f8d 100644 --- a/backend/src/api/services/stratum.ts +++ b/backend/src/api/services/stratum.ts @@ -2,6 +2,7 @@ import { WebSocket } from 'ws'; import logger from '../../logger'; import config from '../../config'; import websocketHandler from '../websocket-handler'; +import { Common } from '../common'; export interface StratumJob { pool: number; @@ -58,6 +59,7 @@ class StratumApi { } } + /** @asyncSafe */ public async connectWebsocket(): Promise { if (!config.STRATUM.ENABLED) { return; @@ -97,7 +99,7 @@ class StratumApi { } }); } - await new Promise(resolve => setTimeout(resolve, 5000)); + await Common.sleep$(5000); } } } diff --git a/backend/src/api/services/wallets.ts b/backend/src/api/services/wallets.ts index e4578f5dc..d1ffa1f27 100644 --- a/backend/src/api/services/wallets.ts +++ b/backend/src/api/services/wallets.ts @@ -56,10 +56,11 @@ class WalletApi { // Load cache on startup if (config.WALLETS.ENABLED) { - this.$loadCache(); + void this.$loadCache(); } } + /** @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/api/statistics/statistics.ts b/backend/src/api/statistics/statistics.ts index 62dff3d66..d6ac7a543 100644 --- a/backend/src/api/statistics/statistics.ts +++ b/backend/src/api/statistics/statistics.ts @@ -22,13 +22,14 @@ class Statistics { const difference = nextInterval.getTime() - now.getTime(); setTimeout(() => { - this.runStatistics(); + void this.runStatistics(); this.intervalTimer = setInterval(() => { - this.runStatistics(true); + void this.runStatistics(true); }, 1 * 60 * 1000); }, difference); } + /** @asyncSafe */ public async runStatistics(skipIfRecent = false): Promise { if (!memPool.isInSync()) { return; diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index bc5b65607..0345282b0 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -47,6 +47,7 @@ class TransactionUtils { * @param addPrevouts * @param lazyPrevouts * @param forceCore - See https://github.com/mempool/mempool/issues/2904 + * @asyncUnsafe */ public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise { let transaction: IEsploraApi.Transaction; @@ -69,10 +70,12 @@ class TransactionUtils { } } + /** @asyncUnsafe */ public async $getMempoolTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise { return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended; } + /** @asyncUnsafe */ public async $getMempoolTransactionsExtended(txids: string[], addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise { if (forceCore || config.MEMPOOL.BACKEND !== 'esplora') { const limiter = pLimit(8); // Run 8 requests at a time diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 6186932b2..70c2bd0af 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -1003,6 +1003,7 @@ class WebsocketHandler { } } + /** @asyncUnsafe */ async handleNewBlock(block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]): Promise { if (!this.webSocketServers.length) { throw new Error('No WebSocket.Server have been set'); @@ -1055,7 +1056,7 @@ class WebsocketHandler { totalWeight += (tx.vsize * 4); } - BlocksSummariesRepository.$saveTemplate({ + void BlocksSummariesRepository.$saveTemplate({ height: block.height, template: { id: block.id, @@ -1064,7 +1065,7 @@ class WebsocketHandler { version: 1, }); - BlocksAuditsRepository.$saveAudit({ + void BlocksAuditsRepository.$saveAudit({ version: 1, time: block.timestamp, height: block.height, @@ -1100,7 +1101,7 @@ class WebsocketHandler { const firstSeen = getRecentFirstSeen(block.id); if (firstSeen) { if (config.DATABASE.ENABLED) { - BlocksRepository.$saveFirstSeenTime(block.id, firstSeen); + void BlocksRepository.$saveFirstSeenTime(block.id, firstSeen); } block.extras.firstSeen = firstSeen; } @@ -1485,6 +1486,7 @@ class WebsocketHandler { return addressCache; } + /** @asyncSafe */ private async getFullTransactions(transactions: MempoolTransactionExtended[]): Promise { for (let i = 0; i < transactions.length; i++) { try { diff --git a/backend/src/database.ts b/backend/src/database.ts index 818878a68..6e853c6e1 100644 --- a/backend/src/database.ts +++ b/backend/src/database.ts @@ -25,6 +25,7 @@ import { execSync } from 'child_process'; timezone: '+00:00', }; + /** @asyncUnsafe */ private checkDBFlag() { if (config.DATABASE.ENABLED === false) { const stack = new Error().stack; @@ -32,6 +33,7 @@ import { execSync } from 'child_process'; } } + /** @asyncUnsafe */ public async query(query, params?, errorLogLevel: LogLevel | 'silent' = 'debug', connection?: PoolConnection): Promise<[T, FieldPacket[]]> { @@ -76,6 +78,7 @@ import { execSync } from 'child_process'; } } + /** @asyncSafe */ private async $rollbackAtomic(connection: PoolConnection): Promise { try { await connection.rollback(); @@ -85,6 +88,7 @@ import { execSync } from 'child_process'; } } + /** @asyncSafe */ public async $atomicQuery(queries: { query, params }[], errorLogLevel: LogLevel | 'silent' = 'debug'): Promise<[T, FieldPacket[]][]> { @@ -116,6 +120,8 @@ import { execSync } from 'child_process'; } } + + /** @asyncSafe */ public async checkDbConnection() { this.checkDBFlag(); try { @@ -171,10 +177,12 @@ import { execSync } from 'child_process'; } } + /** @asyncSafe */ private async getPool(): Promise { if (this.pool === null) { this.pool = createPool(this.poolConfig); this.pool.on('connection', function (newConnection: PoolConnection) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- callback API, not a promise despite types newConnection.query(`SET time_zone='+00:00'`); }); } @@ -188,7 +196,11 @@ import { execSync } from 'child_process'; */ public async close(): Promise { if (this.pool !== null) { - await this.pool.end(); + try { + await this.pool.end(); + } catch (e) { + logger.err(`Exception in close. Reason: ${(e instanceof Error ? e.message : e)}`); + } this.pool = null; logger.debug('Database connection pool closed'); } diff --git a/backend/src/index.ts b/backend/src/index.ts index 7fbcc9c4a..45dc16fb4 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -68,7 +68,7 @@ class Server { this.app = express(); if (!config.MEMPOOL.SPAWN_CLUSTER_PROCS) { - this.startServer(); + void this.startServer(); return; } @@ -92,10 +92,11 @@ class Server { }, 10000); }); } else { - this.startServer(true); + void this.startServer(true); } } + /** @asyncSafe */ async startServer(worker = false): Promise { logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`); @@ -148,6 +149,7 @@ class Server { ; if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) { + /** @asyncUnsafe */ await priceUpdater.$initializeLatestPriceWithDb(); } @@ -168,12 +170,14 @@ class Server { await syncAssets.syncAssets$(); if (config.DATABASE.ENABLED) { + /** @asyncUnsafe */ await mempoolBlocks.updatePools$(); } if (config.MEMPOOL.ENABLED) { if (config.MEMPOOL.CACHE_ENABLED) { await diskCache.$loadMempoolCache(); } else if (config.REDIS.ENABLED) { + /** @asyncUnsafe */ await redisCache.$loadCache(); } } @@ -197,20 +201,20 @@ class Server { } if (config.FIAT_PRICE.ENABLED) { - priceUpdater.$run(); + void priceUpdater.$run(); } await chainTips.updateOrphanedBlocks(); this.setUpHttpApiRoutes(); if (config.MEMPOOL.ENABLED) { - this.runMainUpdateLoop(); + void this.runMainUpdateLoop(); } setInterval(() => { this.healthCheck(); }, 2500); if (config.LIGHTNING.ENABLED) { - this.$runLightningBackend(); + void this.$runLightningBackend(); } this.server.listen(config.MEMPOOL.HTTP_PORT, () => { @@ -231,9 +235,10 @@ class Server { }); } - poolsUpdater.$startService(); + void poolsUpdater.$startService(); } + /** @asyncSafe */ async runMainUpdateLoop(): Promise { const start = Date.now(); try { @@ -256,13 +261,13 @@ class Server { if (numHandledBlocks === 0) { await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate); } - indexer.$run(); + void indexer.$run(); if (config.WALLETS.ENABLED) { // might take a while, so run in the background - walletApi.$syncWallets(); + void walletApi.$syncWallets(); } if (config.FIAT_PRICE.ENABLED) { - priceUpdater.$run(); + void priceUpdater.$run(); } // rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS @@ -294,6 +299,7 @@ class Server { } } + /** @asyncSafe */ async $runLightningBackend(): Promise { try { await fundingTxFetcher.$init(); @@ -303,7 +309,7 @@ class Server { } catch(e) { logger.err(`Exception in $runLightningBackend. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`); await Common.sleep$(1000 * 60); - this.$runLightningBackend(); + void this.$runLightningBackend(); }; } @@ -336,9 +342,9 @@ class Server { } loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler)); - accelerationApi.connectWebsocket(); + void accelerationApi.connectWebsocket(); if (config.STRATUM.ENABLED) { - stratumApi.connectWebsocket(); + void stratumApi.connectWebsocket(); } } diff --git a/backend/src/indexer.ts b/backend/src/indexer.ts index b052c08c2..84fe98a52 100644 --- a/backend/src/indexer.ts +++ b/backend/src/indexer.ts @@ -35,6 +35,8 @@ class Indexer { /** * Check which core index is available for indexing + * + * @asyncUnsafe */ public async checkAvailableCoreIndexes(): Promise { const updatedCoreIndexes: CoreIndex[] = []; @@ -52,7 +54,7 @@ class Indexer { if (indexName === 'coinstatsindex' && newState.synced === true) { const previousState = this.isCoreIndexReady('coinstatsindex'); // if (!previousState || previousState.synced === false) { - this.runSingleTask('coinStatsIndex'); + void this.runSingleTask('coinStatsIndex'); // } } } @@ -125,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]) { @@ -163,6 +167,7 @@ class Indexer { this.tasksRunning[task] = false; } + /** @asyncSafe */ public async $run(): Promise { if (!Common.indexingEnabled() || this.runIndexer === false || this.indexerRunning === true || mempool.hasPriority() @@ -207,7 +212,7 @@ class Indexer { return; } - this.runSingleTask('blocksPrices'); + void this.runSingleTask('blocksPrices'); await blocks.$indexCoinbaseAddresses(); await mining.$indexDifficultyAdjustments(); await mining.$generateNetworkHashrateHistory(); @@ -221,7 +226,7 @@ class Indexer { await BlocksAuditsRepository.$migrateAuditsV0toV1(); await BlocksRepository.$migrateBlocks(); // do not wait for classify blocks to finish - blocks.$classifyBlocks(); + void blocks.$classifyBlocks(); runSuccessful = true; } catch (e) { nextRunDelay = retryDelay; diff --git a/backend/src/replication/AuditReplication.ts b/backend/src/replication/AuditReplication.ts index 6f616dbbe..d92614746 100644 --- a/backend/src/replication/AuditReplication.ts +++ b/backend/src/replication/AuditReplication.ts @@ -17,6 +17,7 @@ class AuditReplication { inProgress: boolean = false; skip: Set = new Set(); + /** @asyncUnsafe */ public async $sync(): Promise { if (!config.REPLICATION.ENABLED || !config.REPLICATION.AUDIT) { // replication not enabled @@ -54,6 +55,7 @@ class AuditReplication { this.inProgress = false; } + /** @asyncUnsafe */ private async $syncAudit(hash: string): Promise { if (this.skip.has(hash)) { // we already know none of our trusted servers have this audit @@ -77,6 +79,8 @@ class AuditReplication { return success; } + + /** @asyncSafe */ private async $getMissingAuditBlocks(): Promise { try { const startHeight = config.REPLICATION.AUDIT_START_HEIGHT || 0; diff --git a/backend/src/replication/StatisticsReplication.ts b/backend/src/replication/StatisticsReplication.ts index 59ecf202f..9569386f2 100644 --- a/backend/src/replication/StatisticsReplication.ts +++ b/backend/src/replication/StatisticsReplication.ts @@ -31,6 +31,7 @@ const steps = { class StatisticsReplication { inProgress: boolean = false; + /** @asyncUnsafe */ public async $sync(): Promise { if (!config.REPLICATION.ENABLED || !config.REPLICATION.STATISTICS || !config.STATISTICS.ENABLED) { // replication not enabled, or statistics not enabled @@ -74,6 +75,7 @@ class StatisticsReplication { this.inProgress = false; } + /** @asyncUnsafe */ private async $syncStatistics(interval: string, missingTimes: Set): Promise { let success = false; @@ -105,6 +107,8 @@ class StatisticsReplication { return { success, synced, missed: missed.size }; } + + /** @asyncUnsafe */ private async $getMissingStatistics(): Promise { try { const now = Math.floor(Date.now() / 1000); @@ -146,6 +150,7 @@ class StatisticsReplication { } } + /** @asyncUnsafe */ private async $getMissingStatisticsInterval(interval: any, startTime: number): Promise> { try { const start = interval[0]; diff --git a/backend/src/replication/replicator.ts b/backend/src/replication/replicator.ts index df90a7d05..853dbdb39 100644 --- a/backend/src/replication/replicator.ts +++ b/backend/src/replication/replicator.ts @@ -4,6 +4,7 @@ import axios, { AxiosResponse } from 'axios'; import { SocksProxyAgent } from 'socks-proxy-agent'; import * as https from 'https'; +/** @asyncSafe */ export async function $sync(path): Promise<{ data?: any, exists: boolean, server?: string }> { // start with a random server so load is uniformly spread let allMissing = true; @@ -33,6 +34,7 @@ export async function $sync(path): Promise<{ data?: any, exists: boolean, server return { exists: !allMissing }; } +/** @asyncUnsafe */ export async function query(path): Promise { type axiosOptions = { headers: { diff --git a/backend/src/repositories/AccelerationRepository.ts b/backend/src/repositories/AccelerationRepository.ts index 76926839f..a45a80621 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) { @@ -60,28 +61,34 @@ class AccelerationRepository { } } + /** @asyncSafe */ public async $getAccelerationInfoForTxid(txid: string): Promise { - const [rows] = await DB.query(` - SELECT *, UNIX_TIMESTAMP(requested) as requested_timestamp, UNIX_TIMESTAMP(added) as block_timestamp FROM accelerations - JOIN pools on pools.unique_id = accelerations.pool - WHERE txid = ? - `, [txid]) as RowDataPacket[][]; - if (rows?.length) { - const row = rows[0]; - return { - txid: row.txid, - height: row.height, - added: row.requested_timestamp || row.block_timestamp, - pool: { - id: row.id, - slug: row.slug, - name: row.name, - }, - effective_vsize: row.effective_vsize, - effective_fee: row.effective_fee, - boost_rate: row.boost_rate, - boost_cost: row.boost_cost, - }; + try { + const [rows] = await DB.query(` + SELECT *, UNIX_TIMESTAMP(requested) as requested_timestamp, UNIX_TIMESTAMP(added) as block_timestamp FROM accelerations + JOIN pools on pools.unique_id = accelerations.pool + WHERE txid = ? + `, [txid]) as RowDataPacket[][]; + if (rows?.length) { + const row = rows[0]; + return { + txid: row.txid, + height: row.height, + added: row.requested_timestamp || row.block_timestamp, + pool: { + id: row.id, + slug: row.slug, + name: row.name, + }, + effective_vsize: row.effective_vsize, + effective_fee: row.effective_fee, + boost_rate: row.boost_rate, + boost_cost: row.boost_cost, + }; + } + } catch (e: any) { + logger.err(`Cannot get acceleration info for txid ${txid}. Reason: ` + (e instanceof Error ? e.message : e)); + return null; } return null; } @@ -191,6 +198,7 @@ class AccelerationRepository { } } + /** @asyncSafe */ public async $getLastSyncedHeight(): Promise { try { const [rows] = await DB.query(` @@ -206,6 +214,7 @@ class AccelerationRepository { return 0; } + /** @asyncSafe */ private async $setLastSyncedHeight(height: number): Promise { try { await DB.query(` @@ -219,6 +228,7 @@ class AccelerationRepository { } // modifies block transactions + /** @asyncSafe */ public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise { const blockTxs: { [txid: string]: MempoolTransactionExtended } = {}; for (const tx of transactions) { @@ -237,7 +247,7 @@ class AccelerationRepository { const tx = blockTxs[acc.txid]; const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions); accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost)); - this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, successfulAccelerations); + void this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, successfulAccelerations); } } let anyConfirmed = false; diff --git a/backend/src/repositories/BlocksAuditsRepository.ts b/backend/src/repositories/BlocksAuditsRepository.ts index 7250304c1..e682cab59 100644 --- a/backend/src/repositories/BlocksAuditsRepository.ts +++ b/backend/src/repositories/BlocksAuditsRepository.ts @@ -15,6 +15,7 @@ interface MigrationAudit { } class BlocksAuditRepositories { + /** @asyncSafe */ public async $saveAudit(audit: BlockAudit): Promise { try { await DB.query(`INSERT INTO blocks_audits(version, time, height, hash, unseen_txs, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight) @@ -29,6 +30,7 @@ class BlocksAuditRepositories { } } + /** @asyncSafe */ public async $setSummary(hash: string, expectedFees: number, expectedWeight: number) { try { await DB.query(` @@ -42,6 +44,7 @@ class BlocksAuditRepositories { } } + /** @asyncSafe */ public async $getBlocksHealthHistory(div: number, interval: string | null): Promise { try { let query = `SELECT UNIX_TIMESTAMP(time) as time, height, match_rate FROM blocks_audits`; @@ -60,6 +63,7 @@ class BlocksAuditRepositories { } } + /** @asyncSafe */ public async $getBlocksHealthCount(): Promise { try { const [rows] = await DB.query(`SELECT count(hash) as count FROM blocks_audits`); @@ -70,6 +74,7 @@ class BlocksAuditRepositories { } } + /** @asyncSafe */ public async $getBlockAudit(hash: string): Promise { try { const [rows]: any[] = await DB.query( @@ -115,6 +120,7 @@ class BlocksAuditRepositories { } } + /** @asyncSafe */ public async $getBlockTxAudit(hash: string, txid: string): Promise { try { const blockAudit = await this.$getBlockAudit(hash); @@ -151,6 +157,7 @@ class BlocksAuditRepositories { } } + /** @asyncSafe */ public async $getBlockAuditScore(hash: string): Promise { try { const [rows]: any[] = await DB.query( @@ -165,6 +172,7 @@ class BlocksAuditRepositories { } } + /** @asyncSafe */ public async $getBlockAuditScores(maxHeight: number, minHeight: number): Promise { try { const [rows]: any[] = await DB.query( @@ -179,6 +187,7 @@ class BlocksAuditRepositories { } } + /** @asyncSafe */ public async $getBlocksWithoutSummaries(): Promise { try { const [fromRows]: any[] = await DB.query(` @@ -207,6 +216,7 @@ class BlocksAuditRepositories { /** * [INDEXING] Migrate audits from v0 to v1 + * @asyncSafe */ public async $migrateAuditsV0toV1(): Promise { try { diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 8f2579b48..2f2a6f515 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -114,6 +114,7 @@ class BlocksRepository { /** * Save indexed block data in the database + * @asyncSafe */ public async $saveBlockInDatabase(block: BlockExtended) { const truncatedCoinbaseSignature = block?.extras?.coinbaseSignature?.substring(0, 500); @@ -220,6 +221,7 @@ class BlocksRepository { * * @param utxoSetSize * @param totalInputAmt + * @asyncSafe */ public async $updateCoinStatsIndexData(blockHash: string, utxoSetSize: number, totalInputAmt: number @@ -248,6 +250,7 @@ class BlocksRepository { * @param blockHash * @param feeAmtPercentiles * @param medianFeeAmt + * @asyncSafe */ public async $updateFeeAmounts(blockHash: string, feeAmtPercentiles, medianFeeAmt) : Promise { try { @@ -270,6 +273,7 @@ class BlocksRepository { /** * Get all block height that have not been indexed between [startHeight, endHeight] + * @asyncSafe */ public async $getMissingBlocksBetweenHeights(startHeight: number, endHeight: number): Promise { // Ensure startHeight is the lower value and endHeight is the higher value @@ -302,6 +306,7 @@ class BlocksRepository { /** * Get empty blocks for one or all pools + * @asyncSafe */ public async $countEmptyBlocks(poolId: number | null, interval: string | null = null): Promise { interval = Common.getSqlInterval(interval); @@ -334,6 +339,7 @@ class BlocksRepository { /** * Return most recent block height + * @asyncSafe */ public async $mostRecentBlockHeight(): Promise { try { @@ -347,6 +353,7 @@ class BlocksRepository { /** * Get blocks count for a period + * @asyncSafe */ public async $blockCount(poolId: number | null, interval: string | null = null): Promise { interval = Common.getSqlInterval(interval); @@ -380,6 +387,7 @@ class BlocksRepository { * @param from - The oldest timestamp * @param to - The newest timestamp * @returns + * @asyncSafe */ public async $blockCountBetweenTimestamp(poolId: number | null, from: number, to: number): Promise { const params: any[] = []; @@ -407,6 +415,7 @@ class BlocksRepository { /** * Get blocks count for a period + * @asyncSafe */ public async $blockCountBetweenHeight(startHeight: number, endHeight: number): Promise { const params: any[] = []; @@ -425,6 +434,7 @@ class BlocksRepository { /** * Get average block health for all blocks for a single pool + * @asyncSafe */ public async $getAvgBlockHealthPerPoolId(poolId: number): Promise { const params: any[] = []; @@ -450,6 +460,7 @@ class BlocksRepository { /** * Get average block health for all blocks for a single pool + * @asyncSafe */ public async $getTotalRewardForPoolId(poolId: number): Promise { const params: any[] = []; @@ -474,6 +485,7 @@ class BlocksRepository { /** * Get the oldest indexed block + * @asyncSafe */ public async $oldestBlockTimestamp(): Promise { const query = `SELECT UNIX_TIMESTAMP(blockTimestamp) as blockTimestamp @@ -498,6 +510,7 @@ class BlocksRepository { /** * Get blocks mined by a specific mining pool + * @asyncSafe */ public async $getBlocksByPool(slug: string, startHeight?: number): Promise { const pool = await PoolsRepository.$getPool(slug); @@ -538,6 +551,7 @@ class BlocksRepository { /** * Get one block by height + * @asyncSafe */ public async $getBlockByHeight(height: number): Promise { try { @@ -586,6 +600,7 @@ class BlocksRepository { /** * Return blocks difficulty + * @asyncSafe */ public async $getBlocksDifficulty(): Promise { try { @@ -601,6 +616,7 @@ class BlocksRepository { * Get the first block at or directly after a given timestamp * @param timestamp number unix time in seconds * @returns The height and timestamp of a block (timestamp might vary from given timestamp) + * @asyncSafe */ public async $getBlockHeightFromTimestamp( timestamp: number, @@ -629,6 +645,7 @@ class BlocksRepository { /** * Get general block stats + * @asyncSafe */ public async $getBlockStats(blockCount: number): Promise { try { @@ -652,6 +669,7 @@ class BlocksRepository { /** * Check if the canonical chain of blocks is valid and fix it if needed + * @asyncSafe */ public async $validateChain(): Promise { try { @@ -737,6 +755,7 @@ class BlocksRepository { /** * Get the historical averaged block fees + * @asyncSafe */ public async $getHistoricalBlockFees(div: number, interval: string | null, timespan?: {from: number, to: number}): Promise { try { @@ -769,6 +788,7 @@ class BlocksRepository { /** * Get the historical averaged block rewards + * @asyncSafe */ public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise { try { @@ -799,6 +819,7 @@ class BlocksRepository { /** * Get the historical averaged block fee rate percentiles + * @asyncSafe */ public async $getHistoricalBlockFeeRates(div: number, interval: string | null): Promise { try { @@ -831,6 +852,7 @@ class BlocksRepository { /** * Get the historical averaged block sizes + * @asyncSafe */ public async $getHistoricalBlockSizes(div: number, interval: string | null): Promise { try { @@ -857,6 +879,7 @@ class BlocksRepository { /** * Get the historical averaged block weights + * @asyncSafe */ public async $getHistoricalBlockWeights(div: number, interval: string | null): Promise { try { @@ -884,6 +907,7 @@ class BlocksRepository { /** * Get a list of blocks that have been indexed * (includes stale blocks) + * @asyncSafe */ public async $getIndexedBlocks(): Promise<{ height: number, hash: string, stale: boolean }[]> { try { @@ -897,6 +921,7 @@ class BlocksRepository { /** * Get a list of blocks that have not had CPFP data indexed + * @asyncSafe */ public async $getCPFPUnindexedBlocks(): Promise { try { @@ -930,6 +955,7 @@ class BlocksRepository { /** * Return the oldest block from a consecutive chain of block from the most recent one + * @asyncSafe */ public async $getOldestConsecutiveBlock(): Promise { try { @@ -948,6 +974,7 @@ class BlocksRepository { /** * Get all blocks which have not be linked to a price yet + * @asyncSafe */ public async $getBlocksWithoutPrice(): Promise { try { @@ -969,6 +996,7 @@ class BlocksRepository { /** * Save block price by batch + * @asyncSafe */ public async $saveBlockPrices(blockPrices: BlockPrice[]): Promise { try { @@ -990,6 +1018,7 @@ class BlocksRepository { /** * Get all indexed blocsk with missing coinstatsindex data + * @asyncSafe */ public async $getBlocksMissingCoinStatsIndex(maxHeight: number, minHeight: number): Promise { try { @@ -1009,6 +1038,7 @@ class BlocksRepository { /** * Get all indexed blocks with missing coinbase addresses * (includes stale blocks) + * @asyncSafe */ public async $getBlocksWithoutCoinbaseAddresses(): Promise { try { @@ -1031,6 +1061,7 @@ class BlocksRepository { * * @param id * @param feePercentiles + * @asyncSafe */ public async $saveFeePercentilesForBlockId(id: string, feePercentiles: number[]): Promise { try { @@ -1050,6 +1081,7 @@ class BlocksRepository { * * @param id * @param feeStats + * @asyncSafe */ public async $saveEffectiveFeeStats(id: string, feeStats: EffectiveFeeStats): Promise { try { @@ -1069,6 +1101,7 @@ class BlocksRepository { * * @param id * @param addresses + * @asyncSafe */ public async $saveCoinbaseAddresses(id: string, addresses: string[]): Promise { try { @@ -1088,6 +1121,7 @@ class BlocksRepository { * * @param id * @param poolId + * @asyncSafe */ public async $savePool(id: string, poolId: number): Promise { try { @@ -1106,6 +1140,7 @@ class BlocksRepository { * Save block first seen time * * @param id + * @asyncSafe */ public async $saveFirstSeenTime(id: string, firstSeen: number): Promise { try { @@ -1153,6 +1188,7 @@ class BlocksRepository { * must provide the correct field into dbBlk object param * * @param dbBlk + * @asyncUnsafe */ private async formatDbBlockIntoExtendedBlock(dbBlk: DatabaseBlock): Promise { const blk: Partial = {}; @@ -1272,6 +1308,7 @@ class BlocksRepository { } // migration to fix median fee bug + /** @asyncSafe */ private async $migrateBlocksToV1(): Promise { let blocksMigrated = 0; try { diff --git a/backend/src/repositories/BlocksSummariesRepository.ts b/backend/src/repositories/BlocksSummariesRepository.ts index 4ab26d2dd..fc2771ce8 100644 --- a/backend/src/repositories/BlocksSummariesRepository.ts +++ b/backend/src/repositories/BlocksSummariesRepository.ts @@ -5,6 +5,7 @@ import logger from '../logger'; import { BlockSummary, TransactionClassified } from '../mempool.interfaces'; class BlocksSummariesRepository { + /** @asyncSafe */ public async $getByBlockId(id: string): Promise { try { const [summary]: any[] = await DB.query(`SELECT * from blocks_summaries WHERE id = ?`, [id]); @@ -19,6 +20,7 @@ class BlocksSummariesRepository { return undefined; } + /** @asyncSafe */ public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionClassified[], version: number): Promise { try { const transactionsStr = JSON.stringify(transactions); @@ -33,6 +35,7 @@ class BlocksSummariesRepository { } } + /** @asyncSafe */ public async $saveTemplate(params: { height: number, template: BlockSummary, version: number}): Promise { const blockId = params.template?.id; try { @@ -53,6 +56,7 @@ class BlocksSummariesRepository { } } + /** @asyncSafe */ public async $getTemplate(id: string): Promise { try { const [templates]: any[] = await DB.query(`SELECT * from blocks_templates WHERE id = ?`, [id]); @@ -69,6 +73,7 @@ class BlocksSummariesRepository { return undefined; } + /** @asyncSafe */ public async $getIndexedSummariesId(): Promise { try { const [rows] = await DB.query(`SELECT id from blocks_summaries`) as RowDataPacket[][]; @@ -80,6 +85,7 @@ class BlocksSummariesRepository { return []; } + /** @asyncSafe */ public async $getSummariesWithVersion(version: number): Promise<{ height: number, id: string }[]> { try { const [rows]: any[] = await DB.query(` @@ -97,6 +103,7 @@ class BlocksSummariesRepository { return []; } + /** @asyncSafe */ public async $getTemplatesWithVersion(version: number): Promise<{ height: number, id: string }[]> { try { const [rows]: any[] = await DB.query(` @@ -115,6 +122,7 @@ class BlocksSummariesRepository { return []; } + /** @asyncSafe */ public async $getSummariesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> { try { const [rows]: any[] = await DB.query(` @@ -133,6 +141,7 @@ class BlocksSummariesRepository { return []; } + /** @asyncSafe */ public async $getTemplatesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> { try { const [rows]: any[] = await DB.query(` @@ -156,6 +165,7 @@ class BlocksSummariesRepository { * Get the fee percentiles if the block has already been indexed, [] otherwise * * @param id + * @asyncSafe */ public async $getFeePercentilesByBlockId(id: string): Promise { try { diff --git a/backend/src/repositories/CpfpRepository.ts b/backend/src/repositories/CpfpRepository.ts index 0242188df..3be43eb64 100644 --- a/backend/src/repositories/CpfpRepository.ts +++ b/backend/src/repositories/CpfpRepository.ts @@ -73,6 +73,8 @@ class CpfpRepository { } } + + /** @asyncUnsafe */ public async $getCluster(clusterRoot: string): Promise { const [clusterRows]: any = await DB.query( ` @@ -91,6 +93,7 @@ class CpfpRepository { return; } + /** @asyncUnsafe */ public async $getClustersAt(height: number): Promise { const [clusterRows]: any = await DB.query( ` diff --git a/backend/src/repositories/NodeRecordsRepository.ts b/backend/src/repositories/NodeRecordsRepository.ts index cf676e35e..cd5856ee1 100644 --- a/backend/src/repositories/NodeRecordsRepository.ts +++ b/backend/src/repositories/NodeRecordsRepository.ts @@ -9,6 +9,7 @@ export interface NodeRecord { } class NodesRecordsRepository { + /** @asyncSafe */ public async $saveRecord(record: NodeRecord): Promise { try { const payloadBytes = Buffer.from(record.payload, 'base64'); @@ -26,6 +27,7 @@ class NodesRecordsRepository { } } + /** @asyncSafe */ public async $getRecordTypes(publicKey: string): Promise { try { const query = ` @@ -40,6 +42,7 @@ class NodesRecordsRepository { } } + /** @asyncSafe */ public async $deleteUnusedRecords(publicKey: string, recordTypes: number[]): Promise { try { let query; diff --git a/backend/src/repositories/NodesSocketsRepository.ts b/backend/src/repositories/NodesSocketsRepository.ts index e85126de4..8ddfafe68 100644 --- a/backend/src/repositories/NodesSocketsRepository.ts +++ b/backend/src/repositories/NodesSocketsRepository.ts @@ -9,6 +9,7 @@ export interface NodeSocket { } class NodesSocketsRepository { + /** @asyncSafe */ public async $saveSocket(socket: NodeSocket): Promise { try { await DB.query(` @@ -23,6 +24,7 @@ class NodesSocketsRepository { } } + /** @asyncSafe */ public async $deleteUnusedSockets(publicKey: string, addresses: string[]): Promise { if (addresses.length === 0) { return 0; diff --git a/backend/src/repositories/PoolsRepository.ts b/backend/src/repositories/PoolsRepository.ts index e17d4136c..fcc218f7b 100644 --- a/backend/src/repositories/PoolsRepository.ts +++ b/backend/src/repositories/PoolsRepository.ts @@ -8,6 +8,7 @@ import { PoolInfo, PoolTag } from '../mempool.interfaces'; class PoolsRepository { /** * Get all pools tagging info + * @asyncUnsafe */ public async $getPools(): Promise { const [rows] = await DB.query('SELECT id, unique_id as uniqueId, name, addresses, regexes, slug FROM pools'); @@ -16,6 +17,7 @@ class PoolsRepository { /** * Get unknown pool tagging info + * @asyncUnsafe */ public async $getUnknownPool(): Promise { let [rows]: any[] = await DB.query('SELECT id, unique_id as uniqueId, name, slug FROM pools where name = "Unknown"'); @@ -28,6 +30,7 @@ class PoolsRepository { /** * Get basic pool info and block count + * @asyncSafe */ public async $getPoolsInfo(interval: string | null = null): Promise { interval = Common.getSqlInterval(interval); @@ -66,6 +69,7 @@ class PoolsRepository { /** * Get basic pool info and block count between two timestamp + * @asyncSafe */ public async $getPoolsInfoBetween(from: number, to: number): Promise { const query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName @@ -85,6 +89,7 @@ class PoolsRepository { /** * Get a mining pool info + * @asyncSafe */ public async $getPool(slug: string, parse: boolean = true): Promise { const query = ` @@ -117,6 +122,7 @@ class PoolsRepository { /** * Get a mining pool info by its unique id + * @asyncSafe */ public async $getPoolByUniqueId(id: number, parse: boolean = true): Promise { const query = ` @@ -151,6 +157,7 @@ class PoolsRepository { * Insert a new mining pool in the database * * @param pool + * @asyncSafe */ public async $insertNewMiningPool(pool: any, slug: string): Promise { try { @@ -170,6 +177,7 @@ class PoolsRepository { * @param dbId * @param newSlug * @param newName + * @asyncSafe */ public async $renameMiningPool(dbId: number, newSlug: string, newName: string): Promise { try { @@ -189,6 +197,7 @@ class PoolsRepository { * * @param dbId * @param newLink + * @asyncSafe */ public async $updateMiningPoolLink(dbId: number, newLink: string): Promise { try { @@ -210,6 +219,7 @@ class PoolsRepository { * @param dbId * @param addresses * @param regexes + * @asyncSafe */ public async $updateMiningPoolTags(dbId: number, addresses: string, regexes: string): Promise { try { diff --git a/backend/src/repositories/PricesRepository.ts b/backend/src/repositories/PricesRepository.ts index 236519420..ac2558cc8 100644 --- a/backend/src/repositories/PricesRepository.ts +++ b/backend/src/repositories/PricesRepository.ts @@ -224,6 +224,7 @@ class PricesRepository { } } + /** @asyncUnsafe */ public async $getOldestPriceTime(): Promise { const [oldestRow] = await DB.query(` SELECT UNIX_TIMESTAMP(time) AS time @@ -234,6 +235,7 @@ class PricesRepository { return oldestRow[0] ? oldestRow[0].time : 0; } + /** @asyncUnsafe */ public async $getLatestPriceId(): Promise { const [oldestRow] = await DB.query(` SELECT id @@ -244,6 +246,7 @@ class PricesRepository { return oldestRow[0] ? oldestRow[0].id : null; } + /** @asyncUnsafe */ public async $getLatestPriceTime(): Promise { const [oldestRow] = await DB.query(` SELECT UNIX_TIMESTAMP(time) AS time @@ -254,6 +257,7 @@ class PricesRepository { return oldestRow[0] ? oldestRow[0].time : 0; } + /** @asyncUnsafe */ public async $getPricesTimes(): Promise { const [times] = await DB.query(` SELECT UNIX_TIMESTAMP(time) AS time @@ -267,6 +271,7 @@ class PricesRepository { return times.map(time => time.time); } + /** @asyncUnsafe */ public async $getPricesTimesWithMissingFields(): Promise<{time: number, USD: number, eur_missing: boolean, gbp_missing: boolean, cad_missing: boolean, chf_missing: boolean, aud_missing: boolean, jpy_missing: boolean}[]> { const [times] = await DB.query(` SELECT UNIX_TIMESTAMP(time) AS time, @@ -289,6 +294,7 @@ class PricesRepository { return times as {time: number, USD: number, eur_missing: boolean, gbp_missing: boolean, cad_missing: boolean, chf_missing: boolean, aud_missing: boolean, jpy_missing: boolean}[]; } + /** @asyncUnsafe */ public async $getPricesTimesAndId(): Promise<{time: number, id: number, USD: number}[]> { const [times] = await DB.query(` SELECT @@ -302,6 +308,7 @@ class PricesRepository { return times as {time: number, id: number, USD: number}[]; } + /** @asyncUnsafe */ public async $getLatestConversionRates(): Promise { const [rates] = await DB.query(` SELECT ${ApiPriceFields} @@ -317,6 +324,7 @@ class PricesRepository { return rates[0] as ApiPrice; } + /** @asyncSafe */ public async $getNearestHistoricalPrice(timestamp: number | undefined, currency?: string): Promise { try { const [rates] = await DB.query(` @@ -428,6 +436,7 @@ class PricesRepository { } } + /** @asyncSafe */ public async $getHistoricalPrices(currency?: string): Promise { try { const [rates] = await DB.query(` diff --git a/backend/src/sync-assets.ts b/backend/src/sync-assets.ts index f4cc1f266..dfae10d04 100644 --- a/backend/src/sync-assets.ts +++ b/backend/src/sync-assets.ts @@ -10,6 +10,7 @@ const PATH = './'; class SyncAssets { constructor() { } + /** @asyncSafe */ public async syncAssets$() { for (const url of config.MEMPOOL.EXTERNAL_ASSETS) { try { diff --git a/backend/src/tasks/lightning/forensics.service.ts b/backend/src/tasks/lightning/forensics.service.ts index 47b7d6aa5..7c6b6f6ac 100644 --- a/backend/src/tasks/lightning/forensics.service.ts +++ b/backend/src/tasks/lightning/forensics.service.ts @@ -23,6 +23,7 @@ class ForensicsService { await this.$runTasks(); } + /** @asyncSafe */ private async $runTasks(): Promise { try { logger.debug(`Running forensics scans`); @@ -36,7 +37,7 @@ class ForensicsService { logger.err('ForensicsService.$runTasks() error: ' + (e instanceof Error ? e.message : e)); } - setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.FORENSICS_INTERVAL); + setTimeout(() => { void this.$runTasks(); }, 1000 * config.LIGHTNING.FORENSICS_INTERVAL); } /* @@ -340,6 +341,7 @@ class ForensicsService { } } + /** @asyncSafe */ private async $attributeChannelBalances( prevChannel, openChannel, input: IEsploraApi.Vin, openContribution: number | null = null, initiator: 'remote' | 'local' | null = null, linkedOpenings: boolean = false @@ -465,6 +467,7 @@ class ForensicsService { } } + /** @asyncSafe */ async fetchTransaction(txid: string, temp: boolean = false): Promise { let tx = this.txCache[txid]; if (!tx) { @@ -485,6 +488,7 @@ class ForensicsService { // fetches a batch of transactions and adds them to the txCache // the returned list of txs does *not* preserve ordering or number + /** @asyncSafe */ async fetchTransactions(txids, temp: boolean = false): Promise<(IEsploraApi.Transaction | null)[]> { // deduplicate txids const uniqueTxids = [...new Set(txids)]; diff --git a/backend/src/tasks/lightning/network-sync.service.ts b/backend/src/tasks/lightning/network-sync.service.ts index f3a20cd22..649b70a35 100644 --- a/backend/src/tasks/lightning/network-sync.service.ts +++ b/backend/src/tasks/lightning/network-sync.service.ts @@ -29,6 +29,7 @@ class NetworkSyncService { await this.$runTasks(); } + /** @asyncSafe */ private async $runTasks(): Promise { const taskStartTime = Date.now(); try { @@ -37,7 +38,7 @@ class NetworkSyncService { const networkGraph = await lightningApi.$getNetworkGraph(); if (networkGraph.nodes.length === 0 || networkGraph.edges.length === 0) { logger.info(`LN Network graph is empty, retrying in 10 seconds`, logger.tags.ln); - setTimeout(() => { this.$runTasks(); }, 10000); + setTimeout(() => { void this.$runTasks(); }, 10000); return; } @@ -57,7 +58,7 @@ class NetworkSyncService { logger.err(`$runTasks() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln); } - setTimeout(() => { this.$runTasks(); }, Math.max(1, (1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL) - (Date.now() - taskStartTime))); + setTimeout(() => { void this.$runTasks(); }, Math.max(1, (1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL) - (Date.now() - taskStartTime))); } /** @@ -111,7 +112,9 @@ class NetworkSyncService { await nodesApi.$setNodesInactive(graphNodesPubkeys); if (config.MAXMIND.ENABLED) { - $lookupNodeLocation(); + $lookupNodeLocation().catch((e) => { + logger.err(`Error in $lookupNodeLocation: ${e instanceof Error ? e.message : e}`); + }); } } diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index 5d0ac3cfc..5bbe6784d 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -9,17 +9,19 @@ class LightningStatsUpdater { logger.info(`Starting Lightning Stats service`, logger.tags.ln); await this.$runTasks(); - LightningStatsImporter.$run(); + void LightningStatsImporter.$run(); } + /** @asyncSafe */ private async $runTasks(): Promise { await this.$logStatsDaily(); - setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.STATS_REFRESH_INTERVAL); + setTimeout(() => { void this.$runTasks(); }, 1000 * config.LIGHTNING.STATS_REFRESH_INTERVAL); } /** * Update the latest entry for each node every config.LIGHTNING.STATS_REFRESH_INTERVAL seconds + * @asyncSafe */ private async $logStatsDaily(): Promise { try { 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 a6c8b9a66..04f5c3708 100644 --- a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts +++ b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts @@ -28,6 +28,7 @@ class FundingTxFetcher { } } + /** @asyncUnsafe */ async $fetchChannelsFundingTxs(channelIds: string[]): Promise { if (this.running) { return; @@ -57,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); - 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; } } @@ -65,12 +68,15 @@ 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); - 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; } + /** @asyncUnsafe */ public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number} | null> { channelId = Common.channelIntegerIdToShortId(channelId); diff --git a/backend/src/tasks/lightning/sync-tasks/node-locations.ts b/backend/src/tasks/lightning/sync-tasks/node-locations.ts index 4eff70ab3..c3d696ff3 100644 --- a/backend/src/tasks/lightning/sync-tasks/node-locations.ts +++ b/backend/src/tasks/lightning/sync-tasks/node-locations.ts @@ -8,6 +8,7 @@ import { ResultSetHeader } from 'mysql2'; import * as IPCheck from '../../../utils/ipcheck.js'; import { Reader } from 'mmdb-lib'; +/** @asyncSafe */ export async function $lookupNodeLocation(): Promise { let loggerTimer = new Date().getTime() / 1000; let progress = 0; diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index 8891bfc3a..e15efacec 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -14,6 +14,7 @@ const fsPromises = promises; class LightningStatsImporter { topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER; + /** @asyncSafe */ async $run(): Promise { try { const [channels]: any[] = await DB.query('SELECT short_id from channels;'); @@ -33,6 +34,7 @@ class LightningStatsImporter { /** * Generate LN network stats for one day + * @asyncUnsafe */ public async computeNetworkStats(timestamp: number, networkGraph: ILightningApi.NetworkGraph, isHistorical: boolean = false): Promise { diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts index d942cee82..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 { @@ -30,6 +31,7 @@ class PoolsUpdater { } } + /** @asyncSafe */ public async updatePoolsJson(): Promise { if (['mainnet', 'testnet', 'signet', 'testnet4', 'regtest'].includes(config.MEMPOOL.NETWORK) === false || config.MEMPOOL.ENABLED === false @@ -134,6 +136,7 @@ class PoolsUpdater { /** * Fetch our latest pools-v2.json sha from github + * @asyncUnsafe */ private async fetchPoolsSha(): Promise { const response = await this.query(this.treeUrl); @@ -152,6 +155,7 @@ class PoolsUpdater { /** * Http request wrapper + * @asyncUnsafe */ private async query(path): Promise { type axiosOptions = { diff --git a/backend/src/tasks/price-feeds/bitfinex-api.ts b/backend/src/tasks/price-feeds/bitfinex-api.ts index d9f348b63..fea9a99ba 100644 --- a/backend/src/tasks/price-feeds/bitfinex-api.ts +++ b/backend/src/tasks/price-feeds/bitfinex-api.ts @@ -8,6 +8,7 @@ class BitfinexApi implements PriceFeed { public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC'; public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist'; + /** @asyncUnsafe */ public async $fetchPrice(currency): Promise { const response = await query(this.url + currency); if (response && response['last_price']) { @@ -17,6 +18,7 @@ class BitfinexApi implements PriceFeed { } } + /** @asyncUnsafe */ public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise { const priceHistory: PriceHistory = {}; diff --git a/backend/src/tasks/price-feeds/bitflyer-api.ts b/backend/src/tasks/price-feeds/bitflyer-api.ts index 72b2e6adf..3f668d325 100644 --- a/backend/src/tasks/price-feeds/bitflyer-api.ts +++ b/backend/src/tasks/price-feeds/bitflyer-api.ts @@ -11,6 +11,7 @@ class BitflyerApi implements PriceFeed { constructor() { } + /** @asyncUnsafe */ public async $fetchPrice(currency): Promise { const response = await query(this.url + currency); if (response && response['ltp']) { diff --git a/backend/src/tasks/price-feeds/coinbase-api.ts b/backend/src/tasks/price-feeds/coinbase-api.ts index d2c6d063a..0ecddfb7d 100644 --- a/backend/src/tasks/price-feeds/coinbase-api.ts +++ b/backend/src/tasks/price-feeds/coinbase-api.ts @@ -11,6 +11,7 @@ class CoinbaseApi implements PriceFeed { constructor() { } + /** @asyncUnsafe */ public async $fetchPrice(currency): Promise { const response = await query(this.url.replace('{CURRENCY}', currency)); if (response && response['data'] && response['data']['amount']) { @@ -20,6 +21,7 @@ class CoinbaseApi implements PriceFeed { } } + /** @asyncUnsafe */ public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise { const priceHistory: PriceHistory = {}; diff --git a/backend/src/tasks/price-feeds/free-currency-api.ts b/backend/src/tasks/price-feeds/free-currency-api.ts index 48e511aa8..cd8e063ea 100644 --- a/backend/src/tasks/price-feeds/free-currency-api.ts +++ b/backend/src/tasks/price-feeds/free-currency-api.ts @@ -56,6 +56,7 @@ class FreeCurrencyApi implements ConversionFeed { constructor() { } + /** @asyncUnsafe */ public async $getQuota(): Promise { const response = await query(`${this.API_URL_PREFIX}status?apikey=${this.API_KEY}`); if (response && response['quotas']) { @@ -64,6 +65,7 @@ class FreeCurrencyApi implements ConversionFeed { return null; } + /** @asyncUnsafe */ public async $fetchLatestConversionRates(): Promise { const response = await query(`${this.API_URL_PREFIX}latest?apikey=${this.API_KEY}`); if (response && response['data']) { @@ -75,6 +77,7 @@ class FreeCurrencyApi implements ConversionFeed { return emptyRates; } + /** @asyncUnsafe */ public async $fetchConversionRates(date: string): Promise { const response = await query(`${this.API_URL_PREFIX}historical?date=${date}&apikey=${this.API_KEY}`, true); if (response && response['data'] && (response['data'][date] || this.PAID)) { diff --git a/backend/src/tasks/price-feeds/gemini-api.ts b/backend/src/tasks/price-feeds/gemini-api.ts index fc86dc0a3..596c9d841 100644 --- a/backend/src/tasks/price-feeds/gemini-api.ts +++ b/backend/src/tasks/price-feeds/gemini-api.ts @@ -11,6 +11,7 @@ class GeminiApi implements PriceFeed { constructor() { } + /** @asyncUnsafe */ public async $fetchPrice(currency): Promise { const response = await query(this.url + currency); if (response && response['last']) { @@ -20,6 +21,7 @@ class GeminiApi implements PriceFeed { } } + /** @asyncUnsafe */ public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise { const priceHistory: PriceHistory = {}; diff --git a/backend/src/tasks/price-feeds/kraken-api.ts b/backend/src/tasks/price-feeds/kraken-api.ts index 0c69ecf00..674b0b936 100644 --- a/backend/src/tasks/price-feeds/kraken-api.ts +++ b/backend/src/tasks/price-feeds/kraken-api.ts @@ -21,6 +21,7 @@ class KrakenApi implements PriceFeed { return ticker; } + /** @asyncUnsafe */ public async $fetchPrice(currency): Promise { const response = await query(this.url + currency); const ticker = this.getTicker(currency); @@ -33,6 +34,7 @@ class KrakenApi implements PriceFeed { } } + /** @asyncUnsafe */ public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise { const priceHistory: PriceHistory = {}; @@ -57,6 +59,7 @@ class KrakenApi implements PriceFeed { /** * Fetch weekly price and save it into the database + * @asyncUnsafe */ public async $insertHistoricalPrice(): Promise { const existingPriceTimes = await PricesRepository.$getPricesTimes(); diff --git a/backend/src/tasks/price-updater.ts b/backend/src/tasks/price-updater.ts index 4f56af951..34646e30f 100644 --- a/backend/src/tasks/price-updater.ts +++ b/backend/src/tasks/price-updater.ts @@ -127,12 +127,15 @@ class PriceUpdater { /** * We execute this function before the websocket initialization since * the websocket init is not done asyncronously + * + * @asyncUnsafe */ public async $initializeLatestPriceWithDb(): Promise { this.latestPrices = await PricesRepository.$getLatestConversionRates(); 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 @@ -210,6 +213,7 @@ class PriceUpdater { /** * Fetch last BTC price from exchanges, average them, and save it in the database once every hour + * @asyncUnsafe */ private async $updatePrice(): Promise { let forceUpdate = false; @@ -303,6 +307,8 @@ class PriceUpdater { * We use MtGox weekly price from July 19, 2010 to September 30, 2013 * We use Kraken weekly price from October 3, 2013 up to last month * We use Kraken hourly price for the past month + * + * @asyncUnsafe */ private async $insertHistoricalPrices(): Promise { const existingPriceTimes = await PricesRepository.$getPricesTimes(); @@ -345,6 +351,8 @@ class PriceUpdater { /** * Find missing hourly prices and insert them in the database * It has a limited backward range and it depends on which API are available + * + * @asyncUnsafe */ private async $insertMissingRecentPrices(type: 'hour' | 'day'): Promise { const existingPriceTimes = await PricesRepository.$getPricesTimes(); @@ -410,6 +418,8 @@ class PriceUpdater { /** * Find missing prices for additional currencies and insert them in the database * We calculate the additional prices from the USD price and the conversion rates + * + * @asyncUnsafe */ private async $insertMissingAdditionalPrices(): Promise { this.lastFailedHistoricalRun = 0; diff --git a/backend/src/utils/axios-query.ts b/backend/src/utils/axios-query.ts index 3a92cd94d..7b01e4369 100644 --- a/backend/src/utils/axios-query.ts +++ b/backend/src/utils/axios-query.ts @@ -5,6 +5,7 @@ import config from '../config'; import logger from '../logger'; import * as https from 'https'; +/** @asyncUnsafe */ export async function query(path, throwOnFail: boolean = false): Promise { type axiosOptions = { headers: { diff --git a/backend/src/utils/p-limit.ts b/backend/src/utils/p-limit.ts index 20cead411..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,9 @@ export default function pLimit(concurrency: number): LimitFunction { const enqueue = (fn, resolve, args) => { queue.enqueue(run.bind(undefined, fn, resolve, args)); - (async () => { + ( + /** @asyncUnsafe */ + async () => { // This function needs to wait until the next microtask before comparing // `activeCount` to `concurrency`, because `activeCount` is updated asynchronously // when the run function is dequeued and called. The comparison in the if-statement @@ -153,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) =>