Merge pull request #6045 from mempool/mononaut/stricter-async-linting

This commit is contained in:
wiz 2026-02-05 20:55:24 +09:00 committed by GitHub
commit 27ff011e77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 875 additions and 81 deletions

View file

@ -1,2 +1,4 @@
node_modules
dist
dist
eslint-local-rules
.eslintrc.js

View file

@ -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,

View file

@ -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' });
},
};
},
},
};

View file

@ -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",

View file

@ -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",

View file

@ -24,6 +24,7 @@ class AccelerationRoutes {
res.status(200).send(Object.values(accelerations));
}
/** @asyncUnsafe */
private async $getAcceleratorAcceleration(req: Request, res: Response): Promise<void> {
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<void> {
const history = await AccelerationRepository.$getAccelerationInfo(null, req.query.blockHeight ? parseInt(req.query.blockHeight as string, 10) : null);
res.status(200).send(history.map(accel => ({

View file

@ -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<void> {
try {
const networkInfo = await bitcoinClient.getNetworkInfo();

View file

@ -112,6 +112,7 @@ class BitcoinApi implements AbstractBitcoinApi {
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
}
/** @asyncUnsafe */
async $getTxsForBlock(hash: string, fallbackToCore = false): Promise<IEsploraApi.Transaction[]> {
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<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return {
@ -229,6 +231,7 @@ class BitcoinApi implements AbstractBitcoinApi {
};
}
/** @asyncUnsafe */
async $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
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<IEsploraApi.Outspend[][]> {
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<IEsploraApi.Outspend[]> {
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<IEsploraApi.Transaction> {
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<IEsploraApi.Transaction> {
let esploraTransaction: IEsploraApi.Transaction = {
txid: transaction.txid,
@ -367,6 +374,7 @@ class BitcoinApi implements AbstractBitcoinApi {
}
}
/** @asyncUnsafe */
private async $appendMempoolFeeData(transaction: IEsploraApi.Transaction): Promise<IEsploraApi.Transaction> {
if (transaction.fee) {
return transaction;
@ -384,6 +392,7 @@ class BitcoinApi implements AbstractBitcoinApi {
return transaction;
}
/** @asyncUnsafe */
protected async $addPrevouts(transaction: TransactionExtended): Promise<TransactionExtended> {
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<IEsploraApi.Transaction> {
if (transaction.vin[0].is_coinbase) {
transaction.fee = 0;

View file

@ -40,6 +40,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
});
}
/** @asyncUnsafe */
async $getAddress(address: string): Promise<IEsploraApi.Address> {
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<IEsploraApi.Transaction[]> {
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<IEsploraApi.UTXO[]> {
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<IEsploraApi.UTXO[]> {
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<IEsploraApi.MerkleProof> {
const tx = await this.$getRawTransaction(txId);
return this.electrumClient.blockchainTransaction_getMerkle(txId, tx.status.block_height);

View file

@ -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<void> {
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<string>('/blocks/tip/hash');
}
/** @asyncUnsafe */
async $getTxIdsForBlock(hash: string, fallbackToCore = false): Promise<string[]> {
try {
const txids = await this.failoverRouter.$get<string[]>('/block/' + hash + '/txids');
@ -462,6 +464,7 @@ class ElectrsApi implements AbstractBitcoinApi {
}
}
/** @asyncUnsafe */
async $getTxsForBlock(hash: string, fallbackToCore = false): Promise<IEsploraApi.Transaction[]> {
try {
const txs = await this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/block/' + hash + '/txs');
@ -555,6 +558,7 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$post<IEsploraApi.Outspend[]>('/internal/txs/outspends/by-outpoint', outpoints.map(out => `${out.txid}:${out.vout}`), 'json');
}
/** @asyncUnsafe */
async $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction> {
const txid = await this.failoverRouter.$get<string>(`/block/${blockhash}/txid/0`);
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid);

View file

@ -85,6 +85,8 @@ class Blocks {
* @param quiet - don't print non-essential logs
* @param addMempoolData - calculate sigops etc
* @returns Promise<TransactionExtended[]>
*
* @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<BlockExtended> {
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<PoolTag> {
if (txMinerInfo === undefined || txMinerInfo.vout.length < 1) {
@ -547,6 +553,7 @@ class Blocks {
}
}
/** @asyncUnsafe */
public async $indexBlockSummary(hash: string, height: number, stale?: boolean): Promise<void> {
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<void> {
const blockIds = await BlocksAuditsRepository.$getBlocksWithoutSummaries();
@ -650,6 +659,8 @@ class Blocks {
/**
* [INDEXING] Index transaction classification flags for Goggles
*
* @asyncSafe
*/
public async $classifyBlocks(): Promise<void> {
if (this.classifyingBlocks) {
@ -830,6 +841,7 @@ class Blocks {
/**
* [INDEXING] Index all blocks metadata for the mining dashboard
* @asyncSafe
*/
public async $generateBlockDatabase(): Promise<boolean> {
try {
@ -907,6 +919,7 @@ class Blocks {
return await BlocksRepository.$validateChain();
}
/** @asyncUnsafe */
public async $updateBlocks(): Promise<number> {
// 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<void> {
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<BlockExtended> {
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<void> {
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<BlockExtended> {
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<BlockExtended | IEsploraApi.Block> {
// 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<TransactionClassified[]>
{
@ -1358,6 +1381,7 @@ class Blocks {
return summary.transactions;
}
/** @asyncUnsafe */
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
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<BlockExtended[]> {
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<any> {
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<CpfpSummary | null> {
let transactions = txs;
if (!transactions) {
@ -1588,6 +1615,7 @@ class Blocks {
}
}
/** @asyncSafe */
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
try {
const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters);

View file

@ -39,6 +39,7 @@ class ChainTips {
private staleTipsCacheSize = 50;
private maxIndexingQueueSize = 100;
/** @asyncSafe */
public async updateOrphanedBlocks(): Promise<void> {
try {
this.chainTips = await bitcoinClient.getChainTips();
@ -135,6 +136,7 @@ class ChainTips {
}
}
/** @asyncSafe */
private async $indexOrphanedBlocks(): Promise<void> {
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;

View file

@ -802,6 +802,7 @@ export class Common {
return txs.map(Common.stripTransaction);
}
/** @asyncSafe */
static sleep$(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {

View file

@ -28,6 +28,7 @@ class DatabaseMigration {
/**
* Entry point
* @asyncUnsafe
*/
public async $initializeOrMigrateDatabase(): Promise<void> {
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<boolean> {
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<number> {
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<void> {
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<void> {
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<void> {
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<void> {
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);

View file

@ -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<void> {
if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) {
return;
@ -174,6 +175,7 @@ class DiskCache {
}
}
/** @asyncSafe */
async $loadMempoolCache(): Promise<void> {
if (!config.MEMPOOL.CACHE_ENABLED || !fs.existsSync(DiskCache.FILE_NAME)) {
return;

View file

@ -298,6 +298,7 @@ class ChannelsApi {
}
}
/** @asyncSafe */
public async $getChannelByClosingId(transactionId: string): Promise<any> {
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<void> {
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<void> {
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<void> {
if (!channel.chan_point?.length) {
@ -717,6 +721,7 @@ class ChannelsApi {
}
}
/** @asyncSafe */
public async $getLatestChannelUpdateForNode(publicKey: string): Promise<number> {
try {
const query = `

View file

@ -143,6 +143,7 @@ class NodesApi {
}
}
/** @asyncUnsafe */
public async $getActiveChannelsStats(node_public_key: string): Promise<unknown> {
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<void> {
try {
@ -727,6 +729,7 @@ class NodesApi {
/**
* Set all nodes not in `nodesPubkeys` as inactive (status = 0)
* @asyncSafe
*/
public async $setNodesInactive(graphNodesPubkeys: string[]): Promise<void> {
if (graphNodesPubkeys.length === 0) {

View file

@ -249,6 +249,7 @@ export default class CLightningClient extends EventEmitter implements AbstractLi
}));
}
/** @asyncUnsafe */
async $getNetworkGraph(): Promise<ILightningApi.NetworkGraph> {
const listnodes: any[] = await this.call('listnodes');
const listchannels: any[] = await this.call('listchannels');

View file

@ -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<ILightningApi.Channel[]> {
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<ILightningApi.Channel | null> {
const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0);
@ -238,6 +240,7 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILigh
/**
* Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format
* In this case, clightning knows the channel policy of only one node
* @asyncUnsafe
*/
async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel | null> {
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id);

View file

@ -40,6 +40,7 @@ class LndApi implements AbstractLightningApi {
.then((response) => response.data);
}
/** @asyncUnsafe */
async $getNetworkGraph(): Promise<ILightningApi.NetworkGraph> {
const graph = await axios.get<ILightningApi.NetworkGraph>(config.LND.REST_API_URL + '/v1/graph', this.axiosConfig)
.then((response) => response.data);

View file

@ -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<void> {
const query = `INSERT IGNORE INTO elements_pegs(
@ -102,12 +107,14 @@ class ElementsParser {
}
}
/** @asyncUnsafe */
protected async $getLatestBlockHeightFromDatabase(): Promise<number> {
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<any> {
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<any> {
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<any> {
const result = await bitcoinSecondClient.getBlockchainInfo();
return {
@ -339,12 +352,14 @@ class ElementsParser {
};
}
/** @asyncUnsafe */
protected async $getLastBlockAudit(): Promise<number> {
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<any[]> {
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<any> {
const lastBlockAudit = await this.$getLastBlockAudit();
const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState();
@ -368,12 +384,14 @@ class ElementsParser {
};
}
/** @asyncUnsafe */
public async $getPegDataByMonth(): Promise<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
const [rows] = await DB.query(`SELECT COUNT(*) AS pegs_count FROM elements_pegs;`);
return rows[0];

View file

@ -45,6 +45,7 @@ class MempoolBlocks {
return this.mempoolBlockDeltas;
}
/** @asyncUnsafe */
public async updatePools$(): Promise<void> {
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<MempoolBlockWithTransactions[]> {
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<void> {
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<MempoolBlockWithTransactions[]> {
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<MempoolBlockWithTransactions[]> {
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<MempoolBlockWithTransactions[]> {
// GBT optimization requires that uids never get too sparse
// as a sanity check, we should also explicitly prevent uint32 uid overflow

View file

@ -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<string, Acceleration> | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
logger.debug(`Updating mempool...`);

View file

@ -326,6 +326,7 @@ class Mining {
/**
* Generate daily hashrate data
* @asyncUnsafe
*/
public async $generateNetworkHashrateHistory(): Promise<void> {
// If a re-index was requested, truncate first
@ -439,6 +440,7 @@ class Mining {
/**
* Index difficulty adjustments
* @asyncUnsafe
*/
public async $indexDifficultyAdjustments(): Promise<void> {
// 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<void> {
if (this.blocksPriceIndexingRunning === true) {
@ -598,6 +602,8 @@ class Mining {
/**
* Index core coinstatsindex
*
* @asyncUnsafe
*/
public async $indexCoinStatsIndex(): Promise<void> {
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));

View file

@ -33,6 +33,7 @@ class PoolsParser {
/**
* Populate our db with updated mining pool definition
* @param pools
* @asyncUnsafe
*/
public async migratePoolsJson(): Promise<void> {
// 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<void> {
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<void> {
let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52

View file

@ -407,6 +407,7 @@ class RbfCache {
};
}
/** @asyncSafe */
public async load({ txs, trees, expiring, mempool, spendMap }): Promise<void> {
try {
txs.forEach(txEntry => {

View file

@ -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<boolean> {
if (!this.connected && config.REDIS.ENABLED) {
try {
@ -95,6 +96,7 @@ class RedisCache {
await this.$flushRbfQueues();
}
/** @asyncSafe */
async $updateBlocks(blocks: BlockExtended[]): Promise<void> {
if (!config.REDIS.ENABLED) {
return;
@ -127,6 +129,7 @@ class RedisCache {
}
}
/** @asyncSafe */
async $addTransaction(tx: MempoolTransactionExtended): Promise<void> {
if (!config.REDIS.ENABLED) {
return;
@ -139,6 +142,7 @@ class RedisCache {
}
}
/** @asyncSafe */
async $flushTransactions(): Promise<void> {
if (!config.REDIS.ENABLED) {
return;
@ -178,6 +182,7 @@ class RedisCache {
}
}
/** @asyncSafe */
async $removeTransactions(transactions: string[]): Promise<void> {
if (!config.REDIS.ENABLED) {
return;
@ -206,6 +211,7 @@ class RedisCache {
}
}
/** @asyncSafe */
async $setRbfEntry(type: string, txid: string, value: any): Promise<void> {
if (!config.REDIS.ENABLED) {
return;
@ -222,6 +228,7 @@ class RedisCache {
}
}
/** @asyncSafe */
async $removeRbfEntry(type: string, txid: string): Promise<void> {
if (!config.REDIS.ENABLED) {
return;
@ -238,6 +245,7 @@ class RedisCache {
}
}
/** @asyncSafe */
private async $flushRbfQueues(): Promise<void> {
if (!config.REDIS.ENABLED) {
return;
@ -263,6 +271,7 @@ class RedisCache {
}
}
/** @asyncSafe */
async $getBlocks(): Promise<BlockExtended[]> {
if (!config.REDIS.ENABLED) {
return [];
@ -280,6 +289,7 @@ class RedisCache {
}
}
/** @asyncSafe */
async $getBlockSummaries(): Promise<BlockSummary[]> {
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<any[]> {
if (!config.REDIS.ENABLED) {
return [];
@ -337,6 +349,7 @@ class RedisCache {
}
}
/** @asyncUnsafe */
async $loadCache(): Promise<void> {
if (!config.REDIS.ENABLED) {
return;
@ -385,12 +398,14 @@ class RedisCache {
}
}
/** @asyncUnsafe */
private async scanKeys<T>(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<void> => {
const values = await this.client.MGET(keys);
for (let i = 0; i < values.length; i++) {

View file

@ -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<Acceleration[] | null> {
try {
const response = await axios.get(this.apiPath, { responseType: 'json', timeout: 10000 });
@ -238,6 +240,7 @@ class AccelerationApi {
}
}
/** @asyncSafe */
public async connectWebsocket(): Promise<void> {
if (this.startedWebsocketLoop) {
return;
@ -314,7 +317,7 @@ class AccelerationApi {
}
}
}
await new Promise(resolve => setTimeout(resolve, 5000));
await Common.sleep$(5000);
}
}
}

View file

@ -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<void> {
if (!config.STRATUM.ENABLED) {
return;
@ -97,7 +99,7 @@ class StratumApi {
}
});
}
await new Promise(resolve => setTimeout(resolve, 5000));
await Common.sleep$(5000);
}
}
}

View file

@ -56,10 +56,11 @@ class WalletApi {
// Load cache on startup
if (config.WALLETS.ENABLED) {
this.$loadCache();
void this.$loadCache();
}
}
/** @asyncSafe */
private async $loadCache(): Promise<void> {
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<void> {
if (!config.WALLETS.ENABLED || this.syncing) {
return;

View file

@ -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<void> {
if (!memPool.isInSync()) {
return;

View file

@ -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<TransactionExtended> {
let transaction: IEsploraApi.Transaction;
@ -69,10 +70,12 @@ class TransactionUtils {
}
}
/** @asyncUnsafe */
public async $getMempoolTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended> {
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
}
/** @asyncUnsafe */
public async $getMempoolTransactionsExtended(txids: string[], addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended[]> {
if (forceCore || config.MEMPOOL.BACKEND !== 'esplora') {
const limiter = pLimit(8); // Run 8 requests at a time

View file

@ -1003,6 +1003,7 @@ class WebsocketHandler {
}
}
/** @asyncUnsafe */
async handleNewBlock(block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]): Promise<void> {
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<MempoolTransactionExtended[]> {
for (let i = 0; i < transactions.length; i++) {
try {

View file

@ -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<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(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<void> {
try {
await connection.rollback();
@ -85,6 +88,7 @@ import { execSync } from 'child_process';
}
}
/** @asyncSafe */
public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(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<Pool> {
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<void> {
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');
}

View file

@ -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<void> {
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<void> {
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<void> {
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();
}
}

View file

@ -35,6 +35,8 @@ class Indexer {
/**
* Check which core index is available for indexing
*
* @asyncUnsafe
*/
public async checkAvailableCoreIndexes(): Promise<void> {
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<void> {
if (!Common.indexingEnabled() || this.tasksRunning[task]) {
@ -163,6 +167,7 @@ class Indexer {
this.tasksRunning[task] = false;
}
/** @asyncSafe */
public async $run(): Promise<void> {
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;

View file

@ -17,6 +17,7 @@ class AuditReplication {
inProgress: boolean = false;
skip: Set<string> = new Set();
/** @asyncUnsafe */
public async $sync(): Promise<void> {
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<boolean> {
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<string[]> {
try {
const startHeight = config.REPLICATION.AUDIT_START_HEIGHT || 0;

View file

@ -31,6 +31,7 @@ const steps = {
class StatisticsReplication {
inProgress: boolean = false;
/** @asyncUnsafe */
public async $sync(): Promise<void> {
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<number>): Promise<any> {
let success = false;
@ -105,6 +107,8 @@ class StatisticsReplication {
return { success, synced, missed: missed.size };
}
/** @asyncUnsafe */
private async $getMissingStatistics(): Promise<MissingStatistics> {
try {
const now = Math.floor(Date.now() / 1000);
@ -146,6 +150,7 @@ class StatisticsReplication {
}
}
/** @asyncUnsafe */
private async $getMissingStatisticsInterval(interval: any, startTime: number): Promise<Set<number>> {
try {
const start = interval[0];

View file

@ -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<object> {
type axiosOptions = {
headers: {

View file

@ -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<void> {
const accelerationMap: { [txid: string]: Acceleration } = {};
for (const acc of accelerationData) {
@ -60,28 +61,34 @@ class AccelerationRepository {
}
}
/** @asyncSafe */
public async $getAccelerationInfoForTxid(txid: string): Promise<PublicAcceleration | null> {
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<number> {
try {
const [rows] = await DB.query(`
@ -206,6 +214,7 @@ class AccelerationRepository {
return 0;
}
/** @asyncSafe */
private async $setLastSyncedHeight(height: number): Promise<void> {
try {
await DB.query(`
@ -219,6 +228,7 @@ class AccelerationRepository {
}
// modifies block transactions
/** @asyncSafe */
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
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;

View file

@ -15,6 +15,7 @@ interface MigrationAudit {
}
class BlocksAuditRepositories {
/** @asyncSafe */
public async $saveAudit(audit: BlockAudit): Promise<void> {
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<any> {
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<number> {
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<BlockAudit | null> {
try {
const [rows]: any[] = await DB.query(
@ -115,6 +120,7 @@ class BlocksAuditRepositories {
}
}
/** @asyncSafe */
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
try {
const blockAudit = await this.$getBlockAudit(hash);
@ -151,6 +157,7 @@ class BlocksAuditRepositories {
}
}
/** @asyncSafe */
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
try {
const [rows]: any[] = await DB.query(
@ -165,6 +172,7 @@ class BlocksAuditRepositories {
}
}
/** @asyncSafe */
public async $getBlockAuditScores(maxHeight: number, minHeight: number): Promise<AuditScore[]> {
try {
const [rows]: any[] = await DB.query(
@ -179,6 +187,7 @@ class BlocksAuditRepositories {
}
}
/** @asyncSafe */
public async $getBlocksWithoutSummaries(): Promise<string[]> {
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<void> {
try {

View file

@ -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<void> {
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<number[]> {
// 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<any> {
interval = Common.getSqlInterval(interval);
@ -334,6 +339,7 @@ class BlocksRepository {
/**
* Return most recent block height
* @asyncSafe
*/
public async $mostRecentBlockHeight(): Promise<number> {
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<number> {
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<number> {
const params: any[] = [];
@ -407,6 +415,7 @@ class BlocksRepository {
/**
* Get blocks count for a period
* @asyncSafe
*/
public async $blockCountBetweenHeight(startHeight: number, endHeight: number): Promise<number> {
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<number | null> {
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<number> {
const params: any[] = [];
@ -474,6 +485,7 @@ class BlocksRepository {
/**
* Get the oldest indexed block
* @asyncSafe
*/
public async $oldestBlockTimestamp(): Promise<number> {
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<BlockExtended[]> {
const pool = await PoolsRepository.$getPool(slug);
@ -538,6 +551,7 @@ class BlocksRepository {
/**
* Get one block by height
* @asyncSafe
*/
public async $getBlockByHeight(height: number): Promise<BlockExtended | null> {
try {
@ -586,6 +600,7 @@ class BlocksRepository {
/**
* Return blocks difficulty
* @asyncSafe
*/
public async $getBlocksDifficulty(): Promise<object[]> {
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<any> {
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<boolean> {
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<any> {
try {
@ -769,6 +788,7 @@ class BlocksRepository {
/**
* Get the historical averaged block rewards
* @asyncSafe
*/
public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise<any> {
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<any> {
try {
@ -831,6 +852,7 @@ class BlocksRepository {
/**
* Get the historical averaged block sizes
* @asyncSafe
*/
public async $getHistoricalBlockSizes(div: number, interval: string | null): Promise<any> {
try {
@ -857,6 +879,7 @@ class BlocksRepository {
/**
* Get the historical averaged block weights
* @asyncSafe
*/
public async $getHistoricalBlockWeights(div: number, interval: string | null): Promise<any> {
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<number[]> {
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<any> {
try {
@ -948,6 +974,7 @@ class BlocksRepository {
/**
* Get all blocks which have not be linked to a price yet
* @asyncSafe
*/
public async $getBlocksWithoutPrice(): Promise<object[]> {
try {
@ -969,6 +996,7 @@ class BlocksRepository {
/**
* Save block price by batch
* @asyncSafe
*/
public async $saveBlockPrices(blockPrices: BlockPrice[]): Promise<void> {
try {
@ -990,6 +1018,7 @@ class BlocksRepository {
/**
* Get all indexed blocsk with missing coinstatsindex data
* @asyncSafe
*/
public async $getBlocksMissingCoinStatsIndex(maxHeight: number, minHeight: number): Promise<any> {
try {
@ -1009,6 +1038,7 @@ class BlocksRepository {
/**
* Get all indexed blocks with missing coinbase addresses
* (includes stale blocks)
* @asyncSafe
*/
public async $getBlocksWithoutCoinbaseAddresses(): Promise<any> {
try {
@ -1031,6 +1061,7 @@ class BlocksRepository {
*
* @param id
* @param feePercentiles
* @asyncSafe
*/
public async $saveFeePercentilesForBlockId(id: string, feePercentiles: number[]): Promise<void> {
try {
@ -1050,6 +1081,7 @@ class BlocksRepository {
*
* @param id
* @param feeStats
* @asyncSafe
*/
public async $saveEffectiveFeeStats(id: string, feeStats: EffectiveFeeStats): Promise<void> {
try {
@ -1069,6 +1101,7 @@ class BlocksRepository {
*
* @param id
* @param addresses
* @asyncSafe
*/
public async $saveCoinbaseAddresses(id: string, addresses: string[]): Promise<void> {
try {
@ -1088,6 +1121,7 @@ class BlocksRepository {
*
* @param id
* @param poolId
* @asyncSafe
*/
public async $savePool(id: string, poolId: number): Promise<void> {
try {
@ -1106,6 +1140,7 @@ class BlocksRepository {
* Save block first seen time
*
* @param id
* @asyncSafe
*/
public async $saveFirstSeenTime(id: string, firstSeen: number): Promise<void> {
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<BlockExtended> {
const blk: Partial<BlockExtended> = {};
@ -1272,6 +1308,7 @@ class BlocksRepository {
}
// migration to fix median fee bug
/** @asyncSafe */
private async $migrateBlocksToV1(): Promise<number> {
let blocksMigrated = 0;
try {

View file

@ -5,6 +5,7 @@ import logger from '../logger';
import { BlockSummary, TransactionClassified } from '../mempool.interfaces';
class BlocksSummariesRepository {
/** @asyncSafe */
public async $getByBlockId(id: string): Promise<BlockSummary | undefined> {
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<void> {
try {
const transactionsStr = JSON.stringify(transactions);
@ -33,6 +35,7 @@ class BlocksSummariesRepository {
}
}
/** @asyncSafe */
public async $saveTemplate(params: { height: number, template: BlockSummary, version: number}): Promise<void> {
const blockId = params.template?.id;
try {
@ -53,6 +56,7 @@ class BlocksSummariesRepository {
}
}
/** @asyncSafe */
public async $getTemplate(id: string): Promise<BlockSummary | undefined> {
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<string[]> {
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<number[] | null> {
try {

View file

@ -73,6 +73,8 @@ class CpfpRepository {
}
}
/** @asyncUnsafe */
public async $getCluster(clusterRoot: string): Promise<CpfpCluster | void> {
const [clusterRows]: any = await DB.query(
`
@ -91,6 +93,7 @@ class CpfpRepository {
return;
}
/** @asyncUnsafe */
public async $getClustersAt(height: number): Promise<CpfpCluster[]> {
const [clusterRows]: any = await DB.query(
`

View file

@ -9,6 +9,7 @@ export interface NodeRecord {
}
class NodesRecordsRepository {
/** @asyncSafe */
public async $saveRecord(record: NodeRecord): Promise<void> {
try {
const payloadBytes = Buffer.from(record.payload, 'base64');
@ -26,6 +27,7 @@ class NodesRecordsRepository {
}
}
/** @asyncSafe */
public async $getRecordTypes(publicKey: string): Promise<any> {
try {
const query = `
@ -40,6 +42,7 @@ class NodesRecordsRepository {
}
}
/** @asyncSafe */
public async $deleteUnusedRecords(publicKey: string, recordTypes: number[]): Promise<number> {
try {
let query;

View file

@ -9,6 +9,7 @@ export interface NodeSocket {
}
class NodesSocketsRepository {
/** @asyncSafe */
public async $saveSocket(socket: NodeSocket): Promise<void> {
try {
await DB.query(`
@ -23,6 +24,7 @@ class NodesSocketsRepository {
}
}
/** @asyncSafe */
public async $deleteUnusedSockets(publicKey: string, addresses: string[]): Promise<number> {
if (addresses.length === 0) {
return 0;

View file

@ -8,6 +8,7 @@ import { PoolInfo, PoolTag } from '../mempool.interfaces';
class PoolsRepository {
/**
* Get all pools tagging info
* @asyncUnsafe
*/
public async $getPools(): Promise<PoolTag[]> {
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<PoolTag> {
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<PoolInfo[]> {
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<PoolInfo[]> {
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<PoolTag | null> {
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<PoolTag | null> {
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<void> {
try {
@ -170,6 +177,7 @@ class PoolsRepository {
* @param dbId
* @param newSlug
* @param newName
* @asyncSafe
*/
public async $renameMiningPool(dbId: number, newSlug: string, newName: string): Promise<void> {
try {
@ -189,6 +197,7 @@ class PoolsRepository {
*
* @param dbId
* @param newLink
* @asyncSafe
*/
public async $updateMiningPoolLink(dbId: number, newLink: string): Promise<void> {
try {
@ -210,6 +219,7 @@ class PoolsRepository {
* @param dbId
* @param addresses
* @param regexes
* @asyncSafe
*/
public async $updateMiningPoolTags(dbId: number, addresses: string, regexes: string): Promise<void> {
try {

View file

@ -224,6 +224,7 @@ class PricesRepository {
}
}
/** @asyncUnsafe */
public async $getOldestPriceTime(): Promise<number> {
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<number | null> {
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<number> {
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<number[]> {
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<ApiPrice> {
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<Conversion | null> {
try {
const [rates] = await DB.query(`
@ -428,6 +436,7 @@ class PricesRepository {
}
}
/** @asyncSafe */
public async $getHistoricalPrices(currency?: string): Promise<Conversion | null> {
try {
const [rates] = await DB.query(`

View file

@ -10,6 +10,7 @@ const PATH = './';
class SyncAssets {
constructor() { }
/** @asyncSafe */
public async syncAssets$() {
for (const url of config.MEMPOOL.EXTERNAL_ASSETS) {
try {

View file

@ -23,6 +23,7 @@ class ForensicsService {
await this.$runTasks();
}
/** @asyncSafe */
private async $runTasks(): Promise<void> {
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<IEsploraApi.Transaction | null> {
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<string>(txids)];

View file

@ -29,6 +29,7 @@ class NetworkSyncService {
await this.$runTasks();
}
/** @asyncSafe */
private async $runTasks(): Promise<void> {
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}`);
});
}
}

View file

@ -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<void> {
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<void> {
try {

View file

@ -28,6 +28,7 @@ class FundingTxFetcher {
}
}
/** @asyncUnsafe */
async $fetchChannelsFundingTxs(channelIds: string[]): Promise<void> {
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);

View file

@ -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<void> {
let loggerTimer = new Date().getTime() / 1000;
let progress = 0;

View file

@ -14,6 +14,7 @@ const fsPromises = promises;
class LightningStatsImporter {
topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER;
/** @asyncSafe */
async $run(): Promise<void> {
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<unknown> {

View file

@ -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<void> {
while ('Bitcoin is still alive') {
try {
@ -30,6 +31,7 @@ class PoolsUpdater {
}
}
/** @asyncSafe */
public async updatePoolsJson(): Promise<void> {
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<string | null> {
const response = await this.query(this.treeUrl);
@ -152,6 +155,7 @@ class PoolsUpdater {
/**
* Http request wrapper
* @asyncUnsafe
*/
private async query(path): Promise<any[] | undefined> {
type axiosOptions = {

View file

@ -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<number> {
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<PriceHistory> {
const priceHistory: PriceHistory = {};

View file

@ -11,6 +11,7 @@ class BitflyerApi implements PriceFeed {
constructor() {
}
/** @asyncUnsafe */
public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency);
if (response && response['ltp']) {

View file

@ -11,6 +11,7 @@ class CoinbaseApi implements PriceFeed {
constructor() {
}
/** @asyncUnsafe */
public async $fetchPrice(currency): Promise<number> {
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<PriceHistory> {
const priceHistory: PriceHistory = {};

View file

@ -56,6 +56,7 @@ class FreeCurrencyApi implements ConversionFeed {
constructor() { }
/** @asyncUnsafe */
public async $getQuota(): Promise<any> {
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<ConversionRates> {
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<ConversionRates> {
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)) {

View file

@ -11,6 +11,7 @@ class GeminiApi implements PriceFeed {
constructor() {
}
/** @asyncUnsafe */
public async $fetchPrice(currency): Promise<number> {
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<PriceHistory> {
const priceHistory: PriceHistory = {};

View file

@ -21,6 +21,7 @@ class KrakenApi implements PriceFeed {
return ticker;
}
/** @asyncUnsafe */
public async $fetchPrice(currency): Promise<number> {
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<PriceHistory> {
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<void> {
const existingPriceTimes = await PricesRepository.$getPricesTimes();

View file

@ -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<void> {
this.latestPrices = await PricesRepository.$getLatestConversionRates();
this.latestGoodPrices = JSON.parse(JSON.stringify(this.latestPrices));
}
/** @asyncSafe */
public async $run(): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
this.lastFailedHistoricalRun = 0;

View file

@ -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<object | undefined> {
type axiosOptions = {
headers: {

View file

@ -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) =>