mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
Merge pull request #6045 from mempool/mononaut/stricter-async-linting
This commit is contained in:
commit
27ff011e77
65 changed files with 875 additions and 81 deletions
|
|
@ -1,2 +1,4 @@
|
|||
node_modules
|
||||
dist
|
||||
dist
|
||||
eslint-local-rules
|
||||
.eslintrc.js
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
{
|
||||
module.exports = {
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json",
|
||||
"tsconfigRootDir": __dirname
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
"@typescript-eslint",
|
||||
"local-rules"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
|
|
@ -10,6 +15,16 @@
|
|||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"ignorePatterns": ["dist", "eslint-local-rules", ".eslintrc.js", "testSetup*.ts", "jest.integration.*.ts", "__tests__", "*.config.ts"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["src/__integration_tests__/**/*"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"local-rules/no-unhandled-await": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-comment": 1,
|
||||
"@typescript-eslint/ban-types": 1,
|
||||
|
|
@ -21,6 +36,8 @@
|
|||
"@typescript-eslint/no-var-requires": 1,
|
||||
"@typescript-eslint/explicit-function-return-type": 1,
|
||||
"@typescript-eslint/no-unused-vars": 1,
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"local-rules/no-unhandled-await": "error",
|
||||
"no-console": 1,
|
||||
"no-constant-condition": 1,
|
||||
"no-dupe-else-if": 1,
|
||||
398
backend/eslint-local-rules/index.js
Normal file
398
backend/eslint-local-rules/index.js
Normal 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' });
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
42
backend/package-lock.json
generated
42
backend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 => ({
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -802,6 +802,7 @@ export class Common {
|
|||
return txs.map(Common.stripTransaction);
|
||||
}
|
||||
|
||||
/** @asyncSafe */
|
||||
static sleep$(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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...`);
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -407,6 +407,7 @@ class RbfCache {
|
|||
};
|
||||
}
|
||||
|
||||
/** @asyncSafe */
|
||||
public async load({ txs, trees, expiring, mempool, spendMap }): Promise<void> {
|
||||
try {
|
||||
txs.forEach(txEntry => {
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
`
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(`
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const PATH = './';
|
|||
class SyncAssets {
|
||||
constructor() { }
|
||||
|
||||
/** @asyncSafe */
|
||||
public async syncAssets$() {
|
||||
for (const url of config.MEMPOOL.EXTERNAL_ASSETS) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -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)];
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -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']) {
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue