diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..001ea3cb3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/backend" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: npm + directory: "/frontend" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: docker + directory: "/docker/backend" + schedule: + interval: weekly +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index a7a9929d9..da30f0641 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -11,6 +11,9 @@ on: - v[0-9]+.[0-9]+.[0-9]+ - v[0-9]+.[0-9]+.[0-9]+-* +permissions: + contents: read + jobs: build: strategy: @@ -35,24 +38,24 @@ jobs: run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - name: Checkout project - uses: actions/checkout@v2 + uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 - name: Init repo for Dockerization run: docker/init.sh "$TAG" - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480 # v1 id: qemu - name: Setup Docker buildx action - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 # v1 id: buildx - name: Available platforms run: echo ${{ steps.buildx.outputs.platforms }} - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@661fd3eb7f2f20d8c7c84bc2b0509efd7a826628 # v2 id: cache with: path: /tmp/.buildx-cache diff --git a/backend/package-lock.json b/backend/package-lock.json index e427bd8ab..33ef22b07 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,7 +15,6 @@ "bitcoinjs-lib": "6.0.1", "crypto-js": "^4.0.0", "express": "^4.17.1", - "locutus": "^2.0.12", "mysql2": "2.3.3", "node-worker-threads-pool": "^1.4.3", "socks-proxy-agent": "^6.1.1", @@ -25,7 +24,6 @@ "devDependencies": { "@types/compression": "^1.0.1", "@types/express": "^4.17.2", - "@types/locutus": "^0.0.6", "tslint": "^6.1.0" } }, @@ -114,12 +112,6 @@ "@types/range-parser": "*" } }, - "node_modules/@types/locutus": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/locutus/-/locutus-0.0.6.tgz", - "integrity": "sha512-P+BQds4wrJhqKiIOBWAYpbsE9UOztnnqW9zHk4Bci7kCXjEQAA7FJrD9HX5JU2Z36fhE2WDctuuIpLvqDsciWQ==", - "dev": true - }, "node_modules/@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", @@ -601,9 +593,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", - "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", "funding": [ { "type": "individual", @@ -804,14 +796,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/locutus": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.15.tgz", - "integrity": "sha512-2xWC4RkoAoCVXEb/stzEgG1TNgd+mrkLBj6TuEDNyUoKeQ2XzDTyJUC23sMiqbL6zJmJSP3w59OZo+zc4IBOmA==", - "engines": { - "node": ">= 10" - } - }, "node_modules/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -902,9 +886,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "node_modules/mkdirp": { @@ -1562,12 +1546,6 @@ "@types/range-parser": "*" } }, - "@types/locutus": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/locutus/-/locutus-0.0.6.tgz", - "integrity": "sha512-P+BQds4wrJhqKiIOBWAYpbsE9UOztnnqW9zHk4Bci7kCXjEQAA7FJrD9HX5JU2Z36fhE2WDctuuIpLvqDsciWQ==", - "dev": true - }, "@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", @@ -1980,9 +1958,9 @@ } }, "follow-redirects": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", - "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" }, "forwarded": { "version": "0.1.2", @@ -2135,11 +2113,6 @@ "esprima": "^4.0.0" } }, - "locutus": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.15.tgz", - "integrity": "sha512-2xWC4RkoAoCVXEb/stzEgG1TNgd+mrkLBj6TuEDNyUoKeQ2XzDTyJUC23sMiqbL6zJmJSP3w59OZo+zc4IBOmA==" - }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -2206,9 +2179,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "mkdirp": { diff --git a/backend/package.json b/backend/package.json index 472b1af0b..31bec5677 100644 --- a/backend/package.json +++ b/backend/package.json @@ -34,7 +34,6 @@ "bitcoinjs-lib": "6.0.1", "crypto-js": "^4.0.0", "express": "^4.17.1", - "locutus": "^2.0.12", "mysql2": "2.3.3", "node-worker-threads-pool": "^1.4.3", "socks-proxy-agent": "^6.1.1", @@ -44,7 +43,6 @@ "devDependencies": { "@types/compression": "^1.0.1", "@types/express": "^4.17.2", - "@types/locutus": "^0.0.6", "tslint": "^6.1.0" } } diff --git a/backend/src/api/backend-info.ts b/backend/src/api/backend-info.ts index 5a556ef18..d98675671 100644 --- a/backend/src/api/backend-info.ts +++ b/backend/src/api/backend-info.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as os from 'os'; import logger from '../logger'; import { IBackendInfo } from '../mempool.interfaces'; +const { spawnSync } = require('child_process'); class BackendInfo { private gitCommitHash = ''; @@ -27,10 +28,23 @@ class BackendInfo { } private setLatestCommitHash(): void { - try { - this.gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim(); - } catch (e) { - logger.err('Could not load git commit info: ' + (e instanceof Error ? e.message : e)); + //TODO: share this logic with `generate-config.js` + if (process.env.DOCKER_COMMIT_HASH) { + this.gitCommitHash = process.env.DOCKER_COMMIT_HASH; + } else { + try { + const gitRevParse = spawnSync('git', ['rev-parse', '--short', 'HEAD']); + if (!gitRevParse.error) { + const output = gitRevParse.stdout.toString('utf-8').replace(/[\n\r\s]+$/, ''); + this.gitCommitHash = output ? output : '?'; + } else if (gitRevParse.error.code === 'ENOENT') { + console.log('git not found, cannot parse git hash'); + this.gitCommitHash = '?'; + } + } catch (e: any) { + console.log('Could not load git commit info: ' + e.message); + this.gitCommitHash = '?'; + } } } diff --git a/backend/src/api/bisq/markets-api.ts b/backend/src/api/bisq/markets-api.ts index 558c390a0..54e0297b7 100644 --- a/backend/src/api/bisq/markets-api.ts +++ b/backend/src/api/bisq/markets-api.ts @@ -1,7 +1,7 @@ import { Currencies, OffersData, TradesData, Depth, Currency, Interval, HighLowOpenClose, Markets, Offers, Offer, BisqTrade, MarketVolume, Tickers, Ticker, SummarizedIntervals, SummarizedInterval } from './interfaces'; -import * as datetime from 'locutus/php/datetime'; +const strtotime = require('./strtotime'); class BisqMarketsApi { private cryptoCurrencyData: Currency[] = []; @@ -312,7 +312,7 @@ class BisqMarketsApi { getTickerFromMarket(market: string): Ticker | null { let ticker: Ticker; - const timestamp_from = datetime.strtotime('-24 hour'); + const timestamp_from = strtotime('-24 hour'); const timestamp_to = new Date().getTime() / 1000; const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from, undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER); @@ -638,13 +638,13 @@ class BisqMarketsApi { case 'half_day': return (ts - (ts % (3600 * 12))); case 'day': - return datetime.strtotime('midnight today', ts); + return strtotime('midnight today', ts); case 'week': - return datetime.strtotime('midnight sunday last week', ts); + return strtotime('midnight sunday last week', ts); case 'month': - return datetime.strtotime('midnight first day of this month', ts); + return strtotime('midnight first day of this month', ts); case 'year': - return datetime.strtotime('midnight first day of january', ts); + return strtotime('midnight first day of january', ts); default: throw new Error('Unsupported interval: ' + interval); } diff --git a/backend/src/api/bisq/strtotime.ts b/backend/src/api/bisq/strtotime.ts new file mode 100644 index 000000000..912f00ec9 --- /dev/null +++ b/backend/src/api/bisq/strtotime.ts @@ -0,0 +1,1375 @@ +// @ts-nocheck + +/* +Copyright (c) 2007-2016 Kevin van Zonneveld (https://kvz.io) +and Contributors (https://locutus.io/authors) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* + +https://github.com/locutusjs/locutus/blob/master/src/php/datetime/strtotime.js + +*/ + +const reSpace = '[ \\t]+' +const reSpaceOpt = '[ \\t]*' +const reMeridian = '(?:([ap])\\.?m\\.?([\\t ]|$))' +const reHour24 = '(2[0-4]|[01]?[0-9])' +const reHour24lz = '([01][0-9]|2[0-4])' +const reHour12 = '(0?[1-9]|1[0-2])' +const reMinute = '([0-5]?[0-9])' +const reMinutelz = '([0-5][0-9])' +const reSecond = '(60|[0-5]?[0-9])' +const reSecondlz = '(60|[0-5][0-9])' +const reFrac = '(?:\\.([0-9]+))' + +const reDayfull = 'sunday|monday|tuesday|wednesday|thursday|friday|saturday' +const reDayabbr = 'sun|mon|tue|wed|thu|fri|sat' +const reDaytext = reDayfull + '|' + reDayabbr + '|weekdays?' + +const reReltextnumber = 'first|second|third|fourth|fifth|sixth|seventh|eighth?|ninth|tenth|eleventh|twelfth' +const reReltexttext = 'next|last|previous|this' +const reReltextunit = '(?:second|sec|minute|min|hour|day|fortnight|forthnight|month|year)s?|weeks|' + reDaytext + +const reYear = '([0-9]{1,4})' +const reYear2 = '([0-9]{2})' +const reYear4 = '([0-9]{4})' +const reYear4withSign = '([+-]?[0-9]{4})' +const reMonth = '(1[0-2]|0?[0-9])' +const reMonthlz = '(0[0-9]|1[0-2])' +const reDay = '(?:(3[01]|[0-2]?[0-9])(?:st|nd|rd|th)?)' +const reDaylz = '(0[0-9]|[1-2][0-9]|3[01])' + +const reMonthFull = 'january|february|march|april|may|june|july|august|september|october|november|december' +const reMonthAbbr = 'jan|feb|mar|apr|may|jun|jul|aug|sept?|oct|nov|dec' +const reMonthroman = 'i[vx]|vi{0,3}|xi{0,2}|i{1,3}' +const reMonthText = '(' + reMonthFull + '|' + reMonthAbbr + '|' + reMonthroman + ')' + +const reTzCorrection = '((?:GMT)?([+-])' + reHour24 + ':?' + reMinute + '?)' +const reTzAbbr = '\\(?([a-zA-Z]{1,6})\\)?' +const reDayOfYear = '(00[1-9]|0[1-9][0-9]|[12][0-9][0-9]|3[0-5][0-9]|36[0-6])' +const reWeekOfYear = '(0[1-9]|[1-4][0-9]|5[0-3])' + +const reDateNoYear = reMonthText + '[ .\\t-]*' + reDay + '[,.stndrh\\t ]*' + +function processMeridian (hour, meridian) { + meridian = meridian && meridian.toLowerCase() + + switch (meridian) { + case 'a': + hour += hour === 12 ? -12 : 0 + break + case 'p': + hour += hour !== 12 ? 12 : 0 + break + } + + return hour +} + +function processYear (yearStr) { + let year = +yearStr + + if (yearStr.length < 4 && year < 100) { + year += year < 70 ? 2000 : 1900 + } + + return year +} + +function lookupMonth (monthStr) { + return { + jan: 0, + january: 0, + i: 0, + feb: 1, + february: 1, + ii: 1, + mar: 2, + march: 2, + iii: 2, + apr: 3, + april: 3, + iv: 3, + may: 4, + v: 4, + jun: 5, + june: 5, + vi: 5, + jul: 6, + july: 6, + vii: 6, + aug: 7, + august: 7, + viii: 7, + sep: 8, + sept: 8, + september: 8, + ix: 8, + oct: 9, + october: 9, + x: 9, + nov: 10, + november: 10, + xi: 10, + dec: 11, + december: 11, + xii: 11 + }[monthStr.toLowerCase()] +} + +function lookupWeekday (dayStr, desiredSundayNumber = 0) { + const dayNumbers = { + mon: 1, + monday: 1, + tue: 2, + tuesday: 2, + wed: 3, + wednesday: 3, + thu: 4, + thursday: 4, + fri: 5, + friday: 5, + sat: 6, + saturday: 6, + sun: 0, + sunday: 0 + } + + return dayNumbers[dayStr.toLowerCase()] || desiredSundayNumber +} + +function lookupRelative (relText) { + const relativeNumbers = { + last: -1, + previous: -1, + this: 0, + first: 1, + next: 1, + second: 2, + third: 3, + fourth: 4, + fifth: 5, + sixth: 6, + seventh: 7, + eight: 8, + eighth: 8, + ninth: 9, + tenth: 10, + eleventh: 11, + twelfth: 12 + } + + const relativeBehavior = { + this: 1 + } + + const relTextLower = relText.toLowerCase() + + return { + amount: relativeNumbers[relTextLower], + behavior: relativeBehavior[relTextLower] || 0 + } +} + +function processTzCorrection (tzOffset, oldValue) { + const reTzCorrectionLoose = /(?:GMT)?([+-])(\d+)(:?)(\d{0,2})/i + tzOffset = tzOffset && tzOffset.match(reTzCorrectionLoose) + + if (!tzOffset) { + return oldValue + } + + const sign = tzOffset[1] === '-' ? -1 : 1 + let hours = +tzOffset[2] + let minutes = +tzOffset[4] + + if (!tzOffset[4] && !tzOffset[3]) { + minutes = Math.floor(hours % 100) + hours = Math.floor(hours / 100) + } + + // timezone offset in seconds + return sign * (hours * 60 + minutes) * 60 +} + +// tz abbrevation : tz offset in seconds +const tzAbbrOffsets = { + acdt: 37800, + acst: 34200, + addt: -7200, + adt: -10800, + aedt: 39600, + aest: 36000, + ahdt: -32400, + ahst: -36000, + akdt: -28800, + akst: -32400, + amt: -13840, + apt: -10800, + ast: -14400, + awdt: 32400, + awst: 28800, + awt: -10800, + bdst: 7200, + bdt: -36000, + bmt: -14309, + bst: 3600, + cast: 34200, + cat: 7200, + cddt: -14400, + cdt: -18000, + cemt: 10800, + cest: 7200, + cet: 3600, + cmt: -15408, + cpt: -18000, + cst: -21600, + cwt: -18000, + chst: 36000, + dmt: -1521, + eat: 10800, + eddt: -10800, + edt: -14400, + eest: 10800, + eet: 7200, + emt: -26248, + ept: -14400, + est: -18000, + ewt: -14400, + ffmt: -14660, + fmt: -4056, + gdt: 39600, + gmt: 0, + gst: 36000, + hdt: -34200, + hkst: 32400, + hkt: 28800, + hmt: -19776, + hpt: -34200, + hst: -36000, + hwt: -34200, + iddt: 14400, + idt: 10800, + imt: 25025, + ist: 7200, + jdt: 36000, + jmt: 8440, + jst: 32400, + kdt: 36000, + kmt: 5736, + kst: 30600, + lst: 9394, + mddt: -18000, + mdst: 16279, + mdt: -21600, + mest: 7200, + met: 3600, + mmt: 9017, + mpt: -21600, + msd: 14400, + msk: 10800, + mst: -25200, + mwt: -21600, + nddt: -5400, + ndt: -9052, + npt: -9000, + nst: -12600, + nwt: -9000, + nzdt: 46800, + nzmt: 41400, + nzst: 43200, + pddt: -21600, + pdt: -25200, + pkst: 21600, + pkt: 18000, + plmt: 25590, + pmt: -13236, + ppmt: -17340, + ppt: -25200, + pst: -28800, + pwt: -25200, + qmt: -18840, + rmt: 5794, + sast: 7200, + sdmt: -16800, + sjmt: -20173, + smt: -13884, + sst: -39600, + tbmt: 10751, + tmt: 12344, + uct: 0, + utc: 0, + wast: 7200, + wat: 3600, + wemt: 7200, + west: 3600, + wet: 0, + wib: 25200, + wita: 28800, + wit: 32400, + wmt: 5040, + yddt: -25200, + ydt: -28800, + ypt: -28800, + yst: -32400, + ywt: -28800, + a: 3600, + b: 7200, + c: 10800, + d: 14400, + e: 18000, + f: 21600, + g: 25200, + h: 28800, + i: 32400, + k: 36000, + l: 39600, + m: 43200, + n: -3600, + o: -7200, + p: -10800, + q: -14400, + r: -18000, + s: -21600, + t: -25200, + u: -28800, + v: -32400, + w: -36000, + x: -39600, + y: -43200, + z: 0 +} + +const formats = { + yesterday: { + regex: /^yesterday/i, + name: 'yesterday', + callback () { + this.rd -= 1 + return this.resetTime() + } + }, + + now: { + regex: /^now/i, + name: 'now' + // do nothing + }, + + noon: { + regex: /^noon/i, + name: 'noon', + callback () { + return this.resetTime() && this.time(12, 0, 0, 0) + } + }, + + midnightOrToday: { + regex: /^(midnight|today)/i, + name: 'midnight | today', + callback () { + return this.resetTime() + } + }, + + tomorrow: { + regex: /^tomorrow/i, + name: 'tomorrow', + callback () { + this.rd += 1 + return this.resetTime() + } + }, + + timestamp: { + regex: /^@(-?\d+)/i, + name: 'timestamp', + callback (match, timestamp) { + this.rs += +timestamp + this.y = 1970 + this.m = 0 + this.d = 1 + this.dates = 0 + + return this.resetTime() && this.zone(0) + } + }, + + firstOrLastDay: { + regex: /^(first|last) day of/i, + name: 'firstdayof | lastdayof', + callback (match, day) { + if (day.toLowerCase() === 'first') { + this.firstOrLastDayOfMonth = 1 + } else { + this.firstOrLastDayOfMonth = -1 + } + } + }, + + backOrFrontOf: { + regex: RegExp('^(back|front) of ' + reHour24 + reSpaceOpt + reMeridian + '?', 'i'), + name: 'backof | frontof', + callback (match, side, hours, meridian) { + const back = side.toLowerCase() === 'back' + let hour = +hours + let minute = 15 + + if (!back) { + hour -= 1 + minute = 45 + } + + hour = processMeridian(hour, meridian) + + return this.resetTime() && this.time(hour, minute, 0, 0) + } + }, + + weekdayOf: { + regex: RegExp('^(' + reReltextnumber + '|' + reReltexttext + ')' + reSpace + '(' + reDayfull + '|' + reDayabbr + ')' + reSpace + 'of', 'i'), + name: 'weekdayof' + // todo + }, + + mssqltime: { + regex: RegExp('^' + reHour12 + ':' + reMinutelz + ':' + reSecondlz + '[:.]([0-9]+)' + reMeridian, 'i'), + name: 'mssqltime', + callback (match, hour, minute, second, frac, meridian) { + return this.time(processMeridian(+hour, meridian), +minute, +second, +frac.substr(0, 3)) + } + }, + + timeLong12: { + regex: RegExp('^' + reHour12 + '[:.]' + reMinute + '[:.]' + reSecondlz + reSpaceOpt + reMeridian, 'i'), + name: 'timelong12', + callback (match, hour, minute, second, meridian) { + return this.time(processMeridian(+hour, meridian), +minute, +second, 0) + } + }, + + timeShort12: { + regex: RegExp('^' + reHour12 + '[:.]' + reMinutelz + reSpaceOpt + reMeridian, 'i'), + name: 'timeshort12', + callback (match, hour, minute, meridian) { + return this.time(processMeridian(+hour, meridian), +minute, 0, 0) + } + }, + + timeTiny12: { + regex: RegExp('^' + reHour12 + reSpaceOpt + reMeridian, 'i'), + name: 'timetiny12', + callback (match, hour, meridian) { + return this.time(processMeridian(+hour, meridian), 0, 0, 0) + } + }, + + soap: { + regex: RegExp('^' + reYear4 + '-' + reMonthlz + '-' + reDaylz + 'T' + reHour24lz + ':' + reMinutelz + ':' + reSecondlz + reFrac + reTzCorrection + '?', 'i'), + name: 'soap', + callback (match, year, month, day, hour, minute, second, frac, tzCorrection) { + return this.ymd(+year, month - 1, +day) && + this.time(+hour, +minute, +second, +frac.substr(0, 3)) && + this.zone(processTzCorrection(tzCorrection)) + } + }, + + wddx: { + regex: RegExp('^' + reYear4 + '-' + reMonth + '-' + reDay + 'T' + reHour24 + ':' + reMinute + ':' + reSecond), + name: 'wddx', + callback (match, year, month, day, hour, minute, second) { + return this.ymd(+year, month - 1, +day) && this.time(+hour, +minute, +second, 0) + } + }, + + exif: { + regex: RegExp('^' + reYear4 + ':' + reMonthlz + ':' + reDaylz + ' ' + reHour24lz + ':' + reMinutelz + ':' + reSecondlz, 'i'), + name: 'exif', + callback (match, year, month, day, hour, minute, second) { + return this.ymd(+year, month - 1, +day) && this.time(+hour, +minute, +second, 0) + } + }, + + xmlRpc: { + regex: RegExp('^' + reYear4 + reMonthlz + reDaylz + 'T' + reHour24 + ':' + reMinutelz + ':' + reSecondlz), + name: 'xmlrpc', + callback (match, year, month, day, hour, minute, second) { + return this.ymd(+year, month - 1, +day) && this.time(+hour, +minute, +second, 0) + } + }, + + xmlRpcNoColon: { + regex: RegExp('^' + reYear4 + reMonthlz + reDaylz + '[Tt]' + reHour24 + reMinutelz + reSecondlz), + name: 'xmlrpcnocolon', + callback (match, year, month, day, hour, minute, second) { + return this.ymd(+year, month - 1, +day) && this.time(+hour, +minute, +second, 0) + } + }, + + clf: { + regex: RegExp('^' + reDay + '/(' + reMonthAbbr + ')/' + reYear4 + ':' + reHour24lz + ':' + reMinutelz + ':' + reSecondlz + reSpace + reTzCorrection, 'i'), + name: 'clf', + callback (match, day, month, year, hour, minute, second, tzCorrection) { + return this.ymd(+year, lookupMonth(month), +day) && + this.time(+hour, +minute, +second, 0) && + this.zone(processTzCorrection(tzCorrection)) + } + }, + + iso8601long: { + regex: RegExp('^t?' + reHour24 + '[:.]' + reMinute + '[:.]' + reSecond + reFrac, 'i'), + name: 'iso8601long', + callback (match, hour, minute, second, frac) { + return this.time(+hour, +minute, +second, +frac.substr(0, 3)) + } + }, + + dateTextual: { + regex: RegExp('^' + reMonthText + '[ .\\t-]*' + reDay + '[,.stndrh\\t ]+' + reYear, 'i'), + name: 'datetextual', + callback (match, month, day, year) { + return this.ymd(processYear(year), lookupMonth(month), +day) + } + }, + + pointedDate4: { + regex: RegExp('^' + reDay + '[.\\t-]' + reMonth + '[.-]' + reYear4), + name: 'pointeddate4', + callback (match, day, month, year) { + return this.ymd(+year, month - 1, +day) + } + }, + + pointedDate2: { + regex: RegExp('^' + reDay + '[.\\t]' + reMonth + '\\.' + reYear2), + name: 'pointeddate2', + callback (match, day, month, year) { + return this.ymd(processYear(year), month - 1, +day) + } + }, + + timeLong24: { + regex: RegExp('^t?' + reHour24 + '[:.]' + reMinute + '[:.]' + reSecond), + name: 'timelong24', + callback (match, hour, minute, second) { + return this.time(+hour, +minute, +second, 0) + } + }, + + dateNoColon: { + regex: RegExp('^' + reYear4 + reMonthlz + reDaylz), + name: 'datenocolon', + callback (match, year, month, day) { + return this.ymd(+year, month - 1, +day) + } + }, + + pgydotd: { + regex: RegExp('^' + reYear4 + '\\.?' + reDayOfYear), + name: 'pgydotd', + callback (match, year, day) { + return this.ymd(+year, 0, +day) + } + }, + + timeShort24: { + regex: RegExp('^t?' + reHour24 + '[:.]' + reMinute, 'i'), + name: 'timeshort24', + callback (match, hour, minute) { + return this.time(+hour, +minute, 0, 0) + } + }, + + iso8601noColon: { + regex: RegExp('^t?' + reHour24lz + reMinutelz + reSecondlz, 'i'), + name: 'iso8601nocolon', + callback (match, hour, minute, second) { + return this.time(+hour, +minute, +second, 0) + } + }, + + iso8601dateSlash: { + // eventhough the trailing slash is optional in PHP + // here it's mandatory and inputs without the slash + // are handled by dateslash + regex: RegExp('^' + reYear4 + '/' + reMonthlz + '/' + reDaylz + '/'), + name: 'iso8601dateslash', + callback (match, year, month, day) { + return this.ymd(+year, month - 1, +day) + } + }, + + dateSlash: { + regex: RegExp('^' + reYear4 + '/' + reMonth + '/' + reDay), + name: 'dateslash', + callback (match, year, month, day) { + return this.ymd(+year, month - 1, +day) + } + }, + + american: { + regex: RegExp('^' + reMonth + '/' + reDay + '/' + reYear), + name: 'american', + callback (match, month, day, year) { + return this.ymd(processYear(year), month - 1, +day) + } + }, + + americanShort: { + regex: RegExp('^' + reMonth + '/' + reDay), + name: 'americanshort', + callback (match, month, day) { + return this.ymd(this.y, month - 1, +day) + } + }, + + gnuDateShortOrIso8601date2: { + // iso8601date2 is complete subset of gnudateshort + regex: RegExp('^' + reYear + '-' + reMonth + '-' + reDay), + name: 'gnudateshort | iso8601date2', + callback (match, year, month, day) { + return this.ymd(processYear(year), month - 1, +day) + } + }, + + iso8601date4: { + regex: RegExp('^' + reYear4withSign + '-' + reMonthlz + '-' + reDaylz), + name: 'iso8601date4', + callback (match, year, month, day) { + return this.ymd(+year, month - 1, +day) + } + }, + + gnuNoColon: { + regex: RegExp('^t?' + reHour24lz + reMinutelz, 'i'), + name: 'gnunocolon', + callback (match, hour, minute) { + // this rule is a special case + // if time was already set once by any preceding rule, it sets the captured value as year + switch (this.times) { + case 0: + return this.time(+hour, +minute, 0, this.f) + case 1: + this.y = hour * 100 + +minute + this.times++ + + return true + default: + return false + } + } + }, + + gnuDateShorter: { + regex: RegExp('^' + reYear4 + '-' + reMonth), + name: 'gnudateshorter', + callback (match, year, month) { + return this.ymd(+year, month - 1, 1) + } + }, + + pgTextReverse: { + // note: allowed years are from 32-9999 + // years below 32 should be treated as days in datefull + regex: RegExp('^' + '(\\d{3,4}|[4-9]\\d|3[2-9])-(' + reMonthAbbr + ')-' + reDaylz, 'i'), + name: 'pgtextreverse', + callback (match, year, month, day) { + return this.ymd(processYear(year), lookupMonth(month), +day) + } + }, + + dateFull: { + regex: RegExp('^' + reDay + '[ \\t.-]*' + reMonthText + '[ \\t.-]*' + reYear, 'i'), + name: 'datefull', + callback (match, day, month, year) { + return this.ymd(processYear(year), lookupMonth(month), +day) + } + }, + + dateNoDay: { + regex: RegExp('^' + reMonthText + '[ .\\t-]*' + reYear4, 'i'), + name: 'datenoday', + callback (match, month, year) { + return this.ymd(+year, lookupMonth(month), 1) + } + }, + + dateNoDayRev: { + regex: RegExp('^' + reYear4 + '[ .\\t-]*' + reMonthText, 'i'), + name: 'datenodayrev', + callback (match, year, month) { + return this.ymd(+year, lookupMonth(month), 1) + } + }, + + pgTextShort: { + regex: RegExp('^(' + reMonthAbbr + ')-' + reDaylz + '-' + reYear, 'i'), + name: 'pgtextshort', + callback (match, month, day, year) { + return this.ymd(processYear(year), lookupMonth(month), +day) + } + }, + + dateNoYear: { + regex: RegExp('^' + reDateNoYear, 'i'), + name: 'datenoyear', + callback (match, month, day) { + return this.ymd(this.y, lookupMonth(month), +day) + } + }, + + dateNoYearRev: { + regex: RegExp('^' + reDay + '[ .\\t-]*' + reMonthText, 'i'), + name: 'datenoyearrev', + callback (match, day, month) { + return this.ymd(this.y, lookupMonth(month), +day) + } + }, + + isoWeekDay: { + regex: RegExp('^' + reYear4 + '-?W' + reWeekOfYear + '(?:-?([0-7]))?'), + name: 'isoweekday | isoweek', + callback (match, year, week, day) { + day = day ? +day : 1 + + if (!this.ymd(+year, 0, 1)) { + return false + } + + // get day of week for Jan 1st + let dayOfWeek = new Date(this.y, this.m, this.d).getDay() + + // and use the day to figure out the offset for day 1 of week 1 + dayOfWeek = 0 - (dayOfWeek > 4 ? dayOfWeek - 7 : dayOfWeek) + + this.rd += dayOfWeek + ((week - 1) * 7) + day + } + }, + + relativeText: { + regex: RegExp('^(' + reReltextnumber + '|' + reReltexttext + ')' + reSpace + '(' + reReltextunit + ')', 'i'), + name: 'relativetext', + callback (match, relValue, relUnit) { + // todo: implement handling of 'this time-unit' + // eslint-disable-next-line no-unused-vars + const { amount, behavior } = lookupRelative(relValue) + + switch (relUnit.toLowerCase()) { + case 'sec': + case 'secs': + case 'second': + case 'seconds': + this.rs += amount + break + case 'min': + case 'mins': + case 'minute': + case 'minutes': + this.ri += amount + break + case 'hour': + case 'hours': + this.rh += amount + break + case 'day': + case 'days': + this.rd += amount + break + case 'fortnight': + case 'fortnights': + case 'forthnight': + case 'forthnights': + this.rd += amount * 14 + break + case 'week': + case 'weeks': + this.rd += amount * 7 + break + case 'month': + case 'months': + this.rm += amount + break + case 'year': + case 'years': + this.ry += amount + break + case 'mon': case 'monday': + case 'tue': case 'tuesday': + case 'wed': case 'wednesday': + case 'thu': case 'thursday': + case 'fri': case 'friday': + case 'sat': case 'saturday': + case 'sun': case 'sunday': + this.resetTime() + this.weekday = lookupWeekday(relUnit, 7) + this.weekdayBehavior = 1 + this.rd += (amount > 0 ? amount - 1 : amount) * 7 + break + case 'weekday': + case 'weekdays': + // todo + break + } + } + }, + + relative: { + regex: RegExp('^([+-]*)[ \\t]*(\\d+)' + reSpaceOpt + '(' + reReltextunit + '|week)', 'i'), + name: 'relative', + callback (match, signs, relValue, relUnit) { + const minuses = signs.replace(/[^-]/g, '').length + + const amount = +relValue * Math.pow(-1, minuses) + + switch (relUnit.toLowerCase()) { + case 'sec': + case 'secs': + case 'second': + case 'seconds': + this.rs += amount + break + case 'min': + case 'mins': + case 'minute': + case 'minutes': + this.ri += amount + break + case 'hour': + case 'hours': + this.rh += amount + break + case 'day': + case 'days': + this.rd += amount + break + case 'fortnight': + case 'fortnights': + case 'forthnight': + case 'forthnights': + this.rd += amount * 14 + break + case 'week': + case 'weeks': + this.rd += amount * 7 + break + case 'month': + case 'months': + this.rm += amount + break + case 'year': + case 'years': + this.ry += amount + break + case 'mon': case 'monday': + case 'tue': case 'tuesday': + case 'wed': case 'wednesday': + case 'thu': case 'thursday': + case 'fri': case 'friday': + case 'sat': case 'saturday': + case 'sun': case 'sunday': + this.resetTime() + this.weekday = lookupWeekday(relUnit, 7) + this.weekdayBehavior = 1 + this.rd += (amount > 0 ? amount - 1 : amount) * 7 + break + case 'weekday': + case 'weekdays': + // todo + break + } + } + }, + + dayText: { + regex: RegExp('^(' + reDaytext + ')', 'i'), + name: 'daytext', + callback (match, dayText) { + this.resetTime() + this.weekday = lookupWeekday(dayText, 0) + + if (this.weekdayBehavior !== 2) { + this.weekdayBehavior = 1 + } + } + }, + + relativeTextWeek: { + regex: RegExp('^(' + reReltexttext + ')' + reSpace + 'week', 'i'), + name: 'relativetextweek', + callback (match, relText) { + this.weekdayBehavior = 2 + + switch (relText.toLowerCase()) { + case 'this': + this.rd += 0 + break + case 'next': + this.rd += 7 + break + case 'last': + case 'previous': + this.rd -= 7 + break + } + + if (isNaN(this.weekday)) { + this.weekday = 1 + } + } + }, + + monthFullOrMonthAbbr: { + regex: RegExp('^(' + reMonthFull + '|' + reMonthAbbr + ')', 'i'), + name: 'monthfull | monthabbr', + callback (match, month) { + return this.ymd(this.y, lookupMonth(month), this.d) + } + }, + + tzCorrection: { + regex: RegExp('^' + reTzCorrection, 'i'), + name: 'tzcorrection', + callback (tzCorrection) { + return this.zone(processTzCorrection(tzCorrection)) + } + }, + + tzAbbr: { + regex: RegExp('^' + reTzAbbr), + name: 'tzabbr', + callback (match, abbr) { + const offset = tzAbbrOffsets[abbr.toLowerCase()] + + if (isNaN(offset)) { + return false + } + + return this.zone(offset) + } + }, + + ago: { + regex: /^ago/i, + name: 'ago', + callback () { + this.ry = -this.ry + this.rm = -this.rm + this.rd = -this.rd + this.rh = -this.rh + this.ri = -this.ri + this.rs = -this.rs + this.rf = -this.rf + } + }, + + year4: { + regex: RegExp('^' + reYear4), + name: 'year4', + callback (match, year) { + this.y = +year + return true + } + }, + + whitespace: { + regex: /^[ .,\t]+/, + name: 'whitespace' + // do nothing + }, + + dateShortWithTimeLong: { + regex: RegExp('^' + reDateNoYear + 't?' + reHour24 + '[:.]' + reMinute + '[:.]' + reSecond, 'i'), + name: 'dateshortwithtimelong', + callback (match, month, day, hour, minute, second) { + return this.ymd(this.y, lookupMonth(month), +day) && this.time(+hour, +minute, +second, 0) + } + }, + + dateShortWithTimeLong12: { + regex: RegExp('^' + reDateNoYear + reHour12 + '[:.]' + reMinute + '[:.]' + reSecondlz + reSpaceOpt + reMeridian, 'i'), + name: 'dateshortwithtimelong12', + callback (match, month, day, hour, minute, second, meridian) { + return this.ymd(this.y, lookupMonth(month), +day) && this.time(processMeridian(+hour, meridian), +minute, +second, 0) + } + }, + + dateShortWithTimeShort: { + regex: RegExp('^' + reDateNoYear + 't?' + reHour24 + '[:.]' + reMinute, 'i'), + name: 'dateshortwithtimeshort', + callback (match, month, day, hour, minute) { + return this.ymd(this.y, lookupMonth(month), +day) && this.time(+hour, +minute, 0, 0) + } + }, + + dateShortWithTimeShort12: { + regex: RegExp('^' + reDateNoYear + reHour12 + '[:.]' + reMinutelz + reSpaceOpt + reMeridian, 'i'), + name: 'dateshortwithtimeshort12', + callback (match, month, day, hour, minute, meridian) { + return this.ymd(this.y, lookupMonth(month), +day) && this.time(processMeridian(+hour, meridian), +minute, 0, 0) + } + } +} + +const resultProto = { + // date + y: NaN, + m: NaN, + d: NaN, + // time + h: NaN, + i: NaN, + s: NaN, + f: NaN, + + // relative shifts + ry: 0, + rm: 0, + rd: 0, + rh: 0, + ri: 0, + rs: 0, + rf: 0, + + // weekday related shifts + weekday: NaN, + weekdayBehavior: 0, + + // first or last day of month + // 0 none, 1 first, -1 last + firstOrLastDayOfMonth: 0, + + // timezone correction in minutes + z: NaN, + + // counters + dates: 0, + times: 0, + zones: 0, + + // helper functions + ymd (y, m, d) { + if (this.dates > 0) { + return false + } + + this.dates++ + this.y = y + this.m = m + this.d = d + return true + }, + + time (h, i, s, f) { + if (this.times > 0) { + return false + } + + this.times++ + this.h = h + this.i = i + this.s = s + this.f = f + + return true + }, + + resetTime () { + this.h = 0 + this.i = 0 + this.s = 0 + this.f = 0 + this.times = 0 + + return true + }, + + zone (minutes) { + if (this.zones <= 1) { + this.zones++ + this.z = minutes + return true + } + + return false + }, + + toDate (relativeTo) { + if (this.dates && !this.times) { + this.h = this.i = this.s = this.f = 0 + } + + // fill holes + if (isNaN(this.y)) { + this.y = relativeTo.getFullYear() + } + + if (isNaN(this.m)) { + this.m = relativeTo.getMonth() + } + + if (isNaN(this.d)) { + this.d = relativeTo.getDate() + } + + if (isNaN(this.h)) { + this.h = relativeTo.getHours() + } + + if (isNaN(this.i)) { + this.i = relativeTo.getMinutes() + } + + if (isNaN(this.s)) { + this.s = relativeTo.getSeconds() + } + + if (isNaN(this.f)) { + this.f = relativeTo.getMilliseconds() + } + + // adjust special early + switch (this.firstOrLastDayOfMonth) { + case 1: + this.d = 1 + break + case -1: + this.d = 0 + this.m += 1 + break + } + + if (!isNaN(this.weekday)) { + const date = new Date(relativeTo.getTime()) + date.setFullYear(this.y, this.m, this.d) + date.setHours(this.h, this.i, this.s, this.f) + + const dow = date.getDay() + + if (this.weekdayBehavior === 2) { + // To make "this week" work, where the current day of week is a "sunday" + if (dow === 0 && this.weekday !== 0) { + this.weekday = -6 + } + + // To make "sunday this week" work, where the current day of week is not a "sunday" + if (this.weekday === 0 && dow !== 0) { + this.weekday = 7 + } + + this.d -= dow + this.d += this.weekday + } else { + let diff = this.weekday - dow + + // some PHP magic + if ((this.rd < 0 && diff < 0) || (this.rd >= 0 && diff <= -this.weekdayBehavior)) { + diff += 7 + } + + if (this.weekday >= 0) { + this.d += diff + } else { + this.d -= (7 - (Math.abs(this.weekday) - dow)) + } + + this.weekday = NaN + } + } + + // adjust relative + this.y += this.ry + this.m += this.rm + this.d += this.rd + + this.h += this.rh + this.i += this.ri + this.s += this.rs + this.f += this.rf + + this.ry = this.rm = this.rd = 0 + this.rh = this.ri = this.rs = this.rf = 0 + + const result = new Date(relativeTo.getTime()) + // since Date constructor treats years <= 99 as 1900+ + // it can't be used, thus this weird way + result.setFullYear(this.y, this.m, this.d) + result.setHours(this.h, this.i, this.s, this.f) + + // note: this is done twice in PHP + // early when processing special relatives + // and late + // todo: check if the logic can be reduced + // to just one time action + switch (this.firstOrLastDayOfMonth) { + case 1: + result.setDate(1) + break + case -1: + result.setMonth(result.getMonth() + 1, 0) + break + } + + // adjust timezone + if (!isNaN(this.z) && result.getTimezoneOffset() !== this.z) { + result.setUTCFullYear( + result.getFullYear(), + result.getMonth(), + result.getDate()) + + result.setUTCHours( + result.getHours(), + result.getMinutes(), + result.getSeconds() - this.z, + result.getMilliseconds()) + } + + return result + } +} + +module.exports = function strtotime (str, now) { + // discuss at: https://locutus.io/php/strtotime/ + // original by: Caio Ariede (https://caioariede.com) + // improved by: Kevin van Zonneveld (https://kvz.io) + // improved by: Caio Ariede (https://caioariede.com) + // improved by: A. Matías Quezada (https://amatiasq.com) + // improved by: preuter + // improved by: Brett Zamir (https://brett-zamir.me) + // improved by: Mirko Faber + // input by: David + // bugfixed by: Wagner B. Soares + // bugfixed by: Artur Tchernychev + // bugfixed by: Stephan Bösch-Plepelits (https://github.com/plepe) + // reimplemented by: Rafał Kukawski + // note 1: Examples all have a fixed timestamp to prevent + // note 1: tests to fail because of variable time(zones) + // example 1: strtotime('+1 day', 1129633200) + // returns 1: 1129719600 + // example 2: strtotime('+1 week 2 days 4 hours 2 seconds', 1129633200) + // returns 2: 1130425202 + // example 3: strtotime('last month', 1129633200) + // returns 3: 1127041200 + // example 4: strtotime('2009-05-04 08:30:00+00') + // returns 4: 1241425800 + // example 5: strtotime('2009-05-04 08:30:00+02:00') + // returns 5: 1241418600 + // example 6: strtotime('2009-05-04 08:30:00 YWT') + // returns 6: 1241454600 + + if (now == null) { + now = Math.floor(Date.now() / 1000) + } + + // the rule order is important + // if multiple rules match, the longest match wins + // if multiple rules match the same string, the first match wins + const rules = [ + formats.yesterday, + formats.now, + formats.noon, + formats.midnightOrToday, + formats.tomorrow, + formats.timestamp, + formats.firstOrLastDay, + formats.backOrFrontOf, + // formats.weekdayOf, // not yet implemented + formats.timeTiny12, + formats.timeShort12, + formats.timeLong12, + formats.mssqltime, + formats.timeShort24, + formats.timeLong24, + formats.iso8601long, + formats.gnuNoColon, + formats.iso8601noColon, + formats.americanShort, + formats.american, + formats.iso8601date4, + formats.iso8601dateSlash, + formats.dateSlash, + formats.gnuDateShortOrIso8601date2, + formats.gnuDateShorter, + formats.dateFull, + formats.pointedDate4, + formats.pointedDate2, + formats.dateNoDay, + formats.dateNoDayRev, + formats.dateTextual, + formats.dateNoYear, + formats.dateNoYearRev, + formats.dateNoColon, + formats.xmlRpc, + formats.xmlRpcNoColon, + formats.soap, + formats.wddx, + formats.exif, + formats.pgydotd, + formats.isoWeekDay, + formats.pgTextShort, + formats.pgTextReverse, + formats.clf, + formats.year4, + formats.ago, + formats.dayText, + formats.relativeTextWeek, + formats.relativeText, + formats.monthFullOrMonthAbbr, + formats.tzCorrection, + formats.tzAbbr, + formats.dateShortWithTimeShort12, + formats.dateShortWithTimeLong12, + formats.dateShortWithTimeShort, + formats.dateShortWithTimeLong, + formats.relative, + formats.whitespace + ] + + const result = Object.create(resultProto) + + while (str.length) { + let longestMatch = null + let finalRule = null + + for (let i = 0, l = rules.length; i < l; i++) { + const format = rules[i] + + const match = str.match(format.regex) + + if (match) { + if (!longestMatch || match[0].length > longestMatch[0].length) { + longestMatch = match + finalRule = format + } + } + } + + if (!finalRule || (finalRule.callback && finalRule.callback.apply(result, longestMatch) === false)) { + return false + } + + str = str.substr(longestMatch[0].length) + finalRule = null + longestMatch = null + } + + return Math.floor(result.toDate(new Date(now * 1000)) / 1000) +} \ No newline at end of file diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 27b021af0..51ed99b6c 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -25,7 +25,7 @@ class BitcoinApi implements AbstractBitcoinApi { .then((transaction: IBitcoinApi.Transaction) => { if (skipConversion) { transaction.vout.forEach((vout) => { - vout.value = vout.value * 100000000; + vout.value = Math.round(vout.value * 100000000); }); return transaction; } @@ -143,11 +143,11 @@ class BitcoinApi implements AbstractBitcoinApi { esploraTransaction.vout = transaction.vout.map((vout) => { return { - value: vout.value * 100000000, + value: Math.round(vout.value * 100000000), scriptpubkey: vout.scriptPubKey.hex, scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address : vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '', - scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.asm) : '', + scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.hex) : '', scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type), }; }); @@ -157,7 +157,7 @@ class BitcoinApi implements AbstractBitcoinApi { is_coinbase: !!vin.coinbase, prevout: null, scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '', - scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.asm) || '', + scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.hex) || '', sequence: vin.sequence, txid: vin.txid || '', vout: vin.vout || 0, @@ -212,6 +212,7 @@ class BitcoinApi implements AbstractBitcoinApi { 'witness_v0_scripthash': 'v0_p2wsh', 'witness_v1_taproot': 'v1_p2tr', 'nonstandard': 'nonstandard', + 'multisig': 'multisig', 'nulldata': 'op_return' }; @@ -235,7 +236,7 @@ class BitcoinApi implements AbstractBitcoinApi { } else { mempoolEntry = await this.$getMempoolEntry(transaction.txid); } - transaction.fee = mempoolEntry.fees.base * 100000000; + transaction.fee = Math.round(mempoolEntry.fees.base * 100000000); return transaction; } @@ -289,23 +290,68 @@ class BitcoinApi implements AbstractBitcoinApi { return transaction; } - private convertScriptSigAsm(str: string): string { - const a = str.split(' '); + private convertScriptSigAsm(hex: string): string { + const buf = Buffer.from(hex, 'hex'); + const b: string[] = []; - a.forEach((chunk) => { - if (chunk.substr(0, 3) === 'OP_') { - chunk = chunk.replace(/^OP_(\d+)/, 'OP_PUSHNUM_$1'); - chunk = chunk.replace('OP_CHECKSEQUENCEVERIFY', 'OP_CSV'); - b.push(chunk); - } else { - chunk = chunk.replace('[ALL]', '01'); - if (chunk === '0') { - b.push('OP_0'); + + let i = 0; + while (i < buf.length) { + const op = buf[i]; + if (op >= 0x01 && op <= 0x4e) { + i++; + let push: number; + if (op === 0x4c) { + push = buf.readUInt8(i); + b.push('OP_PUSHDATA1'); + i += 1; + } else if (op === 0x4d) { + push = buf.readUInt16LE(i); + b.push('OP_PUSHDATA2'); + i += 2; + } else if (op === 0x4e) { + push = buf.readUInt32LE(i); + b.push('OP_PUSHDATA4'); + i += 4; } else { - b.push('OP_PUSHBYTES_' + Math.round(chunk.length / 2) + ' ' + chunk); + push = op; + b.push('OP_PUSHBYTES_' + push); } + + const data = buf.slice(i, i + push); + if (data.length !== push) { + break; + } + + b.push(data.toString('hex')); + i += data.length; + } else { + if (op === 0x00) { + b.push('OP_0'); + } else if (op === 0x4f) { + b.push('OP_PUSHNUM_NEG1'); + } else if (op === 0xb1) { + b.push('OP_CLTV'); + } else if (op === 0xb2) { + b.push('OP_CSV'); + } else if (op === 0xba) { + b.push('OP_CHECKSIGADD'); + } else { + const opcode = bitcoinjs.script.toASM([ op ]); + if (opcode && op < 0xfd) { + if (/^OP_(\d+)$/.test(opcode)) { + b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1')); + } else { + b.push(opcode); + } + } else { + b.push('OP_RETURN_' + op); + } + } + i += 1; } - }); + } + return b.join(' '); } @@ -316,21 +362,21 @@ class BitcoinApi implements AbstractBitcoinApi { if (vin.prevout.scriptpubkey_type === 'p2sh') { const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0]; - vin.inner_redeemscript_asm = this.convertScriptSigAsm(bitcoinjs.script.toASM(Buffer.from(redeemScript, 'hex'))); + vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript); if (vin.witness && vin.witness.length > 2) { const witnessScript = vin.witness[vin.witness.length - 1]; - vin.inner_witnessscript_asm = this.convertScriptSigAsm(bitcoinjs.script.toASM(Buffer.from(witnessScript, 'hex'))); + vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript); } } if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) { const witnessScript = vin.witness[vin.witness.length - 1]; - vin.inner_witnessscript_asm = this.convertScriptSigAsm(bitcoinjs.script.toASM(Buffer.from(witnessScript, 'hex'))); + vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript); } if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness && vin.witness.length > 1) { const witnessScript = vin.witness[vin.witness.length - 2]; - vin.inner_witnessscript_asm = this.convertScriptSigAsm(bitcoinjs.script.toASM(Buffer.from(witnessScript, 'hex'))); + vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript); } } diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index bff73dd54..4402f0d37 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -23,6 +23,7 @@ class Blocks { private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; private blockIndexingStarted = false; public blockIndexingCompleted = false; + public reindexFlag = true; // Always re-index the latest indexed data in case the node went offline with an invalid block tip (reorg) constructor() { } @@ -74,9 +75,12 @@ class Blocks { transactions.push(tx); transactionsFetched++; } catch (e) { - logger.debug('Error fetching block tx: ' + (e instanceof Error ? e.message : e)); if (i === 0) { - throw new Error('Failed to fetch Coinbase transaction: ' + txIds[i]); + const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e); + logger.err(msg); + throw new Error(msg); + } else { + logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e)); } } } @@ -135,9 +139,16 @@ class Blocks { } else { pool = await poolsRepository.$getUnknownPool(); } + + if (!pool) { // We should never have this situation in practise + logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. Check your "pools" table entries`); + return blockExtended; + } + blockExtended.extras.pool = { id: pool.id, - name: pool.name + name: pool.name, + slug: pool.slug, }; } @@ -182,16 +193,19 @@ class Blocks { * [INDEXING] Index all blocks metadata for the mining dashboard */ public async $generateBlockDatabase() { - if (this.blockIndexingStarted) { + if (this.blockIndexingStarted && !this.reindexFlag) { return; } + this.reindexFlag = false; + const blockchainInfo = await bitcoinClient.getBlockchainInfo(); if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync return; } this.blockIndexingStarted = true; + this.blockIndexingCompleted = false; try { let currentBlockHeight = blockchainInfo.blocks; @@ -203,11 +217,12 @@ class Blocks { const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1); - logger.info(`Indexing blocks from #${currentBlockHeight} to #${lastBlockToIndex}`); + logger.debug(`Indexing blocks from #${currentBlockHeight} to #${lastBlockToIndex}`); const chunkSize = 10000; let totaIndexed = await blocksRepository.$blockCount(null, null); let indexedThisRun = 0; + let newlyIndexed = 0; const startedAt = new Date().getTime() / 1000; let timer = new Date().getTime() / 1000; @@ -217,12 +232,11 @@ class Blocks { const missingBlockHeights: number[] = await blocksRepository.$getMissingBlocksBetweenHeights( currentBlockHeight, endBlock); if (missingBlockHeights.length <= 0) { - logger.debug(`No missing blocks between #${currentBlockHeight} to #${endBlock}`); currentBlockHeight -= chunkSize; continue; } - logger.debug(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`); + logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`); for (const blockHeight of missingBlockHeights) { if (blockHeight < lastBlockToIndex) { @@ -244,14 +258,16 @@ class Blocks { const block = BitcoinApi.convertBlock(await bitcoinClient.getBlock(blockHash)); const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true); const blockExtended = await this.$getBlockExtended(block, transactions); + + newlyIndexed++; await blocksRepository.$saveBlockInDatabase(blockExtended); } currentBlockHeight -= chunkSize; } - logger.info('Block indexing completed'); + logger.info(`Indexed ${newlyIndexed} blocks`); } catch (e) { - logger.err('An error occured in $generateBlockDatabase(). Trying again later. ' + e); + logger.err('Block indexing failed. Trying again later. Reason: ' + (e instanceof Error ? e.message : e)); this.blockIndexingStarted = false; return; } @@ -309,6 +325,12 @@ class Blocks { if (Common.indexingEnabled()) { await blocksRepository.$saveBlockInDatabase(blockExtended); + + // If the last 10 blocks chain is not valid, re-index them (reorg) + const chainValid = await blocksRepository.$validateRecentBlocks(); + if (!chainValid) { + this.reindexFlag = true; + } } if (block.height % 2016 === 0) { diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index ffa9041e3..0e9a18220 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -1,12 +1,11 @@ -import { PoolConnection } from 'mysql2/promise'; import config from '../config'; -import { DB } from '../database'; +import DB from '../database'; import logger from '../logger'; const sleep = (ms: number) => new Promise(res => setTimeout(res, ms)); class DatabaseMigration { - private static currentVersion = 15; + private static currentVersion = 17; private queryTimeout = 120000; private statisticsAddedIndexed = false; @@ -77,107 +76,112 @@ class DatabaseMigration { await this.$setStatisticsAddedIndexedFlag(databaseSchemaVersion); const isBitcoin = ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK); - const connection = await DB.getConnection(); try { - await this.$executeQuery(connection, this.getCreateElementsTableQuery(), await this.$checkIfTableExists('elements_pegs')); - await this.$executeQuery(connection, this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics')); + await this.$executeQuery(this.getCreateElementsTableQuery(), await this.$checkIfTableExists('elements_pegs')); + await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics')); if (databaseSchemaVersion < 2 && this.statisticsAddedIndexed === false) { - await this.$executeQuery(connection, `CREATE INDEX added ON statistics (added);`); + await this.$executeQuery(`CREATE INDEX added ON statistics (added);`); } if (databaseSchemaVersion < 3) { - await this.$executeQuery(connection, this.getCreatePoolsTableQuery(), await this.$checkIfTableExists('pools')); + await this.$executeQuery(this.getCreatePoolsTableQuery(), await this.$checkIfTableExists('pools')); } if (databaseSchemaVersion < 4) { - await this.$executeQuery(connection, 'DROP table IF EXISTS blocks;'); - await this.$executeQuery(connection, this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks')); + await this.$executeQuery('DROP table IF EXISTS blocks;'); + await this.$executeQuery(this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks')); } if (databaseSchemaVersion < 5 && isBitcoin === true) { logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`); - await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index - await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index + await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"'); } if (databaseSchemaVersion < 6 && isBitcoin === true) { logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`); - await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index + await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index // Cleanup original blocks fields type - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"'); // We also fix the pools.id type so we need to drop/re-create the foreign key - await this.$executeQuery(connection, 'ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`'); - await this.$executeQuery(connection, 'ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT'); - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL'); - await this.$executeQuery(connection, 'ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)'); + await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`'); + await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL'); + await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)'); // Add new block indexing fields - await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""'); - await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL'); + await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""'); + await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL'); } if (databaseSchemaVersion < 7 && isBitcoin === true) { - await this.$executeQuery(connection, 'DROP table IF EXISTS hashrates;'); - await this.$executeQuery(connection, this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates')); + await this.$executeQuery('DROP table IF EXISTS hashrates;'); + await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates')); } if (databaseSchemaVersion < 8 && isBitcoin === true) { logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`); - await this.$executeQuery(connection, 'TRUNCATE hashrates;'); // Need to re-index - await this.$executeQuery(connection, 'ALTER TABLE `hashrates` DROP INDEX `PRIMARY`'); - await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST'); - await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"'); + await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index + await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`'); + await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST'); + await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"'); } if (databaseSchemaVersion < 9 && isBitcoin === true) { logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`); - await this.$executeQuery(connection, 'TRUNCATE hashrates;'); // Need to re-index - await this.$executeQuery(connection, 'ALTER TABLE `state` CHANGE `name` `name` varchar(100)'); - await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)'); + await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index + await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)'); + await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)'); } if (databaseSchemaVersion < 10 && isBitcoin === true) { - await this.$executeQuery(connection, 'ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)'); + await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)'); } if (databaseSchemaVersion < 11 && isBitcoin === true) { logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`); - await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index - await this.$executeQuery(connection, `ALTER TABLE blocks + await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index + await this.$executeQuery(`ALTER TABLE blocks ADD avg_fee INT UNSIGNED NULL, ADD avg_fee_rate INT UNSIGNED NULL `); - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"'); } if (databaseSchemaVersion < 12 && isBitcoin === true) { // No need to re-index because the new data type can contain larger values - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); } if (databaseSchemaVersion < 13 && isBitcoin === true) { - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); - await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); } if (databaseSchemaVersion < 14 && isBitcoin === true) { logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`); - await this.$executeQuery(connection, 'TRUNCATE hashrates;'); // Need to re-index - await this.$executeQuery(connection, 'ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`'); - await this.$executeQuery(connection, 'ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index + await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`'); + await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"'); } - connection.release(); + if (databaseSchemaVersion < 16 && isBitcoin === true) { + logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`); + await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index because we changed timestamps + } + + if (databaseSchemaVersion < 17 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL'); + } } catch (e) { - connection.release(); throw e; } } @@ -194,13 +198,11 @@ class DatabaseMigration { return; } - const connection = await DB.getConnection(); - try { // We don't use "CREATE INDEX IF NOT EXISTS" because it is not supported on old mariadb version 5.X const query = `SELECT COUNT(1) hasIndex FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema=DATABASE() AND table_name='statistics' AND index_name='added';`; - const [rows] = await this.$executeQuery(connection, query, true); + const [rows] = await this.$executeQuery(query, true); if (rows[0].hasIndex === 0) { logger.debug('MIGRATIONS: `statistics.added` is not indexed'); this.statisticsAddedIndexed = false; @@ -214,28 +216,24 @@ class DatabaseMigration { logger.err('MIGRATIONS: Unable to check if `statistics.added` INDEX exist or not.'); this.statisticsAddedIndexed = true; } - - connection.release(); } /** * Small query execution wrapper to log all executed queries */ - private async $executeQuery(connection: PoolConnection, query: string, silent: boolean = false): Promise { + private async $executeQuery(query: string, silent: boolean = false): Promise { if (!silent) { logger.debug('MIGRATIONS: Execute query:\n' + query); } - return connection.query({ sql: query, timeout: this.queryTimeout }); + return DB.query({ sql: query, timeout: this.queryTimeout }); } /** * Check if 'table' exists in the database */ private async $checkIfTableExists(table: string): Promise { - const connection = await DB.getConnection(); const query = `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${config.DATABASE.DATABASE}' AND TABLE_NAME = '${table}'`; - const [rows] = await connection.query({ sql: query, timeout: this.queryTimeout }); - connection.release(); + const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); return rows[0]['COUNT(*)'] === 1; } @@ -243,10 +241,8 @@ class DatabaseMigration { * Get current database version */ private async $getSchemaVersionFromDatabase(): Promise { - const connection = await DB.getConnection(); const query = `SELECT number FROM state WHERE name = 'schema_version';`; - const [rows] = await this.$executeQuery(connection, query, true); - connection.release(); + const [rows] = await this.$executeQuery(query, true); return rows[0]['number']; } @@ -254,8 +250,6 @@ class DatabaseMigration { * Create the `state` table */ private async $createMigrationStateTable(): Promise { - const connection = await DB.getConnection(); - try { const query = `CREATE TABLE IF NOT EXISTS state ( name varchar(25) NOT NULL, @@ -263,15 +257,12 @@ class DatabaseMigration { string varchar(100) NULL, CONSTRAINT name_unique UNIQUE (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; - await this.$executeQuery(connection, query); + await this.$executeQuery(query); // Set initial values - await this.$executeQuery(connection, `INSERT INTO state VALUES('schema_version', 0, NULL);`); - await this.$executeQuery(connection, `INSERT INTO state VALUES('last_elements_block', 0, NULL);`); - - connection.release(); + await this.$executeQuery(`INSERT INTO state VALUES('schema_version', 0, NULL);`); + await this.$executeQuery(`INSERT INTO state VALUES('last_elements_block', 0, NULL);`); } catch (e) { - connection.release(); throw e; } } @@ -286,18 +277,14 @@ class DatabaseMigration { } transactionQueries.push(this.getUpdateToLatestSchemaVersionQuery()); - const connection = await DB.getConnection(); try { - await this.$executeQuery(connection, 'START TRANSACTION;'); + await this.$executeQuery('START TRANSACTION;'); for (const query of transactionQueries) { - await this.$executeQuery(connection, query); + await this.$executeQuery(query); } - await this.$executeQuery(connection, 'COMMIT;'); - - connection.release(); + await this.$executeQuery('COMMIT;'); } catch (e) { - await this.$executeQuery(connection, 'ROLLBACK;'); - connection.release(); + await this.$executeQuery('ROLLBACK;'); throw e; } } @@ -337,14 +324,12 @@ class DatabaseMigration { * Print current database version */ private async $printDatabaseVersion() { - const connection = await DB.getConnection(); try { - const [rows] = await this.$executeQuery(connection, 'SELECT VERSION() as version;', true); + const [rows] = await this.$executeQuery('SELECT VERSION() as version;', true); logger.debug(`MIGRATIONS: Database engine version '${rows[0].version}'`); } catch (e) { logger.debug(`MIGRATIONS: Could not fetch database engine version. ` + e); } - connection.release(); } // Couple of wrappers to clean the main logic @@ -481,24 +466,22 @@ class DatabaseMigration { public async $truncateIndexedData(tables: string[]) { const allowedTables = ['blocks', 'hashrates']; - const connection = await DB.getConnection(); try { for (const table of tables) { if (!allowedTables.includes(table)) { logger.debug(`Table ${table} cannot to be re-indexed (not allowed)`); continue; - }; + } - await this.$executeQuery(connection, `TRUNCATE ${table}`, true); + await this.$executeQuery(`TRUNCATE ${table}`, true); if (table === 'hashrates') { - await this.$executeQuery(connection, 'UPDATE state set number = 0 where name = "last_hashrates_indexing"', true); + await this.$executeQuery('UPDATE state set number = 0 where name = "last_hashrates_indexing"', true); } logger.notice(`Table ${table} has been truncated`); } } catch (e) { logger.warn(`Unable to erase indexed data`); } - connection.release(); } } diff --git a/backend/src/api/liquid/elements-parser.ts b/backend/src/api/liquid/elements-parser.ts index 24c7ab949..12439e037 100644 --- a/backend/src/api/liquid/elements-parser.ts +++ b/backend/src/api/liquid/elements-parser.ts @@ -2,7 +2,7 @@ import { IBitcoinApi } from '../bitcoin/bitcoin-api.interface'; import bitcoinClient from '../bitcoin/bitcoin-client'; import bitcoinSecondClient from '../bitcoin/bitcoin-second-client'; import { Common } from '../common'; -import { DB } from '../../database'; +import DB from '../../database'; import logger from '../../logger'; class ElementsParser { @@ -33,10 +33,8 @@ class ElementsParser { } public async $getPegDataByMonth(): Promise { - const connection = await DB.getConnection(); 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 connection.query(query); - connection.release(); + const [rows] = await DB.query(query); return rows; } @@ -79,7 +77,6 @@ class ElementsParser { protected async $savePegToDatabase(height: number, blockTime: number, amount: number, txid: string, txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, final_tx: number): Promise { - const connection = await DB.getConnection(); const query = `INSERT INTO elements_pegs( block, datetime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`; @@ -87,24 +84,19 @@ class ElementsParser { const params: (string | number)[] = [ height, blockTime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx ]; - await connection.query(query, params); - connection.release(); + await DB.query(query, params); logger.debug(`Saved L-BTC peg from block height #${height} with TXID ${txid}.`); } protected async $getLatestBlockHeightFromDatabase(): Promise { - const connection = await DB.getConnection(); const query = `SELECT number FROM state WHERE name = 'last_elements_block'`; - const [rows] = await connection.query(query); - connection.release(); + const [rows] = await DB.query(query); return rows[0]['number']; } protected async $saveLatestBlockToDatabase(blockHeight: number) { - const connection = await DB.getConnection(); const query = `UPDATE state SET number = ? WHERE name = 'last_elements_block'`; - await connection.query(query, [blockHeight]); - connection.release(); + await DB.query(query, [blockHeight]); } } diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index 8b277da57..201813899 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -5,6 +5,7 @@ import HashratesRepository from '../repositories/HashratesRepository'; import bitcoinClient from './bitcoin/bitcoin-client'; import logger from '../logger'; import blocks from './blocks'; +import { Common } from './common'; class Mining { hashrateIndexingStarted = false; @@ -13,6 +14,26 @@ class Mining { constructor() { } + /** + * Get historical block reward and total fee + */ + public async $getHistoricalBlockFees(interval: string | null = null): Promise { + return await BlocksRepository.$getHistoricalBlockFees( + this.getTimeRange(interval), + Common.getSqlInterval(interval) + ); + } + + /** + * Get historical block rewards + */ + public async $getHistoricalBlockRewards(interval: string | null = null): Promise { + return await BlocksRepository.$getHistoricalBlockRewards( + this.getTimeRange(interval), + Common.getSqlInterval(interval) + ); + } + /** * Generate high level overview of the pool ranks and general stats */ @@ -33,7 +54,8 @@ class Mining { link: poolInfo.link, blockCount: poolInfo.blockCount, rank: rank++, - emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0 + emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0, + slug: poolInfo.slug, }; poolsStats.push(poolStat); }); @@ -44,8 +66,8 @@ class Mining { const blockCount: number = await BlocksRepository.$blockCount(null, interval); poolsStatistics['blockCount'] = blockCount; - const blockHeightTip = await bitcoinClient.getBlockCount(); - const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(144, blockHeightTip); + const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h'); + const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h); poolsStatistics['lastEstimatedHashrate'] = lastBlockHashrate; return poolsStatistics; @@ -54,19 +76,37 @@ class Mining { /** * Get all mining pool stats for a pool */ - public async $getPoolStat(poolId: number): Promise { - const pool = await PoolsRepository.$getPool(poolId); + public async $getPoolStat(slug: string): Promise { + const pool = await PoolsRepository.$getPool(slug); if (!pool) { throw new Error(`This mining pool does not exist`); } - const blockCount: number = await BlocksRepository.$blockCount(poolId); - const emptyBlocksCount = await BlocksRepository.$countEmptyBlocks(poolId); + const blockCount: number = await BlocksRepository.$blockCount(pool.id); + const totalBlock: number = await BlocksRepository.$blockCount(null, null); + + const blockCount24h: number = await BlocksRepository.$blockCount(pool.id, '24h'); + const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h'); + + const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w'); + const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w'); + + const currentEstimatedkHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h); return { pool: pool, - blockCount: blockCount, - emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0, + blockCount: { + 'all': blockCount, + '24h': blockCount24h, + '1w': blockCount1w, + }, + blockShare: { + 'all': blockCount / totalBlock, + '24h': blockCount24h / totalBlock24h, + '1w': blockCount1w / totalBlock1w, + }, + estimatedHashrate: currentEstimatedkHashrate * (blockCount24h / totalBlock24h), + reportedHashrate: null, }; } @@ -85,37 +125,43 @@ class Mining { return; } - // We only run this once a week - const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp('last_weekly_hashrates_indexing'); const now = new Date(); - if ((now.getTime() / 1000) - latestTimestamp < 604800) { - return; - } try { this.weeklyHashrateIndexingStarted = true; - logger.info(`Indexing mining pools weekly hashrates`); + // We only run this once a week + const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp('last_weekly_hashrates_indexing') * 1000; + if (now.getTime() - latestTimestamp < 604800000) { + this.weeklyHashrateIndexingStarted = false; + return; + } + } catch (e) { + this.weeklyHashrateIndexingStarted = false; + throw e; + } + try { const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps(); const hashrates: any[] = []; - const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f + const genesisTimestamp = 1231006505000; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f const lastMonday = new Date(now.setDate(now.getDate() - (now.getDay() + 6) % 7)); const lastMondayMidnight = this.getDateMidnight(lastMonday); - let toTimestamp = Math.round((lastMondayMidnight.getTime() - 604800) / 1000); + let toTimestamp = lastMondayMidnight.getTime(); const totalWeekIndexed = (await BlocksRepository.$blockCount(null, null)) / 1008; let indexedThisRun = 0; let totalIndexed = 0; - let startedAt = new Date().getTime() / 1000; + let newlyIndexed = 0; + let startedAt = new Date().getTime(); while (toTimestamp > genesisTimestamp) { - const fromTimestamp = toTimestamp - 604800; + const fromTimestamp = toTimestamp - 604800000; // Skip already indexed weeks - if (indexedTimestamp.includes(toTimestamp)) { - toTimestamp -= 604800; + if (indexedTimestamp.includes(toTimestamp / 1000)) { + toTimestamp -= 604800000; ++totalIndexed; continue; } @@ -123,17 +169,17 @@ class Mining { // Check if we have blocks for the previous week (which mean that the week // we are currently indexing has complete data) const blockStatsPreviousWeek: any = await BlocksRepository.$blockCountBetweenTimestamp( - null, fromTimestamp - 604800, toTimestamp - 604800); + null, (fromTimestamp - 604800000) / 1000, (toTimestamp - 604800000) / 1000); if (blockStatsPreviousWeek.blockCount === 0) { // We are done indexing break; } const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp( - null, fromTimestamp, toTimestamp); + null, fromTimestamp / 1000, toTimestamp / 1000); const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, blockStats.lastBlockHeight); - let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp, toTimestamp); + let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp / 1000, toTimestamp / 1000); const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0); pools = pools.map((pool: any) => { pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate; @@ -143,7 +189,7 @@ class Mining { for (const pool of pools) { hashrates.push({ - hashrateTimestamp: toTimestamp, + hashrateTimestamp: toTimestamp / 1000, avgHashrate: pool['hashrate'], poolId: pool.poolId, share: pool['share'], @@ -151,26 +197,29 @@ class Mining { }); } + newlyIndexed += hashrates.length; await HashratesRepository.$saveHashrates(hashrates); hashrates.length = 0; - const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); + const elapsedSeconds = Math.max(1, Math.round((new Date().getTime()) - startedAt)) / 1000; if (elapsedSeconds > 1) { const weeksPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2); - const formattedDate = new Date(fromTimestamp * 1000).toUTCString(); + const formattedDate = new Date(fromTimestamp).toUTCString(); const weeksLeft = Math.round(totalWeekIndexed - totalIndexed); logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds} weeks/sec | ~${weeksLeft} weeks left to index`); - startedAt = new Date().getTime() / 1000; + startedAt = new Date().getTime(); indexedThisRun = 0; } - toTimestamp -= 604800; + toTimestamp -= 604800000; ++indexedThisRun; ++totalIndexed; } this.weeklyHashrateIndexingStarted = false; await HashratesRepository.$setLatestRunTimestamp('last_weekly_hashrates_indexing'); - logger.info(`Weekly pools hashrate indexing completed`); + if (newlyIndexed > 0) { + logger.info(`Indexed ${newlyIndexed} pools weekly hashrate`); + } } catch (e) { this.weeklyHashrateIndexingStarted = false; throw e; @@ -185,35 +234,41 @@ class Mining { return; } - // We only run this once a day - const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp('last_hashrates_indexing'); - const now = new Date().getTime() / 1000; - if (now - latestTimestamp < 86400) { - return; - } + const now = new Date().getTime(); try { this.hashrateIndexingStarted = true; - logger.info(`Indexing network daily hashrate`); + // We only run this once a day + const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp('last_hashrates_indexing') * 1000; + if (now - latestTimestamp < 86400000) { + this.hashrateIndexingStarted = false; + return; + } + } catch (e) { + this.hashrateIndexingStarted = false; + throw e; + } + try { const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp); - const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f + const genesisTimestamp = 1231006505000; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f const lastMidnight = this.getDateMidnight(new Date()); - let toTimestamp = Math.round(lastMidnight.getTime() / 1000); + let toTimestamp = Math.round(lastMidnight.getTime()); const hashrates: any[] = []; const totalDayIndexed = (await BlocksRepository.$blockCount(null, null)) / 144; let indexedThisRun = 0; let totalIndexed = 0; - let startedAt = new Date().getTime() / 1000; + let newlyIndexed = 0; + let startedAt = new Date().getTime(); while (toTimestamp > genesisTimestamp) { - const fromTimestamp = toTimestamp - 86400; + const fromTimestamp = toTimestamp - 86400000; // Skip already indexed weeks - if (indexedTimestamp.includes(toTimestamp)) { - toTimestamp -= 86400; + if (indexedTimestamp.includes(toTimestamp / 1000)) { + toTimestamp -= 86400000; ++totalIndexed; continue; } @@ -221,18 +276,18 @@ class Mining { // Check if we have blocks for the previous day (which mean that the day // we are currently indexing has complete data) const blockStatsPreviousDay: any = await BlocksRepository.$blockCountBetweenTimestamp( - null, fromTimestamp - 86400, toTimestamp - 86400); + null, (fromTimestamp - 86400000) / 1000, (toTimestamp - 86400000) / 1000); if (blockStatsPreviousDay.blockCount === 0) { // We are done indexing break; } const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp( - null, fromTimestamp, toTimestamp); + null, fromTimestamp / 1000, toTimestamp / 1000); const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, blockStats.lastBlockHeight); hashrates.push({ - hashrateTimestamp: toTimestamp, + hashrateTimestamp: toTimestamp / 1000, avgHashrate: lastBlockHashrate, poolId: 0, share: 1, @@ -240,21 +295,23 @@ class Mining { }); if (hashrates.length > 10) { + newlyIndexed += hashrates.length; await HashratesRepository.$saveHashrates(hashrates); hashrates.length = 0; } - const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); + const elapsedSeconds = Math.max(1, Math.round(new Date().getTime() - startedAt)) / 1000; if (elapsedSeconds > 1) { const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2); - const formattedDate = new Date(fromTimestamp * 1000).toUTCString(); + const formattedDate = new Date(fromTimestamp).toUTCString(); const daysLeft = Math.round(totalDayIndexed - totalIndexed); - logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ~${daysLeft} days left to index`); - startedAt = new Date().getTime() / 1000; + logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ` + + `~${daysLeft} days left to index`); + startedAt = new Date().getTime(); indexedThisRun = 0; } - toTimestamp -= 86400; + toTimestamp -= 86400000; ++indexedThisRun; ++totalIndexed; } @@ -269,11 +326,14 @@ class Mining { }); } + newlyIndexed += hashrates.length; await HashratesRepository.$saveHashrates(hashrates); await HashratesRepository.$setLatestRunTimestamp('last_hashrates_indexing'); this.hashrateIndexingStarted = false; - logger.info(`Daily network hashrate indexing completed`); + if (newlyIndexed > 0) { + logger.info(`Indexed ${newlyIndexed} day of network hashrate`); + } } catch (e) { this.hashrateIndexingStarted = false; throw e; @@ -288,6 +348,21 @@ class Mining { return date; } + + private getTimeRange(interval: string | null): number { + switch (interval) { + case '3y': return 43200; // 12h + case '2y': return 28800; // 8h + case '1y': return 28800; // 8h + case '6m': return 10800; // 3h + case '3m': return 7200; // 2h + case '1m': return 1800; // 30min + case '1w': return 300; // 5min + case '3d': return 1; + case '24h': return 1; + default: return 86400; // 24h + } + } } export default new Mining(); diff --git a/backend/src/api/pools-parser.ts b/backend/src/api/pools-parser.ts index ff70c3cb9..202fca1bd 100644 --- a/backend/src/api/pools-parser.ts +++ b/backend/src/api/pools-parser.ts @@ -1,5 +1,4 @@ -import { readFileSync } from 'fs'; -import { DB } from '../database'; +import DB from '../database'; import logger from '../logger'; import config from '../config'; @@ -8,29 +7,20 @@ interface Pool { link: string; regexes: string[]; addresses: string[]; + slug: string; } class PoolsParser { + slugWarnFlag = false; + /** * Parse the pools.json file, consolidate the data and dump it into the database */ - public async migratePoolsJson() { + public async migratePoolsJson(poolsJson: object) { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { return; } - logger.debug('Importing pools.json to the database, open ./pools.json'); - - let poolsJson: object = {}; - try { - const fileContent: string = readFileSync('./pools.json', 'utf8'); - poolsJson = JSON.parse(fileContent); - } catch (e) { - logger.err('Unable to open ./pools.json, does the file exist?'); - await this.insertUnknownPool(); - return; - } - // First we save every entries without paying attention to pool duplication const poolsDuplicated: Pool[] = []; @@ -42,6 +32,7 @@ class PoolsParser { 'link': (coinbaseTags[i][1]).link, 'regexes': [coinbaseTags[i][0]], 'addresses': [], + 'slug': '' }); } logger.debug('Parse payout_addresses'); @@ -52,6 +43,7 @@ class PoolsParser { 'link': (addressesTags[i][1]).link, 'regexes': [], 'addresses': [addressesTags[i][0]], + 'slug': '' }); } @@ -66,13 +58,11 @@ class PoolsParser { logger.debug(`Found ${poolNames.length} unique mining pools`); // Get existing pools from the db - const connection = await DB.getConnection(); let existingPools; try { - [existingPools] = await connection.query({ sql: 'SELECT * FROM pools;', timeout: 120000 }); + [existingPools] = await DB.query({ sql: 'SELECT * FROM pools;', timeout: 120000 }); } catch (e) { - logger.err('Unable to get existing pools from the database, skipping pools.json import'); - connection.release(); + logger.err('Cannot get existing pools from the database, skipping pools.json import'); return; } @@ -91,13 +81,29 @@ class PoolsParser { const finalPoolName = poolNames[i].replace(`'`, `''`); // To support single quote in names when doing db queries + let slug: string | undefined; + try { + slug = poolsJson['slugs'][poolNames[i]]; + } catch (e) { + if (this.slugWarnFlag === false) { + logger.warn(`pools.json does not seem to contain the 'slugs' object`); + this.slugWarnFlag = true; + } + } + + if (slug === undefined) { + // Only keep alphanumerical + slug = poolNames[i].replace(/[^a-z0-9]/gi, '').toLowerCase(); + logger.warn(`No slug found for '${poolNames[i]}', generating it => '${slug}'`); + } + if (existingPools.find((pool) => pool.name === poolNames[i]) !== undefined) { - logger.debug(`Update '${finalPoolName}' mining pool`); finalPoolDataUpdate.push({ 'name': finalPoolName, 'link': match[0].link, 'regexes': allRegexes, 'addresses': allAddresses, + 'slug': slug }); } else { logger.debug(`Add '${finalPoolName}' mining pool`); @@ -106,6 +112,7 @@ class PoolsParser { 'link': match[0].link, 'regexes': allRegexes, 'addresses': allAddresses, + 'slug': slug }); } } @@ -113,10 +120,11 @@ class PoolsParser { logger.debug(`Update pools table now`); // Add new mining pools into the database - let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses) VALUES '; + let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES '; for (let i = 0; i < finalPoolDataAdd.length; ++i) { queryAdd += `('${finalPoolDataAdd[i].name}', '${finalPoolDataAdd[i].link}', - '${JSON.stringify(finalPoolDataAdd[i].regexes)}', '${JSON.stringify(finalPoolDataAdd[i].addresses)}'),`; + '${JSON.stringify(finalPoolDataAdd[i].regexes)}', '${JSON.stringify(finalPoolDataAdd[i].addresses)}', + ${JSON.stringify(finalPoolDataAdd[i].slug)}),`; } queryAdd = queryAdd.slice(0, -1) + ';'; @@ -126,24 +134,23 @@ class PoolsParser { updateQueries.push(` UPDATE pools SET name='${finalPoolDataUpdate[i].name}', link='${finalPoolDataUpdate[i].link}', - regexes='${JSON.stringify(finalPoolDataUpdate[i].regexes)}', addresses='${JSON.stringify(finalPoolDataUpdate[i].addresses)}' + regexes='${JSON.stringify(finalPoolDataUpdate[i].regexes)}', addresses='${JSON.stringify(finalPoolDataUpdate[i].addresses)}', + slug='${finalPoolDataUpdate[i].slug}' WHERE name='${finalPoolDataUpdate[i].name}' ;`); } try { if (finalPoolDataAdd.length > 0) { - await connection.query({ sql: queryAdd, timeout: 120000 }); + await DB.query({ sql: queryAdd, timeout: 120000 }); } for (const query of updateQueries) { - await connection.query({ sql: query, timeout: 120000 }); + await DB.query({ sql: query, timeout: 120000 }); } await this.insertUnknownPool(); - connection.release(); logger.info('Mining pools.json import completed'); } catch (e) { - connection.release(); - logger.err(`Unable to import pools in the database!`); + logger.err(`Cannot import pools in the database`); throw e; } } @@ -152,21 +159,24 @@ class PoolsParser { * Manually add the 'unknown pool' */ private async insertUnknownPool() { - const connection = await DB.getConnection(); try { - const [rows]: any[] = await connection.query({ sql: 'SELECT name from pools where name="Unknown"', timeout: 120000 }); + const [rows]: any[] = await DB.query({ sql: 'SELECT name from pools where name="Unknown"', timeout: 120000 }); if (rows.length === 0) { - logger.debug('Manually inserting "Unknown" mining pool into the databse'); - await connection.query({ - sql: `INSERT INTO pools(name, link, regexes, addresses) - VALUES("Unknown", "https://learnmeabitcoin.com/technical/coinbase-transaction", "[]", "[]"); + await DB.query({ + sql: `INSERT INTO pools(name, link, regexes, addresses, slug) + VALUES("Unknown", "https://learnmeabitcoin.com/technical/coinbase-transaction", "[]", "[]", "unknown"); `}); + } else { + await DB.query(`UPDATE pools + SET name='Unknown', link='https://learnmeabitcoin.com/technical/coinbase-transaction', + regexes='[]', addresses='[]', + slug='unknown' + WHERE name='Unknown' + `); } } catch (e) { logger.err('Unable to insert "Unknown" mining pool'); } - - connection.release(); } } diff --git a/backend/src/api/statistics.ts b/backend/src/api/statistics.ts index 3d99adcb7..bd93b4c6e 100644 --- a/backend/src/api/statistics.ts +++ b/backend/src/api/statistics.ts @@ -1,5 +1,5 @@ import memPool from './mempool'; -import { DB } from '../database'; +import DB from '../database'; import logger from '../logger'; import { Statistic, TransactionExtended, OptimizedStatistic } from '../mempool.interfaces'; @@ -155,7 +155,6 @@ class Statistics { } private async $createZeroedStatistic(): Promise { - const connection = await DB.getConnection(); try { const query = `INSERT INTO statistics( added, @@ -206,17 +205,14 @@ class Statistics { ) VALUES (NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)`; - const [result]: any = await connection.query(query); - connection.release(); + const [result]: any = await DB.query(query); return result.insertId; } catch (e) { - connection.release(); logger.err('$create() error' + (e instanceof Error ? e.message : e)); } } private async $create(statistics: Statistic): Promise { - const connection = await DB.getConnection(); try { const query = `INSERT INTO statistics( added, @@ -314,11 +310,9 @@ class Statistics { statistics.vsize_1800, statistics.vsize_2000, ]; - const [result]: any = await connection.query(query, params); - connection.release(); + const [result]: any = await DB.query(query, params); return result.insertId; } catch (e) { - connection.release(); logger.err('$create() error' + (e instanceof Error ? e.message : e)); } } @@ -421,10 +415,8 @@ class Statistics { private async $get(id: number): Promise { try { - const connection = await DB.getConnection(); const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics WHERE id = ?`; - const [rows] = await connection.query(query, [id]); - connection.release(); + const [rows] = await DB.query(query, [id]); if (rows[0]) { return this.mapStatisticToOptimizedStatistic([rows[0]])[0]; } @@ -435,11 +427,9 @@ class Statistics { public async $list2H(): Promise { try { - const connection = await DB.getConnection(); const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics ORDER BY statistics.added DESC LIMIT 120`; - const [rows] = await connection.query({ sql: query, timeout: this.queryTimeout }); - connection.release(); - return this.mapStatisticToOptimizedStatistic(rows); + const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); + return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list2H() error' + (e instanceof Error ? e.message : e)); return []; @@ -448,11 +438,9 @@ class Statistics { public async $list24H(): Promise { try { - const connection = await DB.getConnection(); const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics ORDER BY statistics.added DESC LIMIT 1440`; - const [rows] = await connection.query({ sql: query, timeout: this.queryTimeout }); - connection.release(); - return this.mapStatisticToOptimizedStatistic(rows); + const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); + return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list24h() error' + (e instanceof Error ? e.message : e)); return []; @@ -461,11 +449,9 @@ class Statistics { public async $list1W(): Promise { try { - const connection = await DB.getConnection(); const query = this.getQueryForDaysAvg(300, '1 WEEK'); // 5m interval - const [rows] = await connection.query({ sql: query, timeout: this.queryTimeout }); - connection.release(); - return this.mapStatisticToOptimizedStatistic(rows); + const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); + return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list1W() error' + (e instanceof Error ? e.message : e)); return []; @@ -474,11 +460,9 @@ class Statistics { public async $list1M(): Promise { try { - const connection = await DB.getConnection(); const query = this.getQueryForDaysAvg(1800, '1 MONTH'); // 30m interval - const [rows] = await connection.query({ sql: query, timeout: this.queryTimeout }); - connection.release(); - return this.mapStatisticToOptimizedStatistic(rows); + const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); + return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list1M() error' + (e instanceof Error ? e.message : e)); return []; @@ -487,11 +471,9 @@ class Statistics { public async $list3M(): Promise { try { - const connection = await DB.getConnection(); const query = this.getQueryForDaysAvg(7200, '3 MONTH'); // 2h interval - const [rows] = await connection.query({ sql: query, timeout: this.queryTimeout }); - connection.release(); - return this.mapStatisticToOptimizedStatistic(rows); + const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); + return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list3M() error' + (e instanceof Error ? e.message : e)); return []; @@ -500,11 +482,9 @@ class Statistics { public async $list6M(): Promise { try { - const connection = await DB.getConnection(); - const query = this.getQueryForDaysAvg(10800, '6 MONTH'); // 3h interval - const [rows] = await connection.query({ sql: query, timeout: this.queryTimeout }); - connection.release(); - return this.mapStatisticToOptimizedStatistic(rows); + const query = this.getQueryForDaysAvg(10800, '6 MONTH'); // 3h interval + const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); + return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list6M() error' + (e instanceof Error ? e.message : e)); return []; @@ -513,11 +493,9 @@ class Statistics { public async $list1Y(): Promise { try { - const connection = await DB.getConnection(); const query = this.getQueryForDays(28800, '1 YEAR'); // 8h interval - const [rows] = await connection.query({ sql: query, timeout: this.queryTimeout }); - connection.release(); - return this.mapStatisticToOptimizedStatistic(rows); + const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); + return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list1Y() error' + (e instanceof Error ? e.message : e)); return []; @@ -526,11 +504,9 @@ class Statistics { public async $list2Y(): Promise { try { - const connection = await DB.getConnection(); - const query = this.getQueryForDays(28800, "2 YEAR"); // 8h interval - const [rows] = await connection.query({ sql: query, timeout: this.queryTimeout }); - connection.release(); - return this.mapStatisticToOptimizedStatistic(rows); + const query = this.getQueryForDays(28800, '2 YEAR'); // 8h interval + const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); + return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list2Y() error' + (e instanceof Error ? e.message : e)); return []; @@ -539,11 +515,9 @@ class Statistics { public async $list3Y(): Promise { try { - const connection = await DB.getConnection(); - const query = this.getQueryForDays(43200, "3 YEAR"); // 12h interval - const [rows] = await connection.query({ sql: query, timeout: this.queryTimeout }); - connection.release(); - return this.mapStatisticToOptimizedStatistic(rows); + const query = this.getQueryForDays(43200, '3 YEAR'); // 12h interval + const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); + return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list3Y() error' + (e instanceof Error ? e.message : e)); return []; diff --git a/backend/src/database.ts b/backend/src/database.ts index 55be0ffc3..3816154cd 100644 --- a/backend/src/database.ts +++ b/backend/src/database.ts @@ -1,51 +1,51 @@ import config from './config'; -import { createPool, PoolConnection } from 'mysql2/promise'; +import { createPool, Pool, PoolConnection } from 'mysql2/promise'; import logger from './logger'; import { PoolOptions } from 'mysql2/typings/mysql'; -export class DB { - static poolConfig = ():PoolOptions => { - let poolConfig:PoolOptions = { - port: config.DATABASE.PORT, - database: config.DATABASE.DATABASE, - user: config.DATABASE.USERNAME, - password: config.DATABASE.PASSWORD, - connectionLimit: 10, - supportBigNumbers: true, - timezone: '+00:00', - } - - if (config.DATABASE.SOCKET !== "") { - poolConfig.socketPath = config.DATABASE.SOCKET; + class DB { + constructor() { + if (config.DATABASE.SOCKET !== '') { + this.poolConfig.socketPath = config.DATABASE.SOCKET; } else { - poolConfig.host = config.DATABASE.HOST; + this.poolConfig.host = config.DATABASE.HOST; } - - return poolConfig; } - - static pool = createPool(DB.poolConfig()); + private pool: Pool | null = null; + private poolConfig: PoolOptions = { + port: config.DATABASE.PORT, + database: config.DATABASE.DATABASE, + user: config.DATABASE.USERNAME, + password: config.DATABASE.PASSWORD, + connectionLimit: 10, + supportBigNumbers: true, + timezone: '+00:00', + }; - static connectionsReady: number[] = []; + public async query(query, params?) { + const pool = await this.getPool(); + return pool.query(query, params); + } - static async getConnection() { - const connection: PoolConnection = await DB.pool.getConnection(); - const connectionId = connection['connection'].connectionId; - if (!DB.connectionsReady.includes(connectionId)) { - await connection.query(`SET time_zone='+00:00';`); - this.connectionsReady.push(connectionId); + public async checkDbConnection() { + try { + await this.query('SELECT ?', [1]); + logger.info('Database connection established.'); + } catch (e) { + logger.err('Could not connect to database: ' + (e instanceof Error ? e.message : e)); + process.exit(1); } - return connection; + } + + private async getPool(): Promise { + if (this.pool === null) { + this.pool = createPool(this.poolConfig); + this.pool.on('connection', function (newConnection: PoolConnection) { + newConnection.query(`SET time_zone='+00:00'`); + }); + } + return this.pool; } } -export async function checkDbConnection() { - try { - const connection = await DB.getConnection(); - logger.info('Database connection established.'); - connection.release(); - } catch (e) { - logger.err('Could not connect to database: ' + (e instanceof Error ? e.message : e)); - process.exit(1); - } -} +export default new DB(); diff --git a/backend/src/index.ts b/backend/src/index.ts index d5bf0e59e..c2f111c65 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -5,7 +5,7 @@ import * as WebSocket from 'ws'; import * as cluster from 'cluster'; import axios from 'axios'; -import { checkDbConnection, DB } from './database'; +import DB from './database'; import config from './config'; import routes from './routes'; import blocks from './api/blocks'; @@ -22,12 +22,13 @@ import loadingIndicators from './api/loading-indicators'; import mempool from './api/mempool'; import elementsParser from './api/liquid/elements-parser'; import databaseMigration from './api/database-migration'; -import poolsParser from './api/pools-parser'; import syncAssets from './sync-assets'; import icons from './api/liquid/icons'; import { Common } from './api/common'; import mining from './api/mining'; import HashratesRepository from './repositories/HashratesRepository'; +import BlocksRepository from './repositories/BlocksRepository'; +import poolsUpdater from './tasks/pools-updater'; class Server { private wss: WebSocket.Server | undefined; @@ -88,18 +89,17 @@ class Server { diskCache.loadMempoolCache(); if (config.DATABASE.ENABLED) { - await checkDbConnection(); + await DB.checkDbConnection(); try { if (process.env.npm_config_reindex != undefined) { // Re-index requests const tables = process.env.npm_config_reindex.split(','); - logger.warn(`Indexed data for "${process.env.npm_config_reindex}" tables will be erased in 5 seconds from now (using '--reindex') ...`); + logger.warn(`Indexed data for "${process.env.npm_config_reindex}" tables will be erased in 5 seconds (using '--reindex')`); await Common.sleep(5000); await databaseMigration.$truncateIndexedData(tables); } await databaseMigration.$initializeOrMigrateDatabase(); if (Common.indexingEnabled()) { await this.$resetHashratesIndexingState(); - await poolsParser.migratePoolsJson(); } } catch (e) { throw new Error(e instanceof Error ? e.message : 'Error'); @@ -169,8 +169,12 @@ class Server { } async $resetHashratesIndexingState() { - await HashratesRepository.$setLatestRunTimestamp('last_hashrates_indexing', 0); - await HashratesRepository.$setLatestRunTimestamp('last_weekly_hashrates_indexing', 0); + try { + await HashratesRepository.$setLatestRunTimestamp('last_hashrates_indexing', 0); + await HashratesRepository.$setLatestRunTimestamp('last_weekly_hashrates_indexing', 0); + } catch (e) { + logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e)); + } } async $runIndexingWhenReady() { @@ -179,11 +183,16 @@ class Server { } try { - blocks.$generateBlockDatabase(); + await poolsUpdater.updatePoolsJson(); + if (blocks.reindexFlag) { + await BlocksRepository.$deleteBlocks(10); + await HashratesRepository.$deleteLastEntries(); + } + await blocks.$generateBlockDatabase(); await mining.$generateNetworkHashrateHistory(); await mining.$generatePoolHashrateHistory(); } catch (e) { - logger.err(`Unable to run indexing right now, trying again later. ` + e); + logger.err(`Indexing failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e)); } } @@ -301,18 +310,18 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/2y', routes.$getPools.bind(routes, '2y')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3y', routes.$getPools.bind(routes, '3y')) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/all', routes.$getPools.bind(routes, 'all')) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/hashrate', routes.$getPoolHistoricalHashrate) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks', routes.$getPoolBlocks) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks/:height', routes.$getPoolBlocks) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty', routes.$getHistoricalDifficulty) - .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty/:interval', routes.$getHistoricalDifficulty) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', routes.$getPoolHistoricalHashrate) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', routes.$getPoolBlocks) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks/:height', routes.$getPoolBlocks) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', routes.$getPool) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/:interval', routes.$getPool) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools', routes.$getPoolsHistoricalHashrate) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate', routes.$getHistoricalHashrate) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', routes.$getHistoricalBlockRewards) ; } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 15d1ad618..0081bd34f 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -6,6 +6,7 @@ export interface PoolTag { link: string; regexes: string; // JSON array addresses: string; // JSON array + slug: string; } export interface PoolInfo { @@ -13,6 +14,7 @@ export interface PoolInfo { name: string; link: string; blockCount: number; + slug: string; } export interface PoolStats extends PoolInfo { @@ -87,6 +89,7 @@ export interface BlockExtension { pool?: { id: number; name: string; + slug: string; }; avgFee?: number; avgFeeRate?: number; diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 33cb727d9..8e96a0c38 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -1,16 +1,15 @@ -import { BlockExtended, PoolTag } from '../mempool.interfaces'; -import { DB } from '../database'; +import { BlockExtended } from '../mempool.interfaces'; +import DB from '../database'; import logger from '../logger'; import { Common } from '../api/common'; import { prepareBlock } from '../utils/blocks-utils'; +import PoolsRepository from './PoolsRepository'; class BlocksRepository { /** * Save indexed block data in the database */ public async $saveBlockInDatabase(block: BlockExtended) { - const connection = await DB.getConnection(); - try { const query = `INSERT INTO blocks( height, hash, blockTimestamp, size, @@ -49,15 +48,12 @@ class BlocksRepository { block.extras.avgFeeRate, ]; - await connection.query(query, params); - connection.release(); + await DB.query(query, params); } catch (e: any) { - connection.release(); - if (e.errno === 1062) { // ER_DUP_ENTRY + if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart logger.debug(`$saveBlockInDatabase() - Block ${block.height} has already been indexed, ignoring`); } else { - connection.release(); - logger.err('$saveBlockInDatabase() error: ' + (e instanceof Error ? e.message : e)); + logger.err('Cannot save indexed block into db. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -71,15 +67,13 @@ class BlocksRepository { return []; } - const connection = await DB.getConnection(); try { - const [rows]: any[] = await connection.query(` + const [rows]: any[] = await DB.query(` SELECT height FROM blocks WHERE height <= ? AND height >= ? ORDER BY height DESC; `, [startHeight, endHeight]); - connection.release(); const indexedBlockHeights: number[] = []; rows.forEach((row: any) => { indexedBlockHeights.push(row.height); }); @@ -88,8 +82,7 @@ class BlocksRepository { return missingBlocksHeights; } catch (e) { - connection.release(); - logger.err('$getMissingBlocksBetweenHeights() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot retrieve blocks list to index. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -117,15 +110,11 @@ class BlocksRepository { query += ` GROUP by pools.id`; - const connection = await DB.getConnection(); try { - const [rows] = await connection.query(query, params); - connection.release(); - + const [rows] = await DB.query(query, params); return rows; } catch (e) { - connection.release(); - logger.err('$getEmptyBlocks() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot count empty blocks. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -154,15 +143,11 @@ class BlocksRepository { query += ` blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; } - const connection = await DB.getConnection(); try { - const [rows] = await connection.query(query, params); - connection.release(); - + const [rows] = await DB.query(query, params); return rows[0].blockCount; } catch (e) { - connection.release(); - logger.err('$blockCount() error' + (e instanceof Error ? e.message : e)); + logger.err(`Cannot count blocks for this pool (using offset). Reason: ` + (e instanceof Error ? e.message : e)); throw e; } } @@ -193,15 +178,11 @@ class BlocksRepository { } query += ` blockTimestamp BETWEEN FROM_UNIXTIME('${from}') AND FROM_UNIXTIME('${to}')`; - const connection = await DB.getConnection(); try { - const [rows] = await connection.query(query, params); - connection.release(); - + const [rows] = await DB.query(query, params); return rows[0]; } catch (e) { - connection.release(); - logger.err('$blockCountBetweenTimestamp() error' + (e instanceof Error ? e.message : e)); + logger.err(`Cannot count blocks for this pool (using timestamps). Reason: ` + (e instanceof Error ? e.message : e)); throw e; } } @@ -215,10 +196,8 @@ class BlocksRepository { ORDER BY height LIMIT 1;`; - const connection = await DB.getConnection(); try { - const [rows]: any[] = await connection.query(query); - connection.release(); + const [rows]: any[] = await DB.query(query); if (rows.length <= 0) { return -1; @@ -226,8 +205,7 @@ class BlocksRepository { return rows[0].blockTimestamp; } catch (e) { - connection.release(); - logger.err('$oldestBlockTimestamp() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot get oldest indexed block timestamp. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -235,13 +213,18 @@ class BlocksRepository { /** * Get blocks mined by a specific mining pool */ - public async $getBlocksByPool(poolId: number, startHeight: number | undefined = undefined): Promise { + public async $getBlocksByPool(slug: string, startHeight?: number): Promise { + const pool = await PoolsRepository.$getPool(slug); + if (!pool) { + throw new Error(`This mining pool does not exist`); + } + const params: any[] = []; let query = ` SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, previous_block_hash as previousblockhash FROM blocks WHERE pool_id = ?`; - params.push(poolId); + params.push(pool.id); if (startHeight !== undefined) { query += ` AND height < ?`; @@ -251,20 +234,17 @@ class BlocksRepository { query += ` ORDER BY height DESC LIMIT 10`; - const connection = await DB.getConnection(); try { - const [rows] = await connection.query(query, params); - connection.release(); + const [rows] = await DB.query(query, params); const blocks: BlockExtended[] = []; - for (let block of rows) { + for (const block of rows) { blocks.push(prepareBlock(block)); } return blocks; } catch (e) { - connection.release(); - logger.err('$getBlocksByPool() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot get blocks for this pool. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -273,18 +253,16 @@ class BlocksRepository { * Get one block by height */ public async $getBlockByHeight(height: number): Promise { - const connection = await DB.getConnection(); try { - const [rows]: any[] = await connection.query(` + const [rows]: any[] = await DB.query(` SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, - pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, + pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.slug as pool_slug, pools.addresses as pool_addresses, pools.regexes as pool_regexes, previous_block_hash as previousblockhash FROM blocks JOIN pools ON blocks.pool_id = pools.id WHERE height = ${height}; `); - connection.release(); if (rows.length <= 0) { return null; @@ -292,8 +270,7 @@ class BlocksRepository { return rows[0]; } catch (e) { - connection.release(); - logger.err('$getBlockByHeight() error' + (e instanceof Error ? e.message : e)); + logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e)); throw e; } } @@ -304,8 +281,6 @@ class BlocksRepository { public async $getBlocksDifficulty(interval: string | null): Promise { interval = Common.getSqlInterval(interval); - const connection = await DB.getConnection(); - // :D ... Yeah don't ask me about this one https://stackoverflow.com/a/40303162 // Basically, using temporary user defined fields, we are able to extract all // difficulty adjustments from the blocks tables. @@ -339,34 +314,15 @@ class BlocksRepository { `; try { - const [rows]: any[] = await connection.query(query); - connection.release(); + const [rows]: any[] = await DB.query(query); - for (let row of rows) { + for (const row of rows) { delete row['rn']; } return rows; } catch (e) { - connection.release(); - logger.err('$getBlocksDifficulty() error' + (e instanceof Error ? e.message : e)); - throw e; - } - } - - /** - * Return oldest blocks height - */ - public async $getOldestIndexedBlockHeight(): Promise { - const connection = await DB.getConnection(); - try { - const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`); - connection.release(); - - return rows[0].minHeight; - } catch (e) { - connection.release(); - logger.err('$getOldestIndexedBlockHeight() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot generate difficulty history. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -375,21 +331,99 @@ class BlocksRepository { * Get general block stats */ public async $getBlockStats(blockCount: number): Promise { - let connection; try { - connection = await DB.getConnection(); - // We need to use a subquery - const query = `SELECT SUM(reward) as totalReward, SUM(fees) as totalFee, SUM(tx_count) as totalTx - FROM (SELECT reward, fees, tx_count FROM blocks ORDER by height DESC LIMIT ${blockCount}) as sub`; + const query = ` + SELECT MIN(height) as startBlock, MAX(height) as endBlock, SUM(reward) as totalReward, SUM(fees) as totalFee, SUM(tx_count) as totalTx + FROM + (SELECT height, reward, fees, tx_count FROM blocks + ORDER by height DESC + LIMIT ?) as sub`; + + const [rows]: any = await DB.query(query, [blockCount]); - const [rows]: any = await connection.query(query); - connection.release(); - return rows[0]; } catch (e) { - connection.release(); - logger.err('$getBlockStats() error: ' + (e instanceof Error ? e.message : e)); + logger.err('Cannot generate reward stats. Reason: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + + /* + * Check if the last 10 blocks chain is valid + */ + public async $validateRecentBlocks(): Promise { + try { + const [lastBlocks]: any[] = await DB.query(`SELECT height, hash, previous_block_hash FROM blocks ORDER BY height DESC LIMIT 10`); + + for (let i = 0; i < lastBlocks.length - 1; ++i) { + if (lastBlocks[i].previous_block_hash !== lastBlocks[i + 1].hash) { + logger.warn(`Chain divergence detected at block ${lastBlocks[i].height}, re-indexing most recent data`); + return false; + } + } + + return true; + } catch (e) { + return true; // Don't do anything if there is a db error + } + } + + /** + * Delete $count blocks from the database + */ + public async $deleteBlocks(count: number) { + logger.info(`Delete ${count} most recent indexed blocks from the database`); + + try { + await DB.query(`DELETE FROM blocks ORDER BY height DESC LIMIT ${count};`); + } catch (e) { + logger.err('Cannot delete recent indexed blocks. Reason: ' + (e instanceof Error ? e.message : e)); + } + } + + /** + * Get the historical averaged block fees + */ + public async $getHistoricalBlockFees(div: number, interval: string | null): Promise { + try { + let query = `SELECT CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp, + CAST(AVG(fees) as INT) as avg_fees + FROM blocks`; + + if (interval !== null) { + query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; + } + + query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`; + + const [rows]: any = await DB.query(query); + return rows; + } catch (e) { + logger.err('Cannot generate block fees history. Reason: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + + /** + * Get the historical averaged block rewards + */ + public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise { + try { + let query = `SELECT CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp, + CAST(AVG(reward) as INT) as avg_rewards + FROM blocks`; + + if (interval !== null) { + query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; + } + + query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`; + + const [rows]: any = await DB.query(query); + return rows; + } catch (e) { + logger.err('Cannot generate block rewards history. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } diff --git a/backend/src/repositories/HashratesRepository.ts b/backend/src/repositories/HashratesRepository.ts index 5237e6cb7..8388b9122 100644 --- a/backend/src/repositories/HashratesRepository.ts +++ b/backend/src/repositories/HashratesRepository.ts @@ -1,5 +1,5 @@ import { Common } from '../api/common'; -import { DB } from '../database'; +import DB from '../database'; import logger from '../logger'; import PoolsRepository from './PoolsRepository'; @@ -20,14 +20,10 @@ class HashratesRepository { } query = query.slice(0, -1); - let connection; try { - connection = await DB.getConnection(); - await connection.query(query); - connection.release(); + await DB.query(query); } catch (e: any) { - connection.release(); - logger.err('$saveHashrateInDatabase() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot save indexed hashrate into db. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -35,8 +31,6 @@ class HashratesRepository { public async $getNetworkDailyHashrate(interval: string | null): Promise { interval = Common.getSqlInterval(interval); - const connection = await DB.getConnection(); - let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate FROM hashrates`; @@ -50,33 +44,25 @@ class HashratesRepository { query += ` ORDER by hashrate_timestamp`; try { - const [rows]: any[] = await connection.query(query); - connection.release(); - + const [rows]: any[] = await DB.query(query); return rows; } catch (e) { - connection.release(); - logger.err('$getNetworkDailyHashrate() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } public async $getWeeklyHashrateTimestamps(): Promise { - const connection = await DB.getConnection(); - const query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp FROM hashrates WHERE type = 'weekly' GROUP BY hashrate_timestamp`; try { - const [rows]: any[] = await connection.query(query); - connection.release(); - + const [rows]: any[] = await DB.query(query); return rows.map(row => row.timestamp); } catch (e) { - connection.release(); - logger.err('$getWeeklyHashrateTimestamps() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -87,7 +73,6 @@ class HashratesRepository { public async $getPoolsWeeklyHashrate(interval: string | null): Promise { interval = Common.getSqlInterval(interval); - const connection = await DB.getConnection(); const topPoolsId = (await PoolsRepository.$getPoolsInfo('1w')).map((pool) => pool.poolId); let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate, share, pools.name as poolName @@ -106,13 +91,10 @@ class HashratesRepository { query += ` ORDER by hashrate_timestamp, FIELD(pool_id, ${topPoolsId})`; try { - const [rows]: any[] = await connection.query(query); - connection.release(); - + const [rows]: any[] = await DB.query(query); return rows; } catch (e) { - connection.release(); - logger.err('$getPoolsWeeklyHashrate() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot fetch weekly pools hashrate history. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -120,13 +102,16 @@ class HashratesRepository { /** * Returns a pool hashrate history */ - public async $getPoolWeeklyHashrate(poolId: number): Promise { - const connection = await DB.getConnection(); + public async $getPoolWeeklyHashrate(slug: string): Promise { + const pool = await PoolsRepository.$getPool(slug); + if (!pool) { + throw new Error(`This mining pool does not exist`); + } // Find hashrate boundaries let query = `SELECT MIN(hashrate_timestamp) as firstTimestamp, MAX(hashrate_timestamp) as lastTimestamp - FROM hashrates - JOIN pools on pools.id = pool_id + FROM hashrates + JOIN pools on pools.id = pool_id WHERE hashrates.type = 'weekly' AND pool_id = ? AND avg_hashrate != 0 ORDER by hashrate_timestamp LIMIT 1`; @@ -134,13 +119,12 @@ class HashratesRepository { firstTimestamp: '1970-01-01', lastTimestamp: '9999-01-01' }; + try { - const [rows]: any[] = await connection.query(query, [poolId]); + const [rows]: any[] = await DB.query(query, [pool.id]); boundaries = rows[0]; - connection.release(); } catch (e) { - connection.release(); - logger.err('$getPoolWeeklyHashrate() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot fetch hashrate start/end timestamps for this pool. Reason: ' + (e instanceof Error ? e.message : e)); } // Get hashrates entries between boundaries @@ -152,47 +136,65 @@ class HashratesRepository { ORDER by hashrate_timestamp`; try { - const [rows]: any[] = await connection.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, poolId]); - connection.release(); - + const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]); return rows; } catch (e) { - connection.release(); - logger.err('$getPoolWeeklyHashrate() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot fetch pool hashrate history for this pool. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } + /** + * Set latest run timestamp + */ public async $setLatestRunTimestamp(key: string, val: any = null) { - const connection = await DB.getConnection(); const query = `UPDATE state SET number = ? WHERE name = ?`; try { - await connection.query(query, (val === null) ? [Math.round(new Date().getTime() / 1000), key] : [val, key]); - connection.release(); + await DB.query(query, (val === null) ? [Math.round(new Date().getTime() / 1000), key] : [val, key]); } catch (e) { - connection.release(); + logger.err(`Cannot set last indexing timestamp for ${key}. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; } } + /** + * Get latest run timestamp + */ public async $getLatestRunTimestamp(key: string): Promise { - const connection = await DB.getConnection(); const query = `SELECT number FROM state WHERE name = ?`; try { - const [rows] = await connection.query(query, [key]); - connection.release(); + const [rows]: any[] = await DB.query(query, [key]); if (rows.length === 0) { return 0; } return rows[0]['number']; } catch (e) { - connection.release(); - logger.err('$setLatestRunTimestamp() error' + (e instanceof Error ? e.message : e)); + logger.err(`Cannot retreive last indexing timestamp for ${key}. Reason: ` + (e instanceof Error ? e.message : e)); throw e; } } + + /** + * Delete most recent data points for re-indexing + */ + public async $deleteLastEntries() { + logger.info(`Delete latest hashrates data points from the database`); + + try { + const [rows]: any[] = await DB.query(`SELECT MAX(hashrate_timestamp) as timestamp FROM hashrates GROUP BY type`); + for (const row of rows) { + await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp = ?`, [row.timestamp]); + } + // Re-run the hashrate indexing to fill up missing data + await this.$setLatestRunTimestamp('last_hashrates_indexing', 0); + await this.$setLatestRunTimestamp('last_weekly_hashrates_indexing', 0); + } catch (e) { + logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e)); + } + } } export default new HashratesRepository(); diff --git a/backend/src/repositories/PoolsRepository.ts b/backend/src/repositories/PoolsRepository.ts index 4c3fd67ce..037a6250a 100644 --- a/backend/src/repositories/PoolsRepository.ts +++ b/backend/src/repositories/PoolsRepository.ts @@ -1,5 +1,6 @@ import { Common } from '../api/common'; -import { DB } from '../database'; +import config from '../config'; +import DB from '../database'; import logger from '../logger'; import { PoolInfo, PoolTag } from '../mempool.interfaces'; @@ -8,9 +9,7 @@ class PoolsRepository { * Get all pools tagging info */ public async $getPools(): Promise { - const connection = await DB.getConnection(); - const [rows] = await connection.query('SELECT id, name, addresses, regexes FROM pools;'); - connection.release(); + const [rows] = await DB.query('SELECT id, name, addresses, regexes, slug FROM pools;'); return rows; } @@ -18,9 +17,7 @@ class PoolsRepository { * Get unknown pool tagging info */ public async $getUnknownPool(): Promise { - const connection = await DB.getConnection(); - const [rows] = await connection.query('SELECT id, name FROM pools where name = "Unknown"'); - connection.release(); + const [rows] = await DB.query('SELECT id, name, slug FROM pools where name = "Unknown"'); return rows[0]; } @@ -30,7 +27,7 @@ class PoolsRepository { public async $getPoolsInfo(interval: string | null = null): Promise { interval = Common.getSqlInterval(interval); - let query = `SELECT COUNT(height) as blockCount, pool_id as poolId, pools.name as name, pools.link as link + let query = `SELECT COUNT(height) as blockCount, pool_id as poolId, pools.name as name, pools.link as link, slug FROM blocks JOIN pools on pools.id = pool_id`; @@ -41,16 +38,11 @@ class PoolsRepository { query += ` GROUP BY pool_id ORDER BY COUNT(height) DESC`; - // logger.debug(query); - const connection = await DB.getConnection(); try { - const [rows] = await connection.query(query); - connection.release(); - + const [rows] = await DB.query(query); return rows; } catch (e) { - connection.release(); - logger.err('$getPoolsInfo() error' + (e instanceof Error ? e.message : e)); + logger.err(`Cannot generate pools stats. Reason: ` + (e instanceof Error ? e.message : e)); throw e; } } @@ -64,15 +56,11 @@ class PoolsRepository { LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?) GROUP BY pools.id`; - const connection = await DB.getConnection(); try { - const [rows] = await connection.query(query, [from, to]); - connection.release(); - + const [rows] = await DB.query(query, [from, to]); return rows; } catch (e) { - connection.release(); - logger.err('$getPoolsInfoBetween() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot generate pools blocks count. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -80,25 +68,30 @@ class PoolsRepository { /** * Get mining pool statistics for one pool */ - public async $getPool(poolId: any): Promise { + public async $getPool(slug: string): Promise { const query = ` SELECT * FROM pools - WHERE pools.id = ?`; + WHERE pools.slug = ?`; - // logger.debug(query); - const connection = await DB.getConnection(); try { - const [rows] = await connection.query(query, [poolId]); - connection.release(); + const [rows]: any[] = await DB.query(query, [slug]); + + if (rows.length < 1) { + logger.debug(`This slug does not match any known pool`); + return null; + } rows[0].regexes = JSON.parse(rows[0].regexes); - rows[0].addresses = JSON.parse(rows[0].addresses); + if (['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { + rows[0].addresses = []; // pools.json only contains mainnet addresses + } else { + rows[0].addresses = JSON.parse(rows[0].addresses); + } return rows[0]; } catch (e) { - connection.release(); - logger.err('$getPool() error' + (e instanceof Error ? e.message : e)); + logger.err('Cannot get pool from db. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } } diff --git a/backend/src/routes.ts b/backend/src/routes.ts index b14ea6ac4..9d4adb796 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -539,20 +539,24 @@ class Routes { public async $getPool(req: Request, res: Response) { try { - const stats = await mining.$getPoolStat(parseInt(req.params.poolId, 10)); + const stats = await mining.$getPoolStat(req.params.slug); res.header('Pragma', 'public'); res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(stats); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { + res.status(404).send(e.message); + } else { + res.status(500).send(e instanceof Error ? e.message : e); + } } } public async $getPoolBlocks(req: Request, res: Response) { try { const poolBlocks = await BlocksRepository.$getBlocksByPool( - parseInt(req.params.poolId, 10), + req.params.slug, req.params.height === undefined ? undefined : parseInt(req.params.height, 10), ); res.header('Pragma', 'public'); @@ -560,7 +564,11 @@ class Routes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(poolBlocks); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { + res.status(404).send(e.message); + } else { + res.status(500).send(e instanceof Error ? e.message : e); + } } } @@ -576,18 +584,6 @@ class Routes { } } - public async $getHistoricalDifficulty(req: Request, res: Response) { - try { - const stats = await BlocksRepository.$getBlocksDifficulty(req.params.interval ?? null); - res.header('Pragma', 'public'); - res.header('Cache-control', 'public'); - res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); - res.json(stats); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - public async $getPoolsHistoricalHashrate(req: Request, res: Response) { try { const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval ?? null); @@ -606,7 +602,7 @@ class Routes { public async $getPoolHistoricalHashrate(req: Request, res: Response) { try { - const hashrates = await HashratesRepository.$getPoolWeeklyHashrate(parseInt(req.params.poolId, 10)); + const hashrates = await HashratesRepository.$getPoolWeeklyHashrate(req.params.slug); const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp(); res.header('Pragma', 'public'); res.header('Cache-control', 'public'); @@ -616,7 +612,11 @@ class Routes { hashrates: hashrates, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { + res.status(404).send(e.message); + } else { + res.status(500).send(e instanceof Error ? e.message : e); + } } } @@ -638,6 +638,38 @@ class Routes { } } + public async $getHistoricalBlockFees(req: Request, res: Response) { + try { + const blockFees = await mining.$getHistoricalBlockFees(req.params.interval ?? null); + const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); + res.json({ + oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp, + blockFees: blockFees, + }); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + public async $getHistoricalBlockRewards(req: Request, res: Response) { + try { + const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval ?? null); + const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); + res.json({ + oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp, + blockRewards: blockRewards, + }); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + public async getBlock(req: Request, res: Response) { try { const result = await bitcoinApi.$getBlock(req.params.hash); diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts new file mode 100644 index 000000000..aee786ff9 --- /dev/null +++ b/backend/src/tasks/pools-updater.ts @@ -0,0 +1,140 @@ +const https = require('https'); +import poolsParser from '../api/pools-parser'; +import config from '../config'; +import DB from '../database'; +import logger from '../logger'; + +/** + * Maintain the most recent version of pools.json + */ +class PoolsUpdater { + lastRun: number = 0; + + constructor() { + } + + public async updatePoolsJson() { + if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { + return; + } + + const oneWeek = 604800; + const oneDay = 86400; + + const now = new Date().getTime() / 1000; + if (now - this.lastRun < oneWeek) { // Execute the PoolsUpdate only once a week, or upon restart + return; + } + + this.lastRun = now; + + try { + const dbSha = await this.getShaFromDb(); + const githubSha = await this.fetchPoolsSha(); // Fetch pools.json sha from github + if (githubSha === undefined) { + return; + } + + logger.debug(`Pools.json sha | Current: ${dbSha} | Github: ${githubSha}`); + if (dbSha !== undefined && dbSha === githubSha) { + return; + } + + logger.warn('Pools.json is outdated, fetch latest from github'); + const poolsJson = await this.fetchPools(); + await poolsParser.migratePoolsJson(poolsJson); + await this.updateDBSha(githubSha); + logger.notice('PoolsUpdater completed'); + + } catch (e) { + this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week + logger.err('PoolsUpdater failed. Will try again in 24h. Reason: ' + (e instanceof Error ? e.message : e)); + } + } + + /** + * Fetch pools.json from github repo + */ + private async fetchPools(): Promise { + const response = await this.query('/repos/mempool/mining-pools/contents/pools.json'); + return JSON.parse(Buffer.from(response['content'], 'base64').toString('utf8')); + } + + /** + * Fetch our latest pools.json sha from the db + */ + private async updateDBSha(githubSha: string) { + try { + await DB.query('DELETE FROM state where name="pools_json_sha"'); + await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`); + } catch (e) { + logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e)); + return undefined; + } + } + + /** + * Fetch our latest pools.json sha from the db + */ + private async getShaFromDb(): Promise { + try { + const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); + return (rows.length > 0 ? rows[0].string : undefined); + } catch (e) { + logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e)); + return undefined; + } + } + + /** + * Fetch our latest pools.json sha from github + */ + private async fetchPoolsSha(): Promise { + const response = await this.query('/repos/mempool/mining-pools/git/trees/master'); + + for (const file of response['tree']) { + if (file['path'] === 'pools.json') { + return file['sha']; + } + } + + logger.err('Cannot to find latest pools.json sha from github api response'); + return undefined; + } + + /** + * Http request wrapper + */ + private query(path): Promise { + return new Promise((resolve, reject) => { + const options = { + host: 'api.github.com', + path: path, + method: 'GET', + headers: { 'user-agent': 'node.js' } + }; + + logger.debug('Querying: api.github.com' + path); + + const request = https.get(options, (response) => { + const chunks_of_data: any[] = []; + response.on('data', (fragments) => { + chunks_of_data.push(fragments); + }); + response.on('end', () => { + resolve(JSON.parse(Buffer.concat(chunks_of_data).toString())); + }); + response.on('error', (error) => { + reject(error); + }); + }); + + request.on('error', (error) => { + logger.err('Github API query failed. Reason: ' + error); + reject(error); + }); + }); + } +} + +export default new PoolsUpdater(); diff --git a/backend/src/utils/blocks-utils.ts b/backend/src/utils/blocks-utils.ts index 107099ba3..7b5c0b23a 100644 --- a/backend/src/utils/blocks-utils.ts +++ b/backend/src/utils/blocks-utils.ts @@ -23,6 +23,7 @@ export function prepareBlock(block: any): BlockExtended { pool: block?.extras?.pool ?? (block?.pool_id ? { id: block.pool_id, name: block.pool_name, + slug: block.pool_slug, } : undefined), } }; diff --git a/contributors/TechMiX.txt b/contributors/TechMiX.txt new file mode 100644 index 000000000..e6a382eae --- /dev/null +++ b/contributors/TechMiX.txt @@ -0,0 +1,3 @@ +I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022. + +Signed: TechMiX diff --git a/contributors/naveensrinivasan.txt b/contributors/naveensrinivasan.txt new file mode 100644 index 000000000..84cdbdcdd --- /dev/null +++ b/contributors/naveensrinivasan.txt @@ -0,0 +1,3 @@ +I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of March 11, 2022. + +Signed: naveensrinivasan diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index ceddc9000..c013fc23a 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -1,5 +1,8 @@ FROM node:16.10.0-buster-slim AS builder +ARG commitHash +ENV DOCKER_COMMIT_HASH=${commitHash} + WORKDIR /build COPY . . diff --git a/frontend/generate-config.js b/frontend/generate-config.js index 617ab3c0e..1f37953b7 100644 --- a/frontend/generate-config.js +++ b/frontend/generate-config.js @@ -51,9 +51,9 @@ if (process.env.DOCKER_COMMIT_HASH) { } else { try { const gitRevParse = spawnSync('git', ['rev-parse', '--short', 'HEAD']); - if (!gitRevParse.error) { - gitCommitHash = gitRevParse.stdout.toString('utf-8').replace(/[\n\r\s]+$/, ''); + const output = gitRevParse.stdout.toString('utf-8').replace(/[\n\r\s]+$/, ''); + gitCommitHash = output ? output : '?'; console.log(`mempool revision ${gitCommitHash}`); } else if (gitRevParse.error.code === 'ENOENT') { console.log('git not found, cannot parse git hash'); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 59f2fb57d..fc01e783d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3895,6 +3895,12 @@ "node": ">= 0.6.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz", + "integrity": "sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q==", + "dev": true + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -4416,12 +4422,6 @@ "node": ">=8.9" } }, - "node_modules/after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", - "dev": true - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -4670,12 +4670,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", - "dev": true - }, "node_modules/asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -5009,15 +5003,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, - "node_modules/base64-arraybuffer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5085,12 +5070,6 @@ "node": ">= 6" } }, - "node_modules/blob": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", - "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", - "dev": true - }, "node_modules/blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -5271,13 +5250,13 @@ } }, "node_modules/browser-sync": { - "version": "2.27.7", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.7.tgz", - "integrity": "sha512-9ElnnA/u+s2Jd+IgY+2SImB+sAEIteHsMG0NR96m7Ph/wztpvJCUpyC2on1KqmG9iAp941j+5jfmd34tEguGbg==", + "version": "2.27.9", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.9.tgz", + "integrity": "sha512-3zBtggcaZIeU9so4ja9yxk7/CZu9B3DOL6zkxFpzHCHsQmkGBPVXg61jItbeoa+WXgNLnr1sYES/2yQwyEZ2+w==", "dev": true, "dependencies": { - "browser-sync-client": "^2.27.7", - "browser-sync-ui": "^2.27.7", + "browser-sync-client": "^2.27.9", + "browser-sync-ui": "^2.27.9", "bs-recipes": "1.3.4", "bs-snippet-injector": "^2.0.1", "chokidar": "^3.5.1", @@ -5303,9 +5282,9 @@ "serve-index": "1.9.1", "serve-static": "1.13.2", "server-destroy": "1.0.1", - "socket.io": "2.4.0", + "socket.io": "^4.4.1", "ua-parser-js": "1.0.2", - "yargs": "^15.4.1" + "yargs": "^17.3.1" }, "bin": { "browser-sync": "dist/bin.js" @@ -5315,9 +5294,9 @@ } }, "node_modules/browser-sync-client": { - "version": "2.27.7", - "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.7.tgz", - "integrity": "sha512-wKg9UP9a4sCIkBBAXUdbkdWFJzfSAQizGh+nC19W9y9zOo9s5jqeYRFUUbs7x5WKhjtspT+xetVp9AtBJ6BmWg==", + "version": "2.27.9", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.9.tgz", + "integrity": "sha512-FHW8kydp7FXo6jnX3gXJCpHAHtWNLK0nx839nnK+boMfMI1n4KZd0+DmTxHBsHsF3OHud4V4jwoN8U5HExMIdQ==", "dev": true, "dependencies": { "etag": "1.8.1", @@ -5351,19 +5330,63 @@ } }, "node_modules/browser-sync-ui": { - "version": "2.27.7", - "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.7.tgz", - "integrity": "sha512-Bt4OQpx9p18OIzk0KKyu7jqlvmjacasUlk8ARY3uuIyiFWSBiRgr2i6XY8dEMF14DtbooaEBOpHEu9VCYvMcCw==", + "version": "2.27.9", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.9.tgz", + "integrity": "sha512-rsduR2bRIwFvM8CX6iY/Nu5aWub0WB9zfSYg9Le/RV5N5DEyxJYey0VxdfWCnzDOoelassTDzYQo+r0iJno3qw==", "dev": true, "dependencies": { "async-each-series": "0.1.1", "connect-history-api-fallback": "^1", "immutable": "^3", "server-destroy": "1.0.1", - "socket.io-client": "^2.4.0", + "socket.io-client": "^4.4.1", "stream-throttle": "^0.1.3" } }, + "node_modules/browser-sync/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/browser-sync/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/browser-sync/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/browser-sync/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/browser-sync/node_modules/fs-extra": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", @@ -5384,26 +5407,57 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/browser-sync/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "node_modules/browser-sync/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/browser-sync/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/browser-sync/node_modules/yargs": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.0.tgz", + "integrity": "sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/browser-sync/node_modules/yargs-parser": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "dev": true, + "engines": { + "node": ">=12" } }, "node_modules/browser-unpack": { @@ -6248,24 +6302,12 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, - "node_modules/component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", - "dev": true - }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "devOptional": true }, - "node_modules/component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", - "dev": true - }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -7824,85 +7866,104 @@ } }, "node_modules/engine.io": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", - "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==", - "dev": true, + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.3.tgz", + "integrity": "sha512-rqs60YwkvWTLLnfazqgZqLa/aKo+9cueVfEi/dZ8PyGyaf8TLOxj++4QMIgeG3Gn0AhrWiFXvghsoY9L9h25GA==", + "devOptional": true, "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.4.1", - "debug": "~4.1.0", - "engine.io-parser": "~2.2.0", - "ws": "~7.4.2" + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" }, "engines": { - "node": ">=8.0.0" + "node": ">=10.0.0" } }, "node_modules/engine.io-client": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", - "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.1.1.tgz", + "integrity": "sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==", "dev": true, "dependencies": { - "component-emitter": "~1.3.0", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.2.0", + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", "has-cors": "1.1.0", - "indexof": "0.0.1", "parseqs": "0.0.6", "parseuri": "0.0.6", - "ws": "~7.4.2", - "xmlhttprequest-ssl": "~1.6.2", + "ws": "~8.2.3", + "xmlhttprequest-ssl": "~2.0.0", "yeast": "0.1.2" } }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "dev": true, - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "node_modules/engine.io-client/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "node_modules/engine.io-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", - "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", - "dev": true, + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", + "devOptional": true, "dependencies": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.4", - "blob": "0.0.5", - "has-binary2": "~1.0.2" + "@socket.io/base64-arraybuffer": "~1.0.2" + }, + "engines": { + "node": ">=10.0.0" } }, "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "dev": true, + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "devOptional": true, "engines": { "node": ">= 0.6" } }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" + "node_modules/engine.io/node_modules/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "devOptional": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/enhanced-resolve": { @@ -9506,21 +9567,6 @@ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" }, - "node_modules/has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "dev": true, - "dependencies": { - "isarray": "2.0.1" - } - }, - "node_modules/has-binary2/node_modules/isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - }, "node_modules/has-cors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", @@ -9882,12 +9928,6 @@ "node": ">=8" } }, - "node_modules/indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, "node_modules/infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -10935,48 +10975,6 @@ "ms": "2.0.0" } }, - "node_modules/karma/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "devOptional": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/karma/node_modules/engine.io": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.3.tgz", - "integrity": "sha512-rqs60YwkvWTLLnfazqgZqLa/aKo+9cueVfEi/dZ8PyGyaf8TLOxj++4QMIgeG3Gn0AhrWiFXvghsoY9L9h25GA==", - "devOptional": true, - "dependencies": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.2.3" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/karma/node_modules/engine.io-parser": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", - "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", - "devOptional": true, - "dependencies": { - "@socket.io/base64-arraybuffer": "~1.0.2" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/karma/node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -11010,43 +11008,6 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "devOptional": true }, - "node_modules/karma/node_modules/socket.io": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", - "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", - "devOptional": true, - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "debug": "~4.3.2", - "engine.io": "~6.1.0", - "socket.io-adapter": "~2.3.3", - "socket.io-parser": "~4.0.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/karma/node_modules/socket.io-adapter": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", - "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==", - "devOptional": true - }, - "node_modules/karma/node_modules/socket.io-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", - "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", - "devOptional": true, - "dependencies": { - "@types/component-emitter": "^1.2.10", - "component-emitter": "~1.3.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/karma/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -11087,27 +11048,6 @@ "node": "*" } }, - "node_modules/karma/node_modules/ws": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", - "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -12057,9 +11997,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "node_modules/minipass": { "version": "3.1.6", @@ -12482,9 +12422,9 @@ "optional": true }, "node_modules/node-forge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", - "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", + "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==", "engines": { "node": ">= 6.13.0" } @@ -15125,115 +15065,70 @@ } }, "node_modules/socket.io": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.0.tgz", - "integrity": "sha512-9UPJ1UTvKayuQfVv2IQ3k7tCQC/fboDyIK62i99dAQIyHKaBsNdTpwHLgKJ6guRWxRtC9H+138UwpaGuQO9uWQ==", - "dev": true, + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", + "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", + "devOptional": true, "dependencies": { - "debug": "~4.1.0", - "engine.io": "~3.5.0", - "has-binary2": "~1.0.2", - "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.4.0", - "socket.io-parser": "~3.4.0" + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" + }, + "engines": { + "node": ">=10.0.0" } }, "node_modules/socket.io-adapter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", - "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==", - "dev": true + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==", + "devOptional": true }, "node_modules/socket.io-client": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", - "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.4.1.tgz", + "integrity": "sha512-N5C/L5fLNha5Ojd7Yeb/puKcPWWcoB/A09fEjjNsg91EDVr5twk/OEyO6VT9dlLSUNY85NpW6KBhVMvaLKQ3vQ==", "dev": true, "dependencies": { - "backo2": "1.0.2", - "component-bind": "1.0.0", - "component-emitter": "~1.3.0", - "debug": "~3.1.0", - "engine.io-client": "~3.5.0", - "has-binary2": "~1.0.2", - "indexof": "0.0.1", - "parseqs": "0.0.6", + "@socket.io/component-emitter": "~3.0.0", + "backo2": "~1.0.2", + "debug": "~4.3.2", + "engine.io-client": "~6.1.1", "parseuri": "0.0.6", - "socket.io-parser": "~3.3.0", - "to-array": "0.1.4" + "socket.io-parser": "~4.1.1" + }, + "engines": { + "node": ">=10.0.0" } }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/socket.io-client/node_modules/isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - }, - "node_modules/socket.io-client/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "node_modules/socket.io-client/node_modules/socket.io-parser": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", - "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.1.2.tgz", + "integrity": "sha512-j3kk71QLJuyQ/hh5F/L2t1goqzdTL0gvDzuhTuNSwihfuFUrcSji0qFZmJJPtG6Rmug153eOPsUizeirf1IIog==", "dev": true, "dependencies": { - "component-emitter": "~1.3.0", - "debug": "~3.1.0", - "isarray": "2.0.1" + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" } }, "node_modules/socket.io-parser": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", - "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", - "dev": true, + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "devOptional": true, "dependencies": { - "component-emitter": "1.2.1", - "debug": "~4.1.0", - "isarray": "2.0.1" - } - }, - "node_modules/socket.io-parser/node_modules/component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/socket.io-parser/node_modules/isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" } }, "node_modules/sockjs": { @@ -15948,12 +15843,6 @@ "node": ">=0.6.0" } }, - "node_modules/to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", - "dev": true - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -17206,9 +17095,9 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, "node_modules/xmlhttprequest-ssl": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.2.tgz", - "integrity": "sha512-tYOaldF/0BLfKuoA39QMwD4j2m8lq4DIncqj1yuNELX4vz9+z/ieG/vwmctjJce+boFHXstqhWnHSxc4W8f4qg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", "dev": true, "engines": { "node": ">=0.4.0" @@ -20081,6 +19970,12 @@ "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==", "devOptional": true }, + "@socket.io/component-emitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz", + "integrity": "sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q==", + "dev": true + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -20568,12 +20463,6 @@ "regex-parser": "^2.2.11" } }, - "after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", - "dev": true - }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -20780,12 +20669,6 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==" }, - "arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", - "dev": true - }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -21055,12 +20938,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, - "base64-arraybuffer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=", - "dev": true - }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -21118,12 +20995,6 @@ } } }, - "blob": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", - "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", - "dev": true - }, "blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -21290,13 +21161,13 @@ } }, "browser-sync": { - "version": "2.27.7", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.7.tgz", - "integrity": "sha512-9ElnnA/u+s2Jd+IgY+2SImB+sAEIteHsMG0NR96m7Ph/wztpvJCUpyC2on1KqmG9iAp941j+5jfmd34tEguGbg==", + "version": "2.27.9", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.9.tgz", + "integrity": "sha512-3zBtggcaZIeU9so4ja9yxk7/CZu9B3DOL6zkxFpzHCHsQmkGBPVXg61jItbeoa+WXgNLnr1sYES/2yQwyEZ2+w==", "dev": true, "requires": { - "browser-sync-client": "^2.27.7", - "browser-sync-ui": "^2.27.7", + "browser-sync-client": "^2.27.9", + "browser-sync-ui": "^2.27.9", "bs-recipes": "1.3.4", "bs-snippet-injector": "^2.0.1", "chokidar": "^3.5.1", @@ -21322,11 +21193,46 @@ "serve-index": "1.9.1", "serve-static": "1.13.2", "server-destroy": "1.0.1", - "socket.io": "2.4.0", + "socket.io": "^4.4.1", "ua-parser-js": "1.0.2", - "yargs": "^15.4.1" + "yargs": "^17.3.1" }, "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "fs-extra": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", @@ -21347,31 +21253,50 @@ "graceful-fs": "^4.1.6" } }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.0.tgz", + "integrity": "sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + } + }, + "yargs-parser": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "dev": true } } }, "browser-sync-client": { - "version": "2.27.7", - "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.7.tgz", - "integrity": "sha512-wKg9UP9a4sCIkBBAXUdbkdWFJzfSAQizGh+nC19W9y9zOo9s5jqeYRFUUbs7x5WKhjtspT+xetVp9AtBJ6BmWg==", + "version": "2.27.9", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.9.tgz", + "integrity": "sha512-FHW8kydp7FXo6jnX3gXJCpHAHtWNLK0nx839nnK+boMfMI1n4KZd0+DmTxHBsHsF3OHud4V4jwoN8U5HExMIdQ==", "dev": true, "requires": { "etag": "1.8.1", @@ -21398,16 +21323,16 @@ } }, "browser-sync-ui": { - "version": "2.27.7", - "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.7.tgz", - "integrity": "sha512-Bt4OQpx9p18OIzk0KKyu7jqlvmjacasUlk8ARY3uuIyiFWSBiRgr2i6XY8dEMF14DtbooaEBOpHEu9VCYvMcCw==", + "version": "2.27.9", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.9.tgz", + "integrity": "sha512-rsduR2bRIwFvM8CX6iY/Nu5aWub0WB9zfSYg9Le/RV5N5DEyxJYey0VxdfWCnzDOoelassTDzYQo+r0iJno3qw==", "dev": true, "requires": { "async-each-series": "0.1.1", "connect-history-api-fallback": "^1", "immutable": "^3", "server-destroy": "1.0.1", - "socket.io-client": "^2.4.0", + "socket.io-client": "^4.4.1", "stream-throttle": "^0.1.3" } }, @@ -22132,24 +22057,12 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", - "dev": true - }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "devOptional": true }, - "component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", - "dev": true - }, "compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -23400,83 +23313,71 @@ } }, "engine.io": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", - "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==", - "dev": true, + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.3.tgz", + "integrity": "sha512-rqs60YwkvWTLLnfazqgZqLa/aKo+9cueVfEi/dZ8PyGyaf8TLOxj++4QMIgeG3Gn0AhrWiFXvghsoY9L9h25GA==", + "devOptional": true, "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.4.1", - "debug": "~4.1.0", - "engine.io-parser": "~2.2.0", - "ws": "~7.4.2" + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" }, "dependencies": { "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "dev": true + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "devOptional": true }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "devOptional": true, + "requires": {} } } }, "engine.io-client": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", - "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.1.1.tgz", + "integrity": "sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==", "dev": true, "requires": { - "component-emitter": "~1.3.0", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.2.0", + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", "has-cors": "1.1.0", - "indexof": "0.0.1", "parseqs": "0.0.6", "parseuri": "0.0.6", - "ws": "~7.4.2", - "xmlhttprequest-ssl": "~1.6.2", + "ws": "~8.2.3", + "xmlhttprequest-ssl": "~2.0.0", "yeast": "0.1.2" }, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "requires": {} } } }, "engine.io-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", - "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", - "dev": true, + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", + "devOptional": true, "requires": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.4", - "blob": "0.0.5", - "has-binary2": "~1.0.2" + "@socket.io/base64-arraybuffer": "~1.0.2" } }, "enhanced-resolve": { @@ -24672,23 +24573,6 @@ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" }, - "has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "dev": true, - "requires": { - "isarray": "2.0.1" - }, - "dependencies": { - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - } - } - }, "has-cors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", @@ -24979,12 +24863,6 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -25729,39 +25607,6 @@ } } }, - "cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "devOptional": true - }, - "engine.io": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.3.tgz", - "integrity": "sha512-rqs60YwkvWTLLnfazqgZqLa/aKo+9cueVfEi/dZ8PyGyaf8TLOxj++4QMIgeG3Gn0AhrWiFXvghsoY9L9h25GA==", - "devOptional": true, - "requires": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.2.3" - } - }, - "engine.io-parser": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", - "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", - "devOptional": true, - "requires": { - "@socket.io/base64-arraybuffer": "~1.0.2" - } - }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -25794,37 +25639,6 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "devOptional": true }, - "socket.io": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", - "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", - "devOptional": true, - "requires": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "debug": "~4.3.2", - "engine.io": "~6.1.0", - "socket.io-adapter": "~2.3.3", - "socket.io-parser": "~4.0.4" - } - }, - "socket.io-adapter": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", - "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==", - "devOptional": true - }, - "socket.io-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", - "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", - "devOptional": true, - "requires": { - "@types/component-emitter": "^1.2.10", - "component-emitter": "~1.3.0", - "debug": "~4.3.1" - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -25845,13 +25659,6 @@ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", "devOptional": true - }, - "ws": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", - "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", - "devOptional": true, - "requires": {} } } }, @@ -26645,9 +26452,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "minipass": { "version": "3.1.6", @@ -26996,9 +26803,9 @@ "optional": true }, "node-forge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", - "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", + "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==" }, "node-gyp": { "version": "8.4.1", @@ -28998,121 +28805,60 @@ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" }, "socket.io": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.0.tgz", - "integrity": "sha512-9UPJ1UTvKayuQfVv2IQ3k7tCQC/fboDyIK62i99dAQIyHKaBsNdTpwHLgKJ6guRWxRtC9H+138UwpaGuQO9uWQ==", - "dev": true, + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", + "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", + "devOptional": true, "requires": { - "debug": "~4.1.0", - "engine.io": "~3.5.0", - "has-binary2": "~1.0.2", - "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.4.0", - "socket.io-parser": "~3.4.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" } }, "socket.io-adapter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", - "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==", - "dev": true + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==", + "devOptional": true }, "socket.io-client": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", - "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.4.1.tgz", + "integrity": "sha512-N5C/L5fLNha5Ojd7Yeb/puKcPWWcoB/A09fEjjNsg91EDVr5twk/OEyO6VT9dlLSUNY85NpW6KBhVMvaLKQ3vQ==", "dev": true, "requires": { - "backo2": "1.0.2", - "component-bind": "1.0.0", - "component-emitter": "~1.3.0", - "debug": "~3.1.0", - "engine.io-client": "~3.5.0", - "has-binary2": "~1.0.2", - "indexof": "0.0.1", - "parseqs": "0.0.6", + "@socket.io/component-emitter": "~3.0.0", + "backo2": "~1.0.2", + "debug": "~4.3.2", + "engine.io-client": "~6.1.1", "parseuri": "0.0.6", - "socket.io-parser": "~3.3.0", - "to-array": "0.1.4" + "socket.io-parser": "~4.1.1" }, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "socket.io-parser": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", - "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.1.2.tgz", + "integrity": "sha512-j3kk71QLJuyQ/hh5F/L2t1goqzdTL0gvDzuhTuNSwihfuFUrcSji0qFZmJJPtG6Rmug153eOPsUizeirf1IIog==", "dev": true, "requires": { - "component-emitter": "~1.3.0", - "debug": "~3.1.0", - "isarray": "2.0.1" + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1" } } } }, "socket.io-parser": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", - "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", - "dev": true, + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "devOptional": true, "requires": { - "component-emitter": "1.2.1", - "debug": "~4.1.0", - "isarray": "2.0.1" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - } + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" } }, "sockjs": { @@ -29679,12 +29425,6 @@ "os-tmpdir": "~1.0.2" } }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", - "dev": true - }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -30622,9 +30362,9 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, "xmlhttprequest-ssl": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.2.tgz", - "integrity": "sha512-tYOaldF/0BLfKuoA39QMwD4j2m8lq4DIncqj1yuNELX4vz9+z/ieG/vwmctjJce+boFHXstqhWnHSxc4W8f4qg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", "dev": true }, "xtend": { diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 003bbcf0d..64d906b77 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -33,6 +33,8 @@ import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/ import { MiningStartComponent } from './components/mining-start/mining-start.component'; import { GraphsComponent } from './components/graphs/graphs.component'; import { BlocksList } from './components/blocks-list/blocks-list.component'; +import { BlockFeesGraphComponent } from './components/block-fees-graph/block-fees-graph.component'; +import { BlockRewardsGraphComponent } from './components/block-rewards-graph/block-rewards-graph.component'; let routes: Routes = [ { @@ -85,7 +87,7 @@ let routes: Routes = [ path: 'pool', children: [ { - path: ':poolId', + path: ':slug', component: PoolComponent, }, ] @@ -117,6 +119,14 @@ let routes: Routes = [ path: 'mining/pools', component: PoolRankingComponent, }, + { + path: 'mining/block-fees', + component: BlockFeesGraphComponent, + }, + { + path: 'mining/block-rewards', + component: BlockRewardsGraphComponent, + } ], }, { @@ -127,13 +137,17 @@ let routes: Routes = [ path: 'docs/api/:type', component: DocsComponent }, + { + path: 'docs/faq', + component: DocsComponent + }, { path: 'docs/api', redirectTo: 'docs/api/rest' }, { path: 'docs', - redirectTo: 'docs/api/rest' + redirectTo: 'docs/faq' }, { path: 'api', @@ -156,10 +170,6 @@ let routes: Routes = [ children: [], component: AddressComponent }, - { - path: 'sponsor', - component: SponsorComponent, - }, ], }, { @@ -211,23 +221,11 @@ let routes: Routes = [ path: 'blocks', component: BlocksList, }, - { - path: 'hashrate', - component: HashrateChartComponent, - }, - { - path: 'hashrate/pools', - component: HashrateChartPoolsComponent, - }, - { - path: 'pools', - component: PoolRankingComponent, - }, { path: 'pool', children: [ { - path: ':poolId', + path: ':slug', component: PoolComponent, }, ] @@ -259,6 +257,14 @@ let routes: Routes = [ path: 'mining/pools', component: PoolRankingComponent, }, + { + path: 'mining/block-fees', + component: BlockFeesGraphComponent, + }, + { + path: 'mining/block-rewards', + component: BlockRewardsGraphComponent, + } ] }, { @@ -270,13 +276,17 @@ let routes: Routes = [ path: 'docs/api/:type', component: DocsComponent }, + { + path: 'docs/faq', + component: DocsComponent + }, { path: 'docs/api', redirectTo: 'docs/api/rest' }, { path: 'docs', - redirectTo: 'docs/api/rest' + redirectTo: 'docs/faq' }, { path: 'api', @@ -347,23 +357,11 @@ let routes: Routes = [ path: 'blocks', component: BlocksList, }, - { - path: 'hashrate', - component: HashrateChartComponent, - }, - { - path: 'hashrate/pools', - component: HashrateChartPoolsComponent, - }, - { - path: 'pools', - component: PoolRankingComponent, - }, { path: 'pool', children: [ { - path: ':poolId', + path: ':slug', component: PoolComponent, }, ] @@ -395,6 +393,14 @@ let routes: Routes = [ path: 'mining/pools', component: PoolRankingComponent, }, + { + path: 'mining/block-fees', + component: BlockFeesGraphComponent, + }, + { + path: 'mining/block-rewards', + component: BlockRewardsGraphComponent, + } ] }, { @@ -406,13 +412,17 @@ let routes: Routes = [ path: 'docs/api/:type', component: DocsComponent }, + { + path: 'docs/faq', + component: DocsComponent + }, { path: 'docs/api', redirectTo: 'docs/api/rest' }, { path: 'docs', - redirectTo: 'docs/api/rest' + redirectTo: 'docs/faq' }, { path: 'api', @@ -442,6 +452,10 @@ let routes: Routes = [ path: 'status', component: StatusViewComponent }, + { + path: 'sponsor', + component: SponsorComponent, + }, { path: '**', redirectTo: '' @@ -507,19 +521,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: 'mempool', component: StatisticsComponent, - }, - { - path: 'mining/hashrate-difficulty', - component: HashrateChartComponent, - }, - { - path: 'mining/pools-dominance', - component: HashrateChartPoolsComponent, - }, - { - path: 'mining/pools', - component: PoolRankingComponent, - }, + } ] }, { @@ -584,10 +586,6 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { path: 'trademark-policy', component: TrademarkPolicyComponent }, - { - path: 'sponsor', - component: SponsorComponent, - }, ], }, { @@ -639,19 +637,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: 'mempool', component: StatisticsComponent, - }, - { - path: 'mining/hashrate-difficulty', - component: HashrateChartComponent, - }, - { - path: 'mining/pools-dominance', - component: HashrateChartPoolsComponent, - }, - { - path: 'mining/pools', - component: PoolRankingComponent, - }, + } ] }, { @@ -712,10 +698,6 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { path: 'trademark-policy', component: TrademarkPolicyComponent }, - { - path: 'sponsor', - component: SponsorComponent, - }, ], }, { diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 807c88ade..8d7253fdf 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -56,6 +56,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa import { ApiDocsComponent } from './components/docs/api-docs.component'; import { DocsComponent } from './components/docs/docs.component'; import { ApiDocsNavComponent } from './components/docs/api-docs-nav.component'; +import { NoSanitizePipe } from './shared/pipes/no-sanitize.pipe'; import { CodeTemplateComponent } from './components/docs/code-template.component'; import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component'; import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component'; @@ -80,6 +81,8 @@ import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments- import { BlocksList } from './components/blocks-list/blocks-list.component'; import { RewardStatsComponent } from './components/reward-stats/reward-stats.component'; import { DataCyDirective } from './data-cy.directive'; +import { BlockFeesGraphComponent } from './components/block-fees-graph/block-fees-graph.component'; +import { BlockRewardsGraphComponent } from './components/block-rewards-graph/block-rewards-graph.component'; @NgModule({ declarations: [ @@ -119,6 +122,7 @@ import { DataCyDirective } from './data-cy.directive'; DashboardComponent, DifficultyComponent, ApiDocsComponent, + NoSanitizePipe, CodeTemplateComponent, TermsOfServiceComponent, PrivacyPolicyComponent, @@ -141,6 +145,8 @@ import { DataCyDirective } from './data-cy.directive'; BlocksList, DataCyDirective, RewardStatsComponent, + BlockFeesGraphComponent, + BlockRewardsGraphComponent, ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index c03a3a00a..74203ab81 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -169,7 +169,7 @@ -
+

Self-Hosted Integrations

diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index 8b9466732..222c14944 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -43,6 +43,7 @@ .alliances, .enterprise-sponsor, .community-integrations-sponsor, + .selfhosted-integrations-sponsor, .maintainers { margin-top: 68px; margin-bottom: 68px; @@ -108,6 +109,7 @@ .contributors, .community-sponsor, .community-integrations-sponsor, + .selfhosted-integrations-sponsor, .maintainers { .wrapper { display: inline-block; @@ -181,3 +183,8 @@ .no-about-margin { height: 10px; } + +.community-integrations-sponsor { + max-width: 750px; + margin: auto; +} diff --git a/frontend/src/app/components/address-labels/address-labels.component.html b/frontend/src/app/components/address-labels/address-labels.component.html index 9abfe32da..2f673a8a9 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.html +++ b/frontend/src/app/components/address-labels/address-labels.component.html @@ -1,17 +1,5 @@ multisig {{ multisigM }} of {{ multisigN }} - -Lightning {{ lightning }} - -Liquid {{ liquid }} +>{{ label }} diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index 75bbe7cba..b22d66e42 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -14,12 +14,7 @@ export class AddressLabelsComponent implements OnInit { @Input() vin: Vin; @Input() vout: Vout; - multisig = false; - multisigM: number; - multisigN: number; - - lightning = null; - liquid = null; + label?: string; constructor( stateService: StateService, @@ -39,30 +34,46 @@ export class AddressLabelsComponent implements OnInit { if (this.vin.inner_witnessscript_asm) { if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0) { if (this.vin.witness.length > 11) { - this.liquid = 'Peg Out'; + this.label = 'Liquid Peg Out'; } else { - this.liquid = 'Emergency Peg Out'; + this.label = 'Emergency Liquid Peg Out'; } return; } - // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs + const topElement = this.vin.witness[this.vin.witness.length - 2]; if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSHBYTES_(1 \w{2}|2 \w{4}) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(this.vin.inner_witnessscript_asm)) { - if (this.vin.witness[this.vin.witness.length - 2] == '01') { - this.lightning = 'Revoked Force Close'; - } else { - this.lightning = 'Force Close'; - } - // https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs - } else if (/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CHECKSEQUENCEVERIFY OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) { - if (this.vin.witness[this.vin.witness.length - 2].length == 66) { - this.lightning = 'Revoked HTLC'; + // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs + if (topElement === '01') { + // top element is '01' to get in the revocation path + this.label = 'Revoked Lightning Force Close'; } else { - this.lightning = 'HTLC'; + // top element is '', this is a delayed to_local output + this.label = 'Lightning Force Close'; + } + return; + } else if (/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) { + // https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs + if (topElement.length === 66) { + // top element is a public key + this.label = 'Revoked Lightning HTLC'; + } else if (topElement) { + // top element is a preimage + this.label = 'Lightning HTLC'; + } else { + // top element is '' to get in the multisig path of the script + this.label = 'Expired Lightning HTLC'; + } + return; + } else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) { + // https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors + if (topElement) { + // top element is a signature + this.label = 'Lightning Anchor'; + } else { + // top element is '', it has been swept after 16 blocks + this.label = 'Swept Lightning Anchor'; } - } - - if (this.lightning) { return; } @@ -77,19 +88,19 @@ export class AddressLabelsComponent implements OnInit { return; } const ops = script.split(' '); - if (ops.length < 3 || ops.pop() != 'OP_CHECKMULTISIG') { + if (ops.length < 3 || ops.pop() !== 'OP_CHECKMULTISIG') { return; } const opN = ops.pop(); if (!opN.startsWith('OP_PUSHNUM_')) { return; } - const n = parseInt(opN.match(/[0-9]+/)[0]); + const n = parseInt(opN.match(/[0-9]+/)[0], 10); if (ops.length < n * 2 + 1) { return; } // pop n public keys - for (var i = 0; i < n; i++) { + for (let i = 0; i < n; i++) { if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop())) { return; } @@ -101,13 +112,12 @@ export class AddressLabelsComponent implements OnInit { if (!opM.startsWith('OP_PUSHNUM_')) { return; } - const m = parseInt(opM.match(/[0-9]+/)[0]); + const m = parseInt(opM.match(/[0-9]+/)[0], 10); - this.multisig = true; - this.multisigM = m; - this.multisigN = n; + this.label = `multisig ${m} of ${n}`; } handleVout() { + this.detectMultisig(this.vout.scriptpubkey_asm); } } diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 0c030f5de..0ac64f86d 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -55,7 +55,7 @@
-

+

  {{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transaction {{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transactions diff --git a/frontend/src/app/components/app/app.component.ts b/frontend/src/app/components/app/app.component.ts index 0cb6ef051..e060fae54 100644 --- a/frontend/src/app/components/app/app.component.ts +++ b/frontend/src/app/components/app/app.component.ts @@ -1,28 +1,33 @@ import { Location } from '@angular/common'; import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core'; import { Router, NavigationEnd } from '@angular/router'; -import { WebsocketService } from '../../services/websocket.service'; import { StateService } from 'src/app/services/state.service'; +import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'app-root', templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + styleUrls: ['./app.component.scss'], + providers: [NgbTooltipConfig] }) export class AppComponent implements OnInit { link: HTMLElement = document.getElementById('canonical'); constructor( public router: Router, - private websocketService: WebsocketService, private stateService: StateService, private location: Location, + tooltipConfig: NgbTooltipConfig, @Inject(LOCALE_ID) private locale: string, ) { if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) { this.dir = 'rtl'; this.class = 'rtl-layout'; } + + tooltipConfig.animation = false; + tooltipConfig.container = 'body'; + tooltipConfig.triggers = 'hover'; } @HostBinding('attr.dir') dir = 'ltr'; diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html new file mode 100644 index 000000000..fc811c5ea --- /dev/null +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html @@ -0,0 +1,63 @@ +
+
+ Block fees +
+
+ + + + + + + + + + +
+
+
+ +
+
+
+
+
+ +
+ + +
+
+
Hashrate
+

+ +

+
+
+
Difficulty
+

+ +

+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.scss b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.scss new file mode 100644 index 000000000..54dbe5fad --- /dev/null +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.scss @@ -0,0 +1,135 @@ +.card-header { + border-bottom: 0; + font-size: 18px; + @media (min-width: 465px) { + font-size: 20px; + } +} + +.main-title { + position: relative; + color: #ffffff91; + margin-top: -13px; + font-size: 10px; + text-transform: uppercase; + font-weight: 500; + text-align: center; + padding-bottom: 3px; +} + +.full-container { + padding: 0px 15px; + width: 100%; + min-height: 500px; + height: calc(100% - 150px); + @media (max-width: 992px) { + height: 100%; + padding-bottom: 100px; + }; +} + +.chart { + width: 100%; + height: 100%; + padding-bottom: 20px; + padding-right: 10px; + @media (max-width: 992px) { + padding-bottom: 25px; + } + @media (max-width: 829px) { + padding-bottom: 50px; + } + @media (max-width: 767px) { + padding-bottom: 25px; + } + @media (max-width: 629px) { + padding-bottom: 55px; + } + @media (max-width: 567px) { + padding-bottom: 55px; + } +} +.chart-widget { + width: 100%; + height: 100%; + max-height: 270px; +} + +.formRadioGroup { + margin-top: 6px; + display: flex; + flex-direction: column; + @media (min-width: 1130px) { + position: relative; + top: -65px; + } + @media (min-width: 830px) and (max-width: 1130px) { + position: relative; + top: 0px; + } + @media (min-width: 830px) { + flex-direction: row; + float: right; + margin-top: 0px; + } + .btn-sm { + font-size: 9px; + @media (min-width: 830px) { + font-size: 14px; + } + } +} + +.pool-distribution { + min-height: 56px; + display: block; + @media (min-width: 485px) { + display: flex; + flex-direction: row; + } + h5 { + margin-bottom: 10px; + } + .item { + width: 50%; + display: inline-block; + margin: 0px auto 20px; + &:nth-child(2) { + order: 2; + @media (min-width: 485px) { + order: 3; + } + } + &:nth-child(3) { + order: 3; + @media (min-width: 485px) { + order: 2; + display: block; + } + @media (min-width: 768px) { + display: none; + } + @media (min-width: 992px) { + display: block; + } + } + .card-title { + font-size: 1rem; + color: #4a68b9; + } + .card-text { + font-size: 18px; + span { + color: #ffffff66; + font-size: 12px; + } + } + } +} + +.skeleton-loader { + width: 100%; + display: block; + max-width: 80px; + margin: 15px auto 3px; +} diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts new file mode 100644 index 000000000..74de3c317 --- /dev/null +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts @@ -0,0 +1,201 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; +import { EChartsOption, graphic } from 'echarts'; +import { Observable } from 'rxjs'; +import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; +import { ApiService } from 'src/app/services/api.service'; +import { SeoService } from 'src/app/services/seo.service'; +import { formatNumber } from '@angular/common'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { formatterXAxisLabel } from 'src/app/shared/graphs.utils'; +import { StorageService } from 'src/app/services/storage.service'; +import { MiningService } from 'src/app/services/mining.service'; + +@Component({ + selector: 'app-block-fees-graph', + templateUrl: './block-fees-graph.component.html', + styleUrls: ['./block-fees-graph.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 15px); + z-index: 100; + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BlockFeesGraphComponent implements OnInit { + @Input() tableOnly = false; + @Input() right: number | string = 45; + @Input() left: number | string = 75; + + miningWindowPreference: string; + radioGroupForm: FormGroup; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + statsObservable$: Observable; + isLoading = true; + formatNumber = formatNumber; + timespan = ''; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private seoService: SeoService, + private apiService: ApiService, + private formBuilder: FormBuilder, + private storageService: StorageService, + private miningService: MiningService + ) { + this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); + this.radioGroupForm.controls.dateSpan.setValue('1y'); + } + + ngOnInit(): void { + this.seoService.setTitle($localize`:@@mining.block-fees:Block Fees`); + this.miningWindowPreference = this.miningService.getDefaultTimespan('24h'); + this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); + this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); + + this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges + .pipe( + startWith(this.miningWindowPreference), + switchMap((timespan) => { + this.storageService.setValue('miningWindowPreference', timespan); + this.timespan = timespan; + this.isLoading = true; + return this.apiService.getHistoricalBlockFees$(timespan) + .pipe( + tap((data: any) => { + this.prepareChartOptions({ + blockFees: data.blockFees.map(val => [val.timestamp * 1000, val.avg_fees / 100000000]), + }); + this.isLoading = false; + }), + map((data: any) => { + const availableTimespanDay = ( + (new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp) + ) / 3600 / 24; + + return { + availableTimespanDay: availableTimespanDay, + }; + }), + ); + }), + share() + ); + } + + prepareChartOptions(data) { + this.chartOptions = { + animation: false, + color: [ + new graphic.LinearGradient(0, 0, 0, 0.65, [ + { offset: 0, color: '#F4511E' }, + { offset: 0.25, color: '#FB8C00' }, + { offset: 0.5, color: '#FFB300' }, + { offset: 0.75, color: '#FDD835' }, + { offset: 1, color: '#7CB342' } + ]), + ], + grid: { + top: 30, + bottom: 80, + right: this.right, + left: this.left, + }, + tooltip: { + show: !this.isMobile(), + trigger: 'axis', + axisPointer: { + type: 'line' + }, + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + align: 'left', + }, + borderColor: '#000', + formatter: (ticks) => { + const tick = ticks[0]; + const feesString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC`; + return ` + ${tick.axisValueLabel}
+ ${feesString} + `; + } + }, + xAxis: { + name: formatterXAxisLabel(this.locale, this.timespan), + nameLocation: 'middle', + nameTextStyle: { + padding: [10, 0, 0, 0], + }, + type: 'time', + splitNumber: this.isMobile() ? 5 : 10, + }, + yAxis: [ + { + type: 'value', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: (val) => { + return `${val} BTC`; + } + }, + splitLine: { + show: false, + } + }, + ], + series: [ + { + zlevel: 0, + name: 'Fees', + showSymbol: false, + symbol: 'none', + data: data.blockFees, + type: 'line', + lineStyle: { + width: 2, + }, + }, + ], + dataZoom: [{ + type: 'inside', + realtime: true, + zoomLock: true, + maxSpan: 100, + minSpan: 10, + moveOnMouseMove: false, + }, { + showDetail: false, + show: true, + type: 'slider', + brushSelect: false, + realtime: true, + left: 20, + right: 15, + selectedDataBackground: { + lineStyle: { + color: '#fff', + opacity: 0.45, + }, + areaStyle: { + opacity: 0, + } + }, + }], + }; + } + + isMobile() { + return (window.innerWidth <= 767.98); + } +} diff --git a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.html b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.html new file mode 100644 index 000000000..c2a3bcf00 --- /dev/null +++ b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.html @@ -0,0 +1,64 @@ +
+ +
+ Block rewards +
+
+ + + + + + + + + + +
+
+
+ +
+
+
+
+
+ +
+ + +
+
+
Hashrate
+

+ +

+
+
+
Difficulty
+

+ +

+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss new file mode 100644 index 000000000..54dbe5fad --- /dev/null +++ b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss @@ -0,0 +1,135 @@ +.card-header { + border-bottom: 0; + font-size: 18px; + @media (min-width: 465px) { + font-size: 20px; + } +} + +.main-title { + position: relative; + color: #ffffff91; + margin-top: -13px; + font-size: 10px; + text-transform: uppercase; + font-weight: 500; + text-align: center; + padding-bottom: 3px; +} + +.full-container { + padding: 0px 15px; + width: 100%; + min-height: 500px; + height: calc(100% - 150px); + @media (max-width: 992px) { + height: 100%; + padding-bottom: 100px; + }; +} + +.chart { + width: 100%; + height: 100%; + padding-bottom: 20px; + padding-right: 10px; + @media (max-width: 992px) { + padding-bottom: 25px; + } + @media (max-width: 829px) { + padding-bottom: 50px; + } + @media (max-width: 767px) { + padding-bottom: 25px; + } + @media (max-width: 629px) { + padding-bottom: 55px; + } + @media (max-width: 567px) { + padding-bottom: 55px; + } +} +.chart-widget { + width: 100%; + height: 100%; + max-height: 270px; +} + +.formRadioGroup { + margin-top: 6px; + display: flex; + flex-direction: column; + @media (min-width: 1130px) { + position: relative; + top: -65px; + } + @media (min-width: 830px) and (max-width: 1130px) { + position: relative; + top: 0px; + } + @media (min-width: 830px) { + flex-direction: row; + float: right; + margin-top: 0px; + } + .btn-sm { + font-size: 9px; + @media (min-width: 830px) { + font-size: 14px; + } + } +} + +.pool-distribution { + min-height: 56px; + display: block; + @media (min-width: 485px) { + display: flex; + flex-direction: row; + } + h5 { + margin-bottom: 10px; + } + .item { + width: 50%; + display: inline-block; + margin: 0px auto 20px; + &:nth-child(2) { + order: 2; + @media (min-width: 485px) { + order: 3; + } + } + &:nth-child(3) { + order: 3; + @media (min-width: 485px) { + order: 2; + display: block; + } + @media (min-width: 768px) { + display: none; + } + @media (min-width: 992px) { + display: block; + } + } + .card-title { + font-size: 1rem; + color: #4a68b9; + } + .card-text { + font-size: 18px; + span { + color: #ffffff66; + font-size: 12px; + } + } + } +} + +.skeleton-loader { + width: 100%; + display: block; + max-width: 80px; + margin: 15px auto 3px; +} diff --git a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts new file mode 100644 index 000000000..a22617922 --- /dev/null +++ b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts @@ -0,0 +1,200 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; +import { EChartsOption, graphic } from 'echarts'; +import { Observable } from 'rxjs'; +import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; +import { ApiService } from 'src/app/services/api.service'; +import { SeoService } from 'src/app/services/seo.service'; +import { formatNumber } from '@angular/common'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { formatterXAxisLabel } from 'src/app/shared/graphs.utils'; +import { MiningService } from 'src/app/services/mining.service'; +import { StorageService } from 'src/app/services/storage.service'; + +@Component({ + selector: 'app-block-rewards-graph', + templateUrl: './block-rewards-graph.component.html', + styleUrls: ['./block-rewards-graph.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 15px); + z-index: 100; + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BlockRewardsGraphComponent implements OnInit { + @Input() right: number | string = 45; + @Input() left: number | string = 75; + + miningWindowPreference: string; + radioGroupForm: FormGroup; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + statsObservable$: Observable; + isLoading = true; + formatNumber = formatNumber; + timespan = ''; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private seoService: SeoService, + private apiService: ApiService, + private formBuilder: FormBuilder, + private miningService: MiningService, + private storageService: StorageService + ) { + } + + ngOnInit(): void { + this.seoService.setTitle($localize`:@@mining.block-reward:Block Reward`); + this.miningWindowPreference = this.miningService.getDefaultTimespan('24h'); + this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); + this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); + + this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges + .pipe( + startWith(this.miningWindowPreference), + switchMap((timespan) => { + this.storageService.setValue('miningWindowPreference', timespan); + this.timespan = timespan; + this.isLoading = true; + return this.apiService.getHistoricalBlockRewards$(timespan) + .pipe( + tap((data: any) => { + this.prepareChartOptions({ + blockRewards: data.blockRewards.map(val => [val.timestamp * 1000, val.avg_rewards / 100000000]), + }); + this.isLoading = false; + }), + map((data: any) => { + const availableTimespanDay = ( + (new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp) + ) / 3600 / 24; + + return { + availableTimespanDay: availableTimespanDay, + }; + }), + ); + }), + share() + ); + } + + prepareChartOptions(data) { + this.chartOptions = { + animation: false, + color: [ + new graphic.LinearGradient(0, 0, 0, 0.65, [ + { offset: 0, color: '#F4511E' }, + { offset: 0.25, color: '#FB8C00' }, + { offset: 0.5, color: '#FFB300' }, + { offset: 0.75, color: '#FDD835' }, + { offset: 1, color: '#7CB342' } + ]), + ], + grid: { + top: 20, + bottom: 80, + right: this.right, + left: this.left, + }, + tooltip: { + show: !this.isMobile(), + trigger: 'axis', + axisPointer: { + type: 'line' + }, + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + align: 'left', + }, + borderColor: '#000', + formatter: (ticks) => { + const tick = ticks[0]; + const rewardsString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC`; + return ` + ${tick.axisValueLabel}
+ ${rewardsString} + `; + } + }, + xAxis: { + name: formatterXAxisLabel(this.locale, this.timespan), + nameLocation: 'middle', + nameTextStyle: { + padding: [10, 0, 0, 0], + }, + type: 'time', + splitNumber: this.isMobile() ? 5 : 10, + }, + yAxis: [ + { + min: value => Math.round(10 * value.min * 0.99) / 10, + max: value => Math.round(10 * value.max * 1.01) / 10, + type: 'value', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: (val) => { + return `${val} BTC`; + } + }, + splitLine: { + show: false, + } + }, + ], + series: [ + { + zlevel: 0, + name: 'Reward', + showSymbol: false, + symbol: 'none', + data: data.blockRewards, + type: 'line', + lineStyle: { + width: 2, + }, + }, + ], + dataZoom: [{ + type: 'inside', + realtime: true, + zoomLock: true, + maxSpan: 100, + minSpan: 10, + moveOnMouseMove: false, + }, { + showDetail: false, + show: true, + type: 'slider', + brushSelect: false, + realtime: true, + left: 20, + right: 15, + selectedDataBackground: { + lineStyle: { + color: '#fff', + opacity: 0.45, + }, + areaStyle: { + opacity: 0, + } + }, + }], + }; + } + + isMobile() { + return (window.innerWidth <= 767.98); + } +} diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 8970bd372..8b511b30c 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -163,7 +163,7 @@

-

+

{{ i }} transaction {{ i }} transactions diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index bc0025d2b..d41d34b81 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -25,7 +25,7 @@

diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index 4352944c6..4fd7d7ada 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -51,7 +51,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { } enabledMiningInfoIfNeeded(url) { - this.showMiningInfo = url === '/mining'; + this.showMiningInfo = url.indexOf('/mining') !== -1; this.cd.markForCheck(); // Need to update the view asap } diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.html b/frontend/src/app/components/blocks-list/blocks-list.component.html index 22693a856..f052b7fd6 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.html +++ b/frontend/src/app/components/blocks-list/blocks-list.component.html @@ -1,4 +1,4 @@ -
+

Blocks

@@ -25,8 +25,8 @@
- - + {{ block.extras.pool.name }} @@ -64,7 +64,7 @@ - + @@ -91,9 +91,9 @@
-
\ No newline at end of file +
diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.scss b/frontend/src/app/components/blocks-list/blocks-list.component.scss index 354c403af..abf337821 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.scss +++ b/frontend/src/app/components/blocks-list/blocks-list.component.scss @@ -14,6 +14,10 @@ td { padding-top: 0.7rem !important; padding-bottom: 0.7rem !important; + @media (max-width: 376px) { + padding-top: 0.73rem !important; + padding-bottom: 0.73rem !important; + } } .clear-link { @@ -35,8 +39,7 @@ td { .pool.widget { width: 40%; padding-left: 30px; - @media (max-width: 576px) { - padding-left: 40px; + @media (max-width: 376px) { width: 60%; } } diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.ts b/frontend/src/app/components/blocks-list/blocks-list.component.ts index c04403446..9da92f158 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.ts +++ b/frontend/src/app/components/blocks-list/blocks-list.component.ts @@ -22,6 +22,7 @@ export class BlocksList implements OnInit { paginationMaxSize: number; page = 1; lastPage = 1; + maxSize = window.innerWidth <= 767.98 ? 3 : 5; blocksCount: number; fromHeightSubject: BehaviorSubject = new BehaviorSubject(this.fromBlockHeight); skeletonLines: number[] = []; diff --git a/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.html b/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.html index 51872c932..787058d91 100644 --- a/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.html +++ b/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.html @@ -1,5 +1,5 @@
- +
diff --git a/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.scss b/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.scss index c4a81f804..a0d8e115e 100644 --- a/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.scss +++ b/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.scss @@ -1,4 +1,4 @@ -.latest-transactions { +.latest-adjustments { width: 100%; text-align: left; table-layout:fixed; @@ -7,34 +7,8 @@ } td { width: 25%; - } - .table-cell-satoshis { - display: none; - text-align: right; - @media (min-width: 576px) { - display: table-cell; + @media (max-width: 376px) { + padding: 0.85rem; } - @media (min-width: 768px) { - display: none; - } - @media (min-width: 1100px) { - display: table-cell; - } - } - .table-cell-fiat { - display: none; - text-align: right; - @media (min-width: 485px) { - display: table-cell; - } - @media (min-width: 768px) { - display: none; - } - @media (min-width: 992px) { - display: table-cell; - } - } - .table-cell-fees { - text-align: right; } } diff --git a/frontend/src/app/components/docs/api-docs-data.ts b/frontend/src/app/components/docs/api-docs-data.ts index 42fefcfe1..3e7d442c7 100644 --- a/frontend/src/app/components/docs/api-docs-data.ts +++ b/frontend/src/app/components/docs/api-docs-data.ts @@ -4411,3 +4411,177 @@ export const restApiDocsData = [ }, ]; +export const faqData = [ + { + type: "category", + category: "basics", + fragment: "basics", + title: "Basics", + showConditions: bitcoinNetworks + }, + { + type: "endpoint", + category: "basics", + showConditions: bitcoinNetworks, + fragment: "what-is-a-mempool", + title: "What is a mempool?", + answer: "

A mempool (short for \"memory pool\") holds the queue of pending and unconfirmed transactions for a cryptocurrency network node. There is no one global mempool: every node on the network maintains its own mempool, so different nodes may hold different transactions in their mempools.

" + }, + { + type: "endpoint", + category: "basics", + showConditions: bitcoinNetworks, + fragment: "what-is-a-mempool-explorer", + title: "What is a mempool explorer?", + answer: "

A mempool explorer is a tool that enables you to view real-time and historical information about a node's mempool, visualize its transactions, and search and view those transactions.

The mempool.space website invented the concept of visualizing a Bitcoin node's mempool as projected blocks. These blocks are the inspiration for our half-filled block logo.

Projected blocks are on the left of the dotted white line, and confirmed blocks are on the right.

" + }, + { + type: "endpoint", + category: "basics", + showConditions: bitcoinNetworks, + fragment: "what-is-a-blockchain", + title: "What is a blockchain?", + answer: "

A blockchain is a distributed ledger that records the transactions for a cryptocurrency network. Miners amend the blockchain ledger by mining new blocks.

" + }, + { + type: "endpoint", + category: "basics", + showConditions: bitcoinNetworks, + fragment: "what-is-a-block-explorer", + title: "What is a block explorer?", + answer: "

A block explorer is a tool that enables you to explore real-time and historical information about the blockchain of a cryptocurrency. This includes data related to blocks, transactions, addresses, and more.

" + }, + { + type: "endpoint", + category: "basics", + showConditions: bitcoinNetworks, + fragment: "what-is-mining", + title: "What is mining?", + answer: "Mining is the process by which unconfirmed transactions in a mempool are confirmed into a block on a blockchain. Miners select unconfirmed transactions from their mempools and arrange them into a block such that they solve a particular math problem.

The first miner on the network to find a suitable block earns all the transaction fees from the transactions in that block. As a result, miners tend to prioritize transactions with higher transaction fees.

" + }, + { + type: "endpoint", + category: "basics", + showConditions: bitcoinNetworks, + fragment: "what-are-mining-pools", + title: "What are mining pools?", + answer: "Mining pools are groups of miners that combine their computational power in order to increase the probability of finding new blocks." + }, + { + type: "category", + category: "help", + fragment: "help-stuck-transaction", + title: "Help! My transaction is stuck", + showConditions: bitcoinNetworks + }, + { + type: "endpoint", + category: "help", + showConditions: bitcoinNetworks, + fragment: "why-is-transaction-stuck-in-mempool", + title: "Why is my transaction stuck in the mempool?", + answer: "

Miners decide which transactions are included in the blocks they mine, so they usually prioritize transactions which pay them the highest transaction fees (transaction fees are measured in sats per virtual byte, or sat/vB). If it's been a while and your transcation hasn't been confirmed, your transaction probably has a lower transaction fee relative to other transactions currently in the mempool.

" + }, + { + type: "endpoint", + category: "help", + showConditions: bitcoinNetworks, + fragment: "how-to-get-transaction-confirmed-quickly", + title: "How can I get my transaction confirmed more quickly?", + answer: "

If your wallet supports RBF, and if your transaction was created with RBF enabled, you can bump the fee higher.

Otherwise, if your wallet does not support RBF, you can increase the effective fee rate of your transaction by spending its change output using a higher fee. This is called CPFP.

" + }, + { + type: "endpoint", + category: "help", + showConditions: bitcoinNetworks, + fragment: "how-prevent-stuck-transaction", + title: "How can I prevent a transaction from getting stuck in the future?", + answer: "

You must use an adequate transaction fee commensurate with how quickly you need the transaction to be confirmed. Also consider using RBF if your wallet supports it so that you can bump the fee rate if needed.

" + }, + { + type: "category", + category: "using", + fragment: "using-this-website", + title: "Using this website", + showConditions: bitcoinNetworks + }, + { + type: "endpoint", + category: "how-to", + showConditions: bitcoinNetworks, + fragment: "looking-up-transactions", + title: "How can I look up a transaction?", + answer: "Search for the transaction ID in the search box at the top-right of this website." + }, + { + type: "endpoint", + category: "how-to", + showConditions: bitcoinNetworks, + fragment: "looking-up-addresses", + title: "How can I look up an address?", + answer: "Search for the address in the search box at the top-right of this website." + }, + { + type: "endpoint", + category: "how-to", + showConditions: bitcoinNetworks, + fragment: "looking-up-blocks", + title: "How can I look up a block?", + answer: "Search for the block number (or block hash) in the search box at the top-right of this website." + }, + { + type: "endpoint", + category: "how-to", + showConditions: bitcoinNetworks, + fragment: "looking-up-fee-estimates", + title: "How can I look up fee estimates?", + answer: "

See real-time fee estimates on the main dashboard.

Low priority is suggested for confirmation within 6 blocks (~1 hour), Medium priority is suggested for confirmation within 3 blocks (~30 minutes), and High priority is suggested for confirmation in the next block (~10 minutes).

" + }, + { + type: "endpoint", + category: "how-to", + showConditions: bitcoinNetworks, + fragment: "looking-up-historical-trends", + title: "How can I explore historical trends?", + answer: "See the graphs page for aggregate trends over time: mempool size over time and incoming transaction velocity over time." + }, + { + type: "category", + category: "advanced", + fragment: "advanced", + title: "Advanced", + showConditions: bitcoinNetworks + }, + { + type: "endpoint", + category: "advanced", + showConditions: bitcoinNetworks, + fragment: "who-runs-this-website", + title: "Who runs this website?", + answer: "The official mempool.space website is operated by The Mempool Open Source Project. See more information on our About page. There are also many unofficial instances of this website operated by individual members of the Bitcoin community." + }, + { + type: "endpoint", + category: "advanced", + showConditions: bitcoinNetworks, + fragment: "host-my-own-instance-raspberry-pi", + title: "How can I host my own instance on a Raspberry Pi?", + answer: "We support one-click installation on a number of Raspberry Pi full-node distros including Umbrel, RaspiBlitz, MyNode, and RoninDojo." + }, + { + type: "endpoint", + category: "advanced", + showConditions: bitcoinNetworks, + fragment: "host-my-own-instance-linux-server", + title: "How can I host my own instance on a Linux server?", + answer: "You can manually install mempool on your own Linux server, but this requires advanced sysadmin skills since you will be manually configuring everything. We do not provide support for manual deployments." + }, + { + type: "endpoint", + category: "advanced", + showConditions: bitcoinNetworks, + fragment: "install-mempool-with-docker", + title: "Can I install Mempool using Docker?", + answer: "Yes, we publish Docker images (or you can build your own), and provide an example docker-compose template." + } +]; diff --git a/frontend/src/app/components/docs/api-docs-nav.component.html b/frontend/src/app/components/docs/api-docs-nav.component.html index 83eaaf9e8..c8460ada4 100644 --- a/frontend/src/app/components/docs/api-docs-nav.component.html +++ b/frontend/src/app/components/docs/api-docs-nav.component.html @@ -1,4 +1,4 @@ -
+

{{ item.title }}

{{ item.title }}
diff --git a/frontend/src/app/components/docs/api-docs-nav.component.ts b/frontend/src/app/components/docs/api-docs-nav.component.ts index 90588860e..a7ec45569 100644 --- a/frontend/src/app/components/docs/api-docs-nav.component.ts +++ b/frontend/src/app/components/docs/api-docs-nav.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { restApiDocsData } from './api-docs-data'; +import { faqData } from './api-docs-data'; @Component({ selector: 'app-api-docs-nav', @@ -9,13 +10,18 @@ import { restApiDocsData } from './api-docs-data'; export class ApiDocsNavComponent implements OnInit { @Input() network: any; + @Input() whichTab: string; @Output() navLinkClickEvent: EventEmitter = new EventEmitter(); - restDocs: any[]; + tabData: any[]; constructor() { } ngOnInit(): void { - this.restDocs = restApiDocsData; + if( this.whichTab === 'rest' ) { + this.tabData = restApiDocsData; + } else if( this.whichTab = 'faq' ) { + this.tabData = faqData; + } } navLinkClick( event ) { diff --git a/frontend/src/app/components/docs/api-docs.component.html b/frontend/src/app/components/docs/api-docs.component.html index 88a48f975..6cb5839a0 100644 --- a/frontend/src/app/components/docs/api-docs.component.html +++ b/frontend/src/app/components/docs/api-docs.component.html @@ -1,20 +1,45 @@
-
+
- + +
+ +
+ +
+

{{ item.title }}

+
+
Height
{{ item.title }}{{ item.category }}
+
+
+
+ +
+
+
+
+ +
+ + + +
+ +
+

Reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} API service.

-
+

{{ item.title }}

- {{ item.title }} {{ item.category }} + {{ item.title }} {{ item.category }}
Endpoint
@@ -65,7 +90,7 @@
-
+
diff --git a/frontend/src/app/components/docs/api-docs.component.scss b/frontend/src/app/components/docs/api-docs.component.scss index cb3770622..91c85c346 100644 --- a/frontend/src/app/components/docs/api-docs.component.scss +++ b/frontend/src/app/components/docs/api-docs.component.scss @@ -152,6 +152,14 @@ h3 { float: right; } +.endpoint-container .section-header table { + width: 100%; +} + +.endpoint-container .section-header table td:first-child { + padding-right: 24px; +} + #doc-nav-mobile { position: fixed; top: 20px; @@ -179,6 +187,16 @@ h3 { border-radius: 0.5rem 0.5rem 0 0; } +.blockchain-wrapper { + position: relative; + width: 100%; + overflow: auto; + scrollbar-width: none; +} +.blockchain-wrapper::-webkit-scrollbar { + display: none; +} + @media (max-width: 992px) { .hide-on-mobile { @@ -231,4 +249,8 @@ h3 { h3 { display: none; } + + .doc-item-container:last-of-type .endpoint-container { + margin-bottom: 4rem; + } } diff --git a/frontend/src/app/components/docs/api-docs.component.ts b/frontend/src/app/components/docs/api-docs.component.ts index 5e2300fed..39f611310 100644 --- a/frontend/src/app/components/docs/api-docs.component.ts +++ b/frontend/src/app/components/docs/api-docs.component.ts @@ -4,7 +4,7 @@ import { Observable, merge, of } from 'rxjs'; import { SeoService } from 'src/app/services/seo.service'; import { tap } from 'rxjs/operators'; import { ActivatedRoute } from "@angular/router"; -import { restApiDocsData, wsApiDocsData } from './api-docs-data'; +import { faqData, restApiDocsData, wsApiDocsData } from './api-docs-data'; @Component({ selector: 'app-api-docs', @@ -18,8 +18,9 @@ export class ApiDocsComponent implements OnInit { env: Env; code: any; baseNetworkUrl = ''; - @Input() restTabActivated: Boolean; + @Input() whichTab: string; desktopDocsNavPosition = "relative"; + faq: any[]; restDocs: any[]; wsDocs: any; screenWidth: number; @@ -33,7 +34,9 @@ export class ApiDocsComponent implements OnInit { ngAfterViewInit() { const that = this; setTimeout( () => { - this.openEndpointContainer( this.route.snapshot.fragment ); + if( this.route.snapshot.fragment ) { + this.openEndpointContainer( this.route.snapshot.fragment ); + } window.addEventListener('scroll', function() { that.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative"; }); @@ -62,6 +65,7 @@ export class ApiDocsComponent implements OnInit { this.hostname = `${document.location.protocol}//${this.hostname}`; + this.faq = faqData; this.restDocs = restApiDocsData; this.wsDocs = wsApiDocsData; @@ -71,7 +75,16 @@ export class ApiDocsComponent implements OnInit { } anchorLinkClick( event: any ) { - const targetId = event.target.hash.substring(1); + let targetId = ""; + if( event.target.nodeName === "A" ) { + targetId = event.target.hash.substring(1); + } else { + let element = event.target; + while( element.nodeName !== "A" ) { + element = element.parentElement; + } + targetId = element.hash.substring(1); + } if( this.route.snapshot.fragment === targetId ) { document.getElementById( targetId ).scrollIntoView(); } @@ -79,7 +92,8 @@ export class ApiDocsComponent implements OnInit { } openEndpointContainer( targetId ) { - if( ( window.innerWidth <= 992 ) && this.restTabActivated && targetId ) { + const tabHeaderHeight = document.getElementById( targetId + "-tab-header" ).scrollHeight; + if( ( window.innerWidth <= 992 ) && ( ( this.whichTab === 'rest' ) || ( this.whichTab === 'faq' ) ) && targetId ) { const endpointContainerEl = document.querySelector( "#" + targetId ); const endpointContentEl = document.querySelector( "#" + targetId + " .endpoint-content" ); const endPointContentElHeight = endpointContentEl.clientHeight; @@ -90,8 +104,8 @@ export class ApiDocsComponent implements OnInit { endpointContentEl.style.opacity = "0"; endpointContentEl.classList.remove( "open" ); } else { - endpointContainerEl.style.height = endPointContentElHeight + 90 + "px"; - endpointContentEl.style.top = "90px"; + endpointContainerEl.style.height = endPointContentElHeight + tabHeaderHeight + 28 + "px"; + endpointContentEl.style.top = tabHeaderHeight + 28 + "px"; endpointContentEl.style.opacity = "1"; endpointContentEl.classList.add( "open" ); } diff --git a/frontend/src/app/components/docs/docs.component.html b/frontend/src/app/components/docs/docs.component.html index 8dc4a9e72..19a664e46 100644 --- a/frontend/src/app/components/docs/docs.component.html +++ b/frontend/src/app/components/docs/docs.component.html @@ -5,20 +5,29 @@
- +
- \ No newline at end of file + diff --git a/frontend/src/app/components/graphs/graphs.component.scss b/frontend/src/app/components/graphs/graphs.component.scss index c4ca483bd..b952137b9 100644 --- a/frontend/src/app/components/graphs/graphs.component.scss +++ b/frontend/src/app/components/graphs/graphs.component.scss @@ -1,9 +1,6 @@ .menu { flex-grow: 1; - max-width: 600px; -} - -.menu-li { - flex-grow: 1; - text-align: center; + @media (min-width: 576px) { + max-width: 400px; + } } diff --git a/frontend/src/app/components/graphs/graphs.component.ts b/frontend/src/app/components/graphs/graphs.component.ts index 58bf58db1..e172d2206 100644 --- a/frontend/src/app/components/graphs/graphs.component.ts +++ b/frontend/src/app/components/graphs/graphs.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit } from "@angular/core"; import { StateService } from "src/app/services/state.service"; +import { WebsocketService } from "src/app/services/websocket.service"; @Component({ selector: 'app-graphs', @@ -7,9 +8,12 @@ import { StateService } from "src/app/services/state.service"; styleUrls: ['./graphs.component.scss'], }) export class GraphsComponent implements OnInit { - constructor(public stateService: StateService) { } + constructor( + public stateService: StateService, + private websocketService: WebsocketService + ) { } ngOnInit(): void { - + this.websocketService.want(['blocks']); } } diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html index e3d2f6213..93f17dcdf 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -19,37 +19,40 @@
- Hashrate & Difficulty -
+ Hashrate & Difficulty +
-
-
+
+
- +
@@ -67,4 +70,4 @@

- \ No newline at end of file + diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss index 62903d4f4..86c1f8ec3 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss @@ -20,12 +20,11 @@ .full-container { padding: 0px 15px; width: 100%; - height: calc(100% - 170px); + min-height: 500px; + height: calc(100% - 150px); @media (max-width: 992px) { - height: calc(100% - 220px); - }; - @media (max-width: 575px) { - height: calc(100% - 260px); + height: 100%; + padding-bottom: 100px; }; } @@ -93,17 +92,8 @@ } .item { width: 50%; - margin: 0px auto 10px; display: inline-block; - @media (min-width: 485px) { - margin: 0px auto 10px; - } - @media (min-width: 785px) { - margin: 0px auto 0px; - } - &:last-child { - margin: 0px auto 0px; - } + margin: 0px auto 20px; &:nth-child(2) { order: 2; @media (min-width: 485px) { @@ -142,4 +132,4 @@ display: block; max-width: 80px; margin: 15px auto 3px; -} +} \ No newline at end of file diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts index 521bee3d5..4cac95557 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; import { EChartsOption, graphic } from 'echarts'; import { Observable } from 'rxjs'; import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators'; @@ -7,6 +7,8 @@ import { SeoService } from 'src/app/services/seo.service'; import { formatNumber } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; import { selectPowerOfTen } from 'src/app/bitcoin.utils'; +import { StorageService } from 'src/app/services/storage.service'; +import { MiningService } from 'src/app/services/mining.service'; @Component({ selector: 'app-hashrate-chart', @@ -28,15 +30,16 @@ export class HashrateChartComponent implements OnInit { @Input() right: number | string = 45; @Input() left: number | string = 75; + miningWindowPreference: string; radioGroupForm: FormGroup; chartOptions: EChartsOption = {}; chartInitOptions = { renderer: 'svg', - width: 'auto', - height: 'auto', }; + @HostBinding('attr.dir') dir = 'ltr'; + hashrateObservable$: Observable; isLoading = true; formatNumber = formatNumber; @@ -47,20 +50,32 @@ export class HashrateChartComponent implements OnInit { private apiService: ApiService, private formBuilder: FormBuilder, private cd: ChangeDetectorRef, + private storageService: StorageService, + private miningService: MiningService ) { - this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); - this.radioGroupForm.controls.dateSpan.setValue('1y'); } ngOnInit(): void { - if (!this.widget) { + let firstRun = true; + + if (this.widget) { + this.miningWindowPreference = '1y'; + } else { this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Difficulty`); + this.miningWindowPreference = this.miningService.getDefaultTimespan('1m'); } + this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); + this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges .pipe( - startWith('1y'), + startWith(this.miningWindowPreference), switchMap((timespan) => { + if (!this.widget && !firstRun) { + this.storageService.setValue('miningWindowPreference', timespan); + } + firstRun = false; + this.miningWindowPreference = timespan; this.isLoading = true; return this.apiService.getHistoricalHashrate$(timespan) .pipe( @@ -157,10 +172,10 @@ export class HashrateChartComponent implements OnInit { '#D81B60', ], grid: { - top: 30, + top: 20, + bottom: this.widget ? 30 : 70, right: this.right, left: this.left, - bottom: this.widget ? 30 : this.isMobile() ? 90 : 60, }, tooltip: { show: !this.isMobile() || !this.widget, @@ -176,7 +191,7 @@ export class HashrateChartComponent implements OnInit { align: 'left', }, borderColor: '#000', - formatter: function (ticks) { + formatter: (ticks) => { let hashrateString = ''; let difficultyString = ''; let hashratePowerOfTen: any = selectPowerOfTen(1); @@ -207,11 +222,14 @@ export class HashrateChartComponent implements OnInit { ${hashrateString}
${difficultyString} `; - }.bind(this) + } }, xAxis: data.hashrates.length === 0 ? undefined : { type: 'time', splitNumber: (this.isMobile() || this.widget) ? 5 : 10, + axisLabel: { + hideOverlap: true, + } }, legend: (this.widget || data.hashrates.length === 0) ? undefined : { data: [ @@ -241,7 +259,7 @@ export class HashrateChartComponent implements OnInit { }, yAxis: data.hashrates.length === 0 ? undefined : [ { - min: function (value) { + min: (value) => { return value.min * 0.9; }, type: 'value', @@ -250,7 +268,7 @@ export class HashrateChartComponent implements OnInit { formatter: (val) => { const selectedPowerOfTen: any = selectPowerOfTen(val); const newVal = Math.round(val / selectedPowerOfTen.divider); - return `${newVal} ${selectedPowerOfTen.unit}H/s` + return `${newVal} ${selectedPowerOfTen.unit}H/s`; } }, splitLine: { @@ -258,7 +276,7 @@ export class HashrateChartComponent implements OnInit { } }, { - min: function (value) { + min: (value) => { return value.min * 0.9; }, type: 'value', @@ -268,7 +286,7 @@ export class HashrateChartComponent implements OnInit { formatter: (val) => { const selectedPowerOfTen: any = selectPowerOfTen(val); const newVal = Math.round(val / selectedPowerOfTen.divider); - return `${newVal} ${selectedPowerOfTen.unit}` + return `${newVal} ${selectedPowerOfTen.unit}`; } }, splitLine: { @@ -278,6 +296,7 @@ export class HashrateChartComponent implements OnInit { ], series: data.hashrates.length === 0 ? [] : [ { + zlevel: 0, name: 'Hashrate', showSymbol: false, symbol: 'none', @@ -288,6 +307,7 @@ export class HashrateChartComponent implements OnInit { }, }, { + zlevel: 1, yAxisIndex: 1, name: 'Difficulty', showSymbol: false, @@ -312,7 +332,6 @@ export class HashrateChartComponent implements OnInit { type: 'slider', brushSelect: false, realtime: true, - bottom: this.isMobile() ? 30 : 0, left: 20, right: 15, selectedDataBackground: { diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html index 32f6a7b25..f3d547dd6 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html @@ -1,32 +1,35 @@ -
+
-
- Mining pools dominance -
+
+ Mining pools dominance +
-
-
diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss index 095d33583..e89c8f173 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss @@ -20,19 +20,18 @@ .full-container { padding: 0px 15px; width: 100%; - height: calc(100% - 140px); - @media (max-width: 991px) { - height: calc(100% - 190px); - }; - @media (max-width: 575px) { - height: calc(100% - 235px); + min-height: 500px; + height: calc(100% - 150px); + @media (max-width: 992px) { + height: 100%; + padding-bottom: 100px; }; } .chart { width: 100%; height: 100%; - padding-bottom: 25px; + padding-bottom: 20px; padding-right: 10px; @media (max-width: 992px) { padding-bottom: 25px; @@ -43,12 +42,6 @@ @media (max-width: 767px) { padding-bottom: 50px; } - @media (max-width: 629px) { - padding-bottom: 85px; - } - @media (max-width: 567px) { - padding-bottom: 85px; - } } .chart-widget { width: 100%; diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts index 280852d47..85bea9840 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; import { EChartsOption } from 'echarts'; import { Observable } from 'rxjs'; import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators'; @@ -6,6 +6,8 @@ import { ApiService } from 'src/app/services/api.service'; import { SeoService } from 'src/app/services/seo.service'; import { FormBuilder, FormGroup } from '@angular/forms'; import { poolsColor } from 'src/app/app.constants'; +import { StorageService } from 'src/app/services/storage.service'; +import { MiningService } from 'src/app/services/mining.service'; @Component({ selector: 'app-hashrate-chart-pools', @@ -22,19 +24,19 @@ import { poolsColor } from 'src/app/app.constants'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class HashrateChartPoolsComponent implements OnInit { - @Input() widget = false; @Input() right: number | string = 45; @Input() left: number | string = 25; + miningWindowPreference: string; radioGroupForm: FormGroup; chartOptions: EChartsOption = {}; chartInitOptions = { renderer: 'svg', - width: 'auto', - height: 'auto', }; + @HostBinding('attr.dir') dir = 'ltr'; + hashrateObservable$: Observable; isLoading = true; @@ -44,20 +46,29 @@ export class HashrateChartPoolsComponent implements OnInit { private apiService: ApiService, private formBuilder: FormBuilder, private cd: ChangeDetectorRef, + private storageService: StorageService, + private miningService: MiningService ) { this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); this.radioGroupForm.controls.dateSpan.setValue('1y'); } ngOnInit(): void { - if (!this.widget) { - this.seoService.setTitle($localize`:@@mining.pools-historical-dominance:Pools Historical Dominance`); - } + let firstRun = true; + + this.seoService.setTitle($localize`:@@mining.pools-historical-dominance:Pools Historical Dominance`); + this.miningWindowPreference = this.miningService.getDefaultTimespan('1m'); + this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); + this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges .pipe( - startWith('1y'), + startWith(this.miningWindowPreference), switchMap((timespan) => { + if (!firstRun) { + this.storageService.setValue('miningWindowPreference', timespan); + } + firstRun = false; this.isLoading = true; return this.apiService.getHistoricalPoolsHashrate$(timespan) .pipe( @@ -75,6 +86,7 @@ export class HashrateChartPoolsComponent implements OnInit { const legends = []; for (const name in grouped) { series.push({ + zlevel: 0, stack: 'Total', name: name, showSymbol: false, @@ -84,7 +96,7 @@ export class HashrateChartPoolsComponent implements OnInit { lineStyle: { width: 0 }, areaStyle: { opacity: 1 }, smooth: true, - color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()], + color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()], emphasis: { disabled: true, scale: false, @@ -157,11 +169,11 @@ export class HashrateChartPoolsComponent implements OnInit { grid: { right: this.right, left: this.left, - bottom: this.widget ? 30 : 60, - top: this.widget || this.isMobile() ? 10 : 50, + bottom: 70, + top: this.isMobile() ? 10 : 50, }, tooltip: { - show: !this.isMobile() || !this.widget, + show: !this.isMobile(), trigger: 'axis', axisPointer: { type: 'line' @@ -188,9 +200,12 @@ export class HashrateChartPoolsComponent implements OnInit { }, xAxis: data.series.length === 0 ? undefined : { type: 'time', - splitNumber: (this.isMobile() || this.widget) ? 5 : 10, + splitNumber: this.isMobile() ? 5 : 10, + axisLabel: { + hideOverlap: true, + } }, - legend: (this.isMobile() || this.widget || data.series.length === 0) ? undefined : { + legend: (this.isMobile() || data.series.length === 0) ? undefined : { data: data.legends }, yAxis: data.series.length === 0 ? undefined : { @@ -207,7 +222,7 @@ export class HashrateChartPoolsComponent implements OnInit { min: 0, }, series: data.series, - dataZoom: this.widget ? null : [{ + dataZoom: [{ type: 'inside', realtime: true, zoomLock: true, @@ -220,7 +235,6 @@ export class HashrateChartPoolsComponent implements OnInit { type: 'slider', brushSelect: false, realtime: true, - bottom: 0, left: 20, right: 15, selectedDataBackground: { diff --git a/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts b/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts index af188bbb3..89a089f06 100644 --- a/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts +++ b/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts @@ -157,6 +157,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges { }, series: [ { + zlevel: 0, data: this.data.series[0], type: 'line', smooth: false, diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index db5f06b57..a4bd584f3 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -62,7 +62,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { ) { } enabledMiningInfoIfNeeded(url) { - this.showMiningInfo = url === '/mining'; + this.showMiningInfo = url.indexOf('/mining') !== -1; this.cd.markForCheck(); // Need to update the view asap } diff --git a/frontend/src/app/components/mempool-graph/mempool-graph.component.ts b/frontend/src/app/components/mempool-graph/mempool-graph.component.ts index f290d91ca..3b4e18fb4 100644 --- a/frontend/src/app/components/mempool-graph/mempool-graph.component.ts +++ b/frontend/src/app/components/mempool-graph/mempool-graph.component.ts @@ -122,6 +122,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges { if (index >= this.feeLimitIndex) { newColors.push(this.chartColorsOrdered[index]); seriesGraph.push({ + zlevel: 0, name: this.feeLevelsOrdered[index], type: 'line', stack: 'fees', diff --git a/frontend/src/app/components/miner/miner.component.html b/frontend/src/app/components/miner/miner.component.html index 4a54fb4d0..f4798d07d 100644 --- a/frontend/src/app/components/miner/miner.component.html +++ b/frontend/src/app/components/miner/miner.component.html @@ -4,7 +4,7 @@ - {{ miner }} + {{ miner }} Unknown diff --git a/frontend/src/app/components/miner/miner.component.ts b/frontend/src/app/components/miner/miner.component.ts index c022526fb..733204120 100644 --- a/frontend/src/app/components/miner/miner.component.ts +++ b/frontend/src/app/components/miner/miner.component.ts @@ -1,6 +1,8 @@ import { Component, Input, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { AssetsService } from 'src/app/services/assets.service'; import { Transaction } from 'src/app/interfaces/electrs.interface'; +import { StateService } from 'src/app/services/state.service'; +import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; @Component({ selector: 'app-miner', @@ -13,15 +15,23 @@ export class MinerComponent implements OnChanges { miner = ''; title = ''; url = ''; + target = '_blank'; loading = true; constructor( private assetsService: AssetsService, private cd: ChangeDetectorRef, + public stateService: StateService, + private relativeUrlPipe: RelativeUrlPipe, ) { } ngOnChanges() { this.miner = ''; + if (this.stateService.env.MINING_DASHBOARD) { + this.miner = 'Unknown'; + this.url = this.relativeUrlPipe.transform(`/mining/pool/unknown`); + this.target = ''; + } this.loading = true; this.findMinerFromCoinbase(); } @@ -40,7 +50,13 @@ export class MinerComponent implements OnChanges { if (pools.payout_addresses[vout.scriptpubkey_address]) { this.miner = pools.payout_addresses[vout.scriptpubkey_address].name; this.title = $localize`:@@miner-identified-by-payout:Identified by payout address: '${vout.scriptpubkey_address}:PAYOUT_ADDRESS:'`; - this.url = pools.payout_addresses[vout.scriptpubkey_address].link; + const pool = pools.payout_addresses[vout.scriptpubkey_address]; + if (this.stateService.env.MINING_DASHBOARD && pools.slugs && pools.slugs[pool.name] !== undefined) { + this.url = this.relativeUrlPipe.transform(`/mining/pool/${pools.slugs[pool.name]}`); + this.target = ''; + } else { + this.url = pool.link; + } break; } @@ -48,9 +64,15 @@ export class MinerComponent implements OnChanges { if (pools.coinbase_tags.hasOwnProperty(tag)) { const coinbaseAscii = this.hex2ascii(this.coinbaseTransaction.vin[0].scriptsig); if (coinbaseAscii.indexOf(tag) > -1) { - this.miner = pools.coinbase_tags[tag].name; + const pool = pools.coinbase_tags[tag]; + this.miner = pool.name; this.title = $localize`:@@miner-identified-by-coinbase:Identified by coinbase tag: '${tag}:TAG:'`; - this.url = pools.coinbase_tags[tag].link; + if (this.stateService.env.MINING_DASHBOARD && pools.slugs && pools.slugs[pool.name] !== undefined) { + this.url = this.relativeUrlPipe.transform(`/mining/pool/${pools.slugs[pool.name]}`); + this.target = ''; + } else { + this.url = pool.link; + } break; } } diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html index 674d0bc44..3b32408c8 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html @@ -27,7 +27,7 @@
-
+
View more @@ -38,7 +38,7 @@
-
+
View more @@ -49,7 +49,7 @@
-
+
Latest blocks @@ -63,13 +63,13 @@
-
-
+
+
+
+
+
@@ -75,7 +79,7 @@ Rank Pool - Hashrate + Hashrate Blocks Empty Blocks @@ -85,8 +89,8 @@ {{ pool.rank }} -
{{ pool.name }} - {{ pool.lastEstimatedHashrate }} {{ + {{ pool.name }} + {{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }} {{ pool['blockText'] }} {{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%) @@ -95,7 +99,7 @@ All miners - {{ miningStats.lastEstimatedHashrate}} {{ + {{ miningStats.lastEstimatedHashrate}} {{ miningStats.miningUnits.hashrateUnit }} {{ miningStats.blockCount }} {{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio @@ -129,4 +133,4 @@

- + \ No newline at end of file diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.scss b/frontend/src/app/components/pool-ranking/pool-ranking.component.scss index 5dddabe80..2d253eef6 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.scss +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.scss @@ -28,7 +28,7 @@ width: 100%; height: 100%; max-height: 270px; - @media (max-width: 767.98px) { + @media (max-width: 485px) { max-height: 200px; } } @@ -93,17 +93,8 @@ } .item { width: 50%; - margin: 0px auto 10px; display: inline-block; - @media (min-width: 485px) { - margin: 0px auto 10px; - } - @media (min-width: 785px) { - margin: 0px auto 0px; - } - &:last-child { - margin: 0px auto 0px; - } + margin: 0px auto 20px; &:nth-child(2) { order: 2; @media (min-width: 485px) { diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts index 0722466fc..95c2be2b7 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { EChartsOption, PieSeriesOption } from 'echarts'; @@ -10,6 +10,7 @@ import { StorageService } from '../..//services/storage.service'; import { MiningService, MiningStats } from '../../services/mining.service'; import { StateService } from '../../services/state.service'; import { chartColors, poolsColor } from 'src/app/app.constants'; +import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; @Component({ selector: 'app-pool-ranking', @@ -18,20 +19,20 @@ import { chartColors, poolsColor } from 'src/app/app.constants'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class PoolRankingComponent implements OnInit { - @Input() widget: boolean = false; + @Input() widget = false; - poolsWindowPreference: string; + miningWindowPreference: string; radioGroupForm: FormGroup; isLoading = true; chartOptions: EChartsOption = {}; chartInitOptions = { renderer: 'svg', - width: 'auto', - height: 'auto', }; chartInstance: any = undefined; + @HostBinding('attr.dir') dir = 'ltr'; + miningStatsObservable$: Observable; constructor( @@ -47,13 +48,13 @@ export class PoolRankingComponent implements OnInit { ngOnInit(): void { if (this.widget) { - this.poolsWindowPreference = '1w'; + this.miningWindowPreference = '1w'; } else { this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`); - this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w'; + this.miningWindowPreference = this.miningService.getDefaultTimespan('24h'); } - this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference }); - this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference); + this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); + this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); // When... this.miningStatsObservable$ = combineLatest([ @@ -66,12 +67,12 @@ export class PoolRankingComponent implements OnInit { // ...or we change the timespan this.radioGroupForm.get('dateSpan').valueChanges .pipe( - startWith(this.poolsWindowPreference), // (trigger when the page loads) + startWith(this.miningWindowPreference), // (trigger when the page loads) tap((value) => { if (!this.widget) { - this.storageService.setValue('poolsWindowPreference', value); + this.storageService.setValue('miningWindowPreference', value); } - this.poolsWindowPreference = value; + this.miningWindowPreference = value; }) ) ]) @@ -79,7 +80,7 @@ export class PoolRankingComponent implements OnInit { .pipe( switchMap(() => { this.isLoading = true; - return this.miningService.getMiningStats(this.poolsWindowPreference) + return this.miningService.getMiningStats(this.miningWindowPreference) .pipe( catchError((e) => of(this.getEmptyMiningStat())) ); @@ -149,7 +150,7 @@ export class PoolRankingComponent implements OnInit { }, borderColor: '#000', formatter: () => { - if (this.poolsWindowPreference === '24h') { + if (this.miningWindowPreference === '24h') { return `${pool.name} (${pool.share}%)
` + pool.lastEstimatedHashrate.toString() + ' PH/s' + `
` + pool.blockCount.toString() + ` blocks`; @@ -159,7 +160,7 @@ export class PoolRankingComponent implements OnInit { } } }, - data: pool.poolId, + data: pool.slug, } as PieSeriesOption); }); @@ -185,7 +186,7 @@ export class PoolRankingComponent implements OnInit { }, borderColor: '#000', formatter: () => { - if (this.poolsWindowPreference === '24h') { + if (this.miningWindowPreference === '24h') { return `${'Other'} (${totalShareOther.toFixed(2)}%)
` + totalEstimatedHashrateOther.toString() + ' PH/s' + `
` + totalBlockOther.toString() + ` blocks`; @@ -202,30 +203,6 @@ export class PoolRankingComponent implements OnInit { } prepareChartOptions(miningStats) { - let network = this.stateService.network; - if (network === '') { - network = 'bitcoin'; - } - network = network.charAt(0).toUpperCase() + network.slice(1); - - let radius: any[] = ['20%', '80%']; - let top: number = 0; let height = undefined; - if (this.isMobile() && this.widget) { - top = -30; - height = 270; - radius = ['10%', '50%']; - } else if (this.isMobile() && !this.widget) { - top = -40; - height = 300; - radius = ['10%', '50%']; - } else if (this.widget) { - radius = ['15%', '60%']; - top = -20; - height = 330; - } else { - top = 0; - } - this.chartOptions = { animation: false, color: chartColors, @@ -237,12 +214,11 @@ export class PoolRankingComponent implements OnInit { }, series: [ { + zlevel: 0, minShowLabelAngle: 3.6, - top: top, - height: height, name: 'Mining pool', type: 'pie', - radius: radius, + radius: ['20%', '80%'], data: this.generatePoolsChartSerieData(miningStats), labelLine: { lineStyle: { @@ -284,7 +260,8 @@ export class PoolRankingComponent implements OnInit { return; } this.zone.run(() => { - this.router.navigate(['/mining/pool/', e.data.data]); + const url = new RelativeUrlPipe(this.stateService).transform(`/mining/pool/${e.data.data}`); + this.router.navigate([url]); }); }); } diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index 240648e2c..c51360a2d 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -1,5 +1,6 @@
+
-
- + +
+
- - - + + + + + + - - - + + + + + + - - -
Tags -
+ + +
Tags +
{{ poolStats.pool.regexes }}
+
+ Tags +
{{ poolStats.pool.regexes }}
Addresses -
- {{ - address }}
+ + +
Addresses + + {{ poolStats.pool.addresses[0] }} + +
+ + +
+
+ Addresses +
+ + + {{ poolStats.pool.addresses[0] | shortenString: 40 }} + +
~
-
+ +
- - - + + + + + - - - + + + + + + + + + + + + + + + + + + +
Mined Blocks{{ formatNumber(poolStats.blockCount, this.locale, '1.0-0') }}
Hashrate (24h) + + + + + + + + + + + + + + + +
Estimated + Reported + Luck
{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}{{ poolStats.reportedHashrate | amountShortener : 1 : 'H/s' }}{{ formatNumber(poolStats.luck, this.locale, '1.2-2') }}%
+
Empty Blocks{{ formatNumber(poolStats.emptyBlocks, this.locale, '1.0-0') }}
+ Hashrate (24h) + + + + + + + + + + + + + + + +
Estimated + Reported + Luck
{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}{{ poolStats.reportedHashrate | amountShortener : 1 : 'H/s' }}{{ formatNumber(poolStats.luck, this.locale, '1.2-2') }}%
+
~~
Mined Blocks + + + + + + + + + + + + + +
24h1wAll
{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 * + poolStats.blockShare['24h'], this.locale, '1.0-0') }}%){{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 * + poolStats.blockShare['1w'], this.locale, '1.0-0') }}%){{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 * + poolStats.blockShare['all'], this.locale, '1.0-0') }}%)
+
+ Mined Blocks + + + + + + + + + + + + + +
24h1wAll
{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 * + poolStats.blockShare['24h'], this.locale, '1.0-0') }}%){{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 * + poolStats.blockShare['1w'], this.locale, '1.0-0') }}%){{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 * + poolStats.blockShare['all'], this.locale, '1.0-0') }}%)
+
+
+ + ~ + + +
~
+
+ +
- - - - - - - - - - - - - - - - - - - - - - - + +
HeightTimestampMined - Coinbase Tag - RewardFeesTxsSize
- {{ block.height - }} - - ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} - - - - - {{ block.extras.coinbaseRaw | hex2ascii }} - - - - - - - {{ block.tx_count | number }} - -
-
-
-
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + @@ -155,6 +158,9 @@ {{ vout.scriptpubkey_address | shortenString : 16 }}{{ vout.scriptpubkey_address | shortenString : 35 }} +
+ +
Peg-out to diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 3a028adc8..95ff4aa33 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -120,7 +120,7 @@
HeightTimestampMined + Coinbase Tag + RewardFeesTxsSize
+ {{ block.height + }} + + ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} + + + + + {{ block.extras.coinbaseRaw | hex2ascii }} + + + + + + + {{ block.tx_count | number }} + +
+
+
+
+
HeightTimestampMined + Coinbase Tag + RewardFeesTxsSize
@@ -143,56 +305,184 @@ +
-

+

+
+

-
- + +
+
- - - + + - - - + - - - + + + + + + + + + +
Tags + + +
Tags
Addresses -
+ +
+ Tags +
~
Addresses +
+
+
+
+
+ Addresses +
+
+
+
+
+
+
-
- + +
+
- - - + + - - - + + + + + + + + + + + +
Mined Blocks -
+ + +
Hashrate (24h) + + + + + + + + + + + + + +
Estimated + Reported + Luck
+
+
+
+
+
+
Empty Blocks -
+ +
+ Hashrate (24h) + + + + + + + + + + + + + +
Estimated + Reported + Luck
+
+
+
+
+
+
Mined Blocks + + + + + + + + + + + + + +
24h1wAll
+
+
+
+
+
+
+
+ Mined Blocks + + + + + + + + + + + + + +
24h1wAll
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/pool/pool.component.scss b/frontend/src/app/components/pool/pool.component.scss index 2a06de54a..9103f38f5 100644 --- a/frontend/src/app/components/pool/pool.component.scss +++ b/frontend/src/app/components/pool/pool.component.scss @@ -31,6 +31,14 @@ } } +.chart { + margin-bottom: 20px; + @media (max-width: 768px) { + margin-bottom: 10px; + } + height: 400px; +} + div.scrollable { width: 100%; height: 100%; @@ -42,16 +50,30 @@ div.scrollable { .box { padding-bottom: 5px; + @media (min-width: 767.98px) { + min-height: 187px; + } } .label { - width: 35%; + width: 25%; + @media (min-width: 767.98px) { + vertical-align: middle; + } + @media (max-width: 767.98px) { + font-weight: bold; + } +} +.label.addresses { + vertical-align: top; + padding-top: 25px; } .data { - text-align: left; - padding-left: 25%; - @media (max-width: 991px) { + text-align: right; + padding-left: 5%; + @media (max-width: 992px) { + text-align: left; padding-left: 12px; } @media (max-width: 450px) { @@ -103,10 +125,6 @@ div.scrollable { } } -.fees { - width: 0%; -} - .size { width: 12%; @media (max-width: 1000px) { @@ -132,15 +150,13 @@ div.scrollable { text-align: left; } -.right-mobile { - @media (max-width: 450px) { - text-align: right; - } -} - .skeleton-loader { max-width: 200px; } +.skeleton-loader.data { + max-width: 70px; +} + .loadingGraphs { position: absolute; @@ -151,3 +167,41 @@ div.scrollable { top: 600px; } } + +.small-button { + height: 20px; + font-size: 10px; + padding-top: 0; + padding-bottom: 0; + outline: none; + box-shadow: none; +} +.small-button.mobile { + transform: translateY(-20px); + @media (min-width: 767.98px) { + transform: translateY(-17px); + } +} + +.block-count-title { + color: #4a68b9; + font-size: 14px; + text-align: left; + @media (max-width: 767.98px) { + text-align: center; + } +} + +.table-data tr { + background-color: transparent; +} +.table-data td { + text-align: left; + @media (max-width: 767.98px) { + text-align: center; + } +} + +.taller-row { + height: 75px; +} \ No newline at end of file diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index dee4a9713..4d41c2437 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -2,12 +2,13 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } import { ActivatedRoute } from '@angular/router'; import { EChartsOption, graphic } from 'echarts'; import { BehaviorSubject, Observable, timer } from 'rxjs'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; import { BlockExtended, PoolStat } from 'src/app/interfaces/node-api.interface'; import { ApiService } from 'src/app/services/api.service'; import { StateService } from 'src/app/services/state.service'; import { selectPowerOfTen } from 'src/app/bitcoin.utils'; import { formatNumber } from '@angular/common'; +import { SeoService } from 'src/app/services/seo.service'; @Component({ selector: 'app-pool', @@ -19,6 +20,8 @@ export class PoolComponent implements OnInit { @Input() right: number | string = 45; @Input() left: number | string = 75; + gfg = true; + formatNumber = formatNumber; poolStats$: Observable; blocks$: Observable; @@ -27,43 +30,45 @@ export class PoolComponent implements OnInit { chartOptions: EChartsOption = {}; chartInitOptions = { renderer: 'svg', - width: 'auto', - height: 'auto', }; blocks: BlockExtended[] = []; - poolId: number = undefined; + slug: string = undefined; - loadMoreSubject: BehaviorSubject = new BehaviorSubject(this.poolId); + loadMoreSubject: BehaviorSubject = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height); constructor( @Inject(LOCALE_ID) public locale: string, private apiService: ApiService, private route: ActivatedRoute, public stateService: StateService, + private seoService: SeoService, ) { } ngOnInit(): void { - this.poolStats$ = this.route.params.pipe(map((params) => params.poolId)) + this.poolStats$ = this.route.params.pipe(map((params) => params.slug)) .pipe( - switchMap((poolId: any) => { + switchMap((slug: any) => { this.isLoading = true; - this.poolId = poolId; - this.loadMoreSubject.next(this.poolId); - return this.apiService.getPoolHashrate$(this.poolId) + this.slug = slug; + return this.apiService.getPoolHashrate$(this.slug) .pipe( switchMap((data) => { this.isLoading = false; this.prepareChartOptions(data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate])); - return poolId; + return [slug]; }), ); }), - switchMap(() => { - return this.apiService.getPoolStats$(this.poolId); + switchMap((slug) => { + return this.apiService.getPoolStats$(slug); + }), + tap(() => { + this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height); }), map((poolStats) => { + this.seoService.setTitle(poolStats.pool.name); let regexes = '"'; for (const regex of poolStats.pool.regexes) { regexes += regex + '", "'; @@ -71,6 +76,10 @@ export class PoolComponent implements OnInit { poolStats.pool.regexes = regexes.slice(0, -3); poolStats.pool.addresses = poolStats.pool.addresses; + if (poolStats.reportedHashrate) { + poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100; + } + return Object.assign({ logo: `./resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg' }, poolStats); @@ -79,21 +88,37 @@ export class PoolComponent implements OnInit { this.blocks$ = this.loadMoreSubject .pipe( + distinctUntilChanged(), switchMap((flag) => { - if (this.poolId === undefined) { + if (this.slug === undefined) { return []; } - return this.apiService.getPoolBlocks$(this.poolId, this.blocks[this.blocks.length - 1]?.height); + return this.apiService.getPoolBlocks$(this.slug, this.blocks[this.blocks.length - 1]?.height); }), tap((newBlocks) => { this.blocks = this.blocks.concat(newBlocks); }), - map(() => this.blocks) + map(() => this.blocks), + share(), ); } prepareChartOptions(data) { + let title: object; + if (data.length === 0) { + title = { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: `No data`, + left: 'center', + top: 'center' + }; + } + this.chartOptions = { + title: title, animation: false, color: [ new graphic.LinearGradient(0, 0, 0, 0.65, [ @@ -124,7 +149,7 @@ export class PoolComponent implements OnInit { align: 'left', }, borderColor: '#000', - formatter: function(ticks: any[]) { + formatter: function (ticks: any[]) { let hashratePowerOfTen: any = selectPowerOfTen(1); let hashrate = ticks[0].data[1]; @@ -142,6 +167,9 @@ export class PoolComponent implements OnInit { xAxis: { type: 'time', splitNumber: (this.isMobile()) ? 5 : 10, + axisLabel: { + hideOverlap: true, + } }, yAxis: [ { @@ -164,6 +192,7 @@ export class PoolComponent implements OnInit { ], series: [ { + zlevel: 0, name: 'Hashrate', showSymbol: false, symbol: 'none', @@ -174,6 +203,34 @@ export class PoolComponent implements OnInit { }, }, ], + dataZoom: data.length === 0 ? undefined : [{ + type: 'inside', + realtime: true, + zoomLock: true, + maxSpan: 100, + minSpan: 10, + moveOnMouseMove: false, + }, { + fillerColor: '#aaaaff15', + borderColor: '#ffffff88', + showDetail: false, + show: true, + type: 'slider', + brushSelect: false, + realtime: true, + bottom: 0, + left: 20, + right: 15, + selectedDataBackground: { + lineStyle: { + color: '#fff', + opacity: 0.45, + }, + areaStyle: { + opacity: 0, + }, + }, + }], }; } @@ -182,7 +239,7 @@ export class PoolComponent implements OnInit { } loadMore() { - this.loadMoreSubject.next(this.poolId); + this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height); } trackByBlock(index: number, block: BlockExtended) { diff --git a/frontend/src/app/components/privacy-policy/privacy-policy.component.html b/frontend/src/app/components/privacy-policy/privacy-policy.component.html index de2ec69ba..7df3db8c1 100644 --- a/frontend/src/app/components/privacy-policy/privacy-policy.component.html +++ b/frontend/src/app/components/privacy-policy/privacy-policy.component.html @@ -11,7 +11,7 @@
-

The mempool.space website, the liquid.network website, the bisq.markets website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from AS142052.

+

The mempool.space website, the liquid.network website, the bisq.markets website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from AS142052.

This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.

diff --git a/frontend/src/app/components/reward-stats/reward-stats.component.html b/frontend/src/app/components/reward-stats/reward-stats.component.html index 861921ca6..26c791e1a 100644 --- a/frontend/src/app/components/reward-stats/reward-stats.component.html +++ b/frontend/src/app/components/reward-stats/reward-stats.component.html @@ -2,7 +2,7 @@
Miners Reward
-
@@ -14,10 +14,10 @@
Reward Per Tx
-
- {{ rewardStats.rewardPerTx | amountShortener }} + {{ rewardStats.rewardPerTx | amountShortener: 2 }} sats/tx
@@ -27,9 +27,9 @@
Average Fee
-
-
{{ rewardStats.feePerTx | amountShortener }} +
{{ rewardStats.feePerTx | amountShortener: 2 }} sats/tx
@@ -65,55 +65,3 @@
- - \ No newline at end of file diff --git a/frontend/src/app/components/reward-stats/reward-stats.component.ts b/frontend/src/app/components/reward-stats/reward-stats.component.ts index dd466985e..582796fa1 100644 --- a/frontend/src/app/components/reward-stats/reward-stats.component.ts +++ b/frontend/src/app/components/reward-stats/reward-stats.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; -import { map, skip, switchMap } from 'rxjs/operators'; +import { concat, Observable } from 'rxjs'; +import { map, switchMap, tap } from 'rxjs/operators'; import { ApiService } from 'src/app/services/api.service'; import { StateService } from 'src/app/services/state.service'; @@ -12,25 +12,39 @@ import { StateService } from 'src/app/services/state.service'; }) export class RewardStatsComponent implements OnInit { public $rewardStats: Observable; + private lastBlockHeight: number; constructor(private apiService: ApiService, private stateService: StateService) { } ngOnInit(): void { - this.$rewardStats = this.stateService.blocks$ + this.$rewardStats = concat( + // We fetch the latest reward stats when the page load and + // wait for the API response before listening to websocket blocks + this.apiService.getRewardStats$() + .pipe( + tap((stats) => { + this.lastBlockHeight = stats.endBlock; + }) + ), + // Or when we receive a newer block, newer than the latest reward stats api call + this.stateService.blocks$ + .pipe( + switchMap((block) => { + if (block[0].height <= this.lastBlockHeight) { + return []; // Return an empty stream so the last pipe is not executed + } + this.lastBlockHeight = block[0].height; + return this.apiService.getRewardStats$(); + }) + ) + ) .pipe( - // (we always receives some blocks at start so only trigger for the last one) - skip(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT - 1), - switchMap(() => { - return this.apiService.getRewardStats$() - .pipe( - map((stats) => { - return { - totalReward: stats.totalReward, - rewardPerTx: stats.totalReward / stats.totalTx, - feePerTx: stats.totalFee / stats.totalTx, - }; - }) - ); + map((stats) => { + return { + totalReward: stats.totalReward, + rewardPerTx: stats.totalReward / stats.totalTx, + feePerTx: stats.totalFee / stats.totalTx, + }; }) ); } diff --git a/frontend/src/app/components/sponsor/sponsor.component.html b/frontend/src/app/components/sponsor/sponsor.component.html index 001ea1272..b0218f78b 100644 --- a/frontend/src/app/components/sponsor/sponsor.component.html +++ b/frontend/src/app/components/sponsor/sponsor.component.html @@ -1,5 +1,9 @@ - diff --git a/frontend/src/app/components/sponsor/sponsor.component.scss b/frontend/src/app/components/sponsor/sponsor.component.scss index d8cf49460..11b502143 100644 --- a/frontend/src/app/components/sponsor/sponsor.component.scss +++ b/frontend/src/app/components/sponsor/sponsor.component.scss @@ -15,8 +15,8 @@ } .card { - width: 220px; - height: 200px; + width: 240px; + height: 220px; background-color: #1d1f31; border: 2px solid #1d1f31; cursor: pointer; @@ -46,7 +46,9 @@ &:hover { background-color: #5058926b; border: 2px solid #505892; - transform: scale(1.11) translateY(-10px); + transform: scale(1.1) translateY(-10px); + margin-top: 70px; + .card-header { background-color: #505892; } @@ -54,7 +56,7 @@ } .donation-form { - max-width: 240px; + max-width: 280px; margin: auto; button { width: 100%; @@ -72,14 +74,28 @@ justify-content: center; } +.middle-card { + width: 280px; + height: 260px; + margin-top: 40px; + &:hover { + margin-top: 50px; + } +} + .shiny-border { background-color: #5058926b; border: 2px solid #505892; - transform: scale(1.11) translateY(-10px); + transform: scale(1.1) translateY(-10px); + margin-top: 70px; box-shadow: 0px 0px 100px #9858ff52; .card-header { background-color: #505892; } + + &.middle-card { + margin-top: 50px; + } } .input-group { @@ -106,4 +122,16 @@ margin-left: 10px; } } +} + +.logo { + margin: 30px; +} + +.card-body { + align-items: center; + display: flex; + justify-content: center; + flex-direction: column; + height: 100%; } \ No newline at end of file diff --git a/frontend/src/app/components/terms-of-service/terms-of-service.component.html b/frontend/src/app/components/terms-of-service/terms-of-service.component.html index 44643c855..35a6413bd 100644 --- a/frontend/src/app/components/terms-of-service/terms-of-service.component.html +++ b/frontend/src/app/components/terms-of-service/terms-of-service.component.html @@ -11,7 +11,7 @@
-

The mempool.space website, the liquid.network website, the bisq.markets website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from AS142052.

+

The mempool.space website, the liquid.network website, the bisq.markets website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from AS142052.

This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.

diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index a0c92cbb4..1b6844cda 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -200,7 +200,7 @@ -
+

Details

diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 3d9c67b17..eded208bd 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -111,7 +111,10 @@
P2WSH witness scriptP2TR tapscriptP2WSH witness script
{{ block.height }} - + {{ block.extras.pool.name }} diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 786fd6687..4998a0d70 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -71,6 +71,7 @@ export interface SinglePoolStats { lastEstimatedHashrate: string; emptyBlockRatio: string; logo: string; + slug: string; } export interface PoolsStats { blockCount: number; @@ -92,8 +93,19 @@ export interface PoolInfo { } export interface PoolStat { pool: PoolInfo; - blockCount: number; - emptyBlocks: number; + blockCount: { + all: number, + '24h': number, + '1w': number, + }; + blockShare: { + all: number, + '24h': number, + '1w': number, + }; + estimatedHashrate: number; + reportedHashrate: number; + luck?: number; } export interface BlockExtension { @@ -107,6 +119,7 @@ export interface BlockExtension { pool?: { id: number; name: string; + slug: string; } stage?: number; // Frontend only @@ -117,6 +130,8 @@ export interface BlockExtended extends Block { } export interface RewardStats { + startBlock: number; + endBlock: number; totalReward: number; totalFee: number; totalTx: number; diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 92068c44e..16a8d21d5 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -132,17 +132,17 @@ export class ApiService { ); } - getPoolStats$(poolId: number): Observable { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}`); + getPoolStats$(slug: string): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${slug}`); } - getPoolHashrate$(poolId: number): Observable { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}/hashrate`); + getPoolHashrate$(slug: string): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${slug}/hashrate`); } - getPoolBlocks$(poolId: number, fromHeight: number): Observable { + getPoolBlocks$(slug: string, fromHeight: number): Observable { return this.httpClient.get( - this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}/blocks` + + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${slug}/blocks` + (fromHeight !== undefined ? `/${fromHeight}` : '') ); } @@ -154,13 +154,6 @@ export class ApiService { ); } - getHistoricalDifficulty$(interval: string | undefined): Observable { - return this.httpClient.get( - this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty` + - (interval !== undefined ? `/${interval}` : '') - ); - } - getHistoricalHashrate$(interval: string | undefined): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` + @@ -175,6 +168,20 @@ export class ApiService { ); } + getHistoricalBlockFees$(interval: string | undefined) : Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/fees` + + (interval !== undefined ? `/${interval}` : '') + ); + } + + getHistoricalBlockRewards$(interval: string | undefined) : Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/rewards` + + (interval !== undefined ? `/${interval}` : '') + ); + } + getRewardStats$(blockCount: number = 144): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`); } diff --git a/frontend/src/app/services/mining.service.ts b/frontend/src/app/services/mining.service.ts index 68f7e9da1..0480b09cd 100644 --- a/frontend/src/app/services/mining.service.ts +++ b/frontend/src/app/services/mining.service.ts @@ -4,6 +4,7 @@ import { map } from 'rxjs/operators'; import { PoolsStats, SinglePoolStats } from '../interfaces/node-api.interface'; import { ApiService } from '../services/api.service'; import { StateService } from './state.service'; +import { StorageService } from './storage.service'; export interface MiningUnits { hashrateDivider: number; @@ -28,8 +29,12 @@ export class MiningService { constructor( private stateService: StateService, private apiService: ApiService, + private storageService: StorageService, ) { } + /** + * Generate pool ranking stats + */ public getMiningStats(interval: string): Observable { return this.apiService.listPools$(interval).pipe( map(pools => this.generateMiningStats(pools)) @@ -63,6 +68,20 @@ export class MiningService { }; } + /** + * Get the default selection timespan, cap with `min` + */ + public getDefaultTimespan(min: string): string { + const timespans = [ + '24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all' + ]; + const preference = this.storageService.getValue('miningWindowPreference') ?? '1w'; + if (timespans.indexOf(preference) < timespans.indexOf(min)) { + return min; + } + return preference; + } + private generateMiningStats(stats: PoolsStats): MiningStats { const miningUnits = this.getMiningUnits(); const hashrateDivider = miningUnits.hashrateDivider; diff --git a/frontend/src/app/services/storage.service.ts b/frontend/src/app/services/storage.service.ts index aa848a21c..f3ea694b2 100644 --- a/frontend/src/app/services/storage.service.ts +++ b/frontend/src/app/services/storage.service.ts @@ -7,21 +7,21 @@ import { Router, ActivatedRoute } from '@angular/router'; export class StorageService { constructor(private router: Router, private route: ActivatedRoute) { this.setDefaultValueIfNeeded('graphWindowPreference', '2h'); - this.setDefaultValueIfNeeded('poolsWindowPreference', '1w'); + this.setDefaultValueIfNeeded('miningWindowPreference', '1w'); } setDefaultValueIfNeeded(key: string, defaultValue: string) { - let graphWindowPreference: string = this.getValue(key); + const graphWindowPreference: string = this.getValue(key); if (graphWindowPreference === null) { // First visit to mempool.space if (this.router.url.includes('graphs') && key === 'graphWindowPreference' || - this.router.url.includes('pools') && key === 'poolsWindowPreference' + this.router.url.includes('pools') && key === 'miningWindowPreference' ) { this.setValue(key, this.route.snapshot.fragment ? this.route.snapshot.fragment : defaultValue); } else { this.setValue(key, defaultValue); } } else if (this.router.url.includes('graphs') && key === 'graphWindowPreference' || - this.router.url.includes('pools') && key === 'poolsWindowPreference' + this.router.url.includes('pools') && key === 'miningWindowPreference' ) { // Visit a different graphs#fragment from last visit if (this.route.snapshot.fragment !== null && graphWindowPreference !== this.route.snapshot.fragment) { diff --git a/frontend/src/app/shared/pipes/amount-shortener.pipe.ts b/frontend/src/app/shared/pipes/amount-shortener.pipe.ts index 529c6be79..a31a5712e 100644 --- a/frontend/src/app/shared/pipes/amount-shortener.pipe.ts +++ b/frontend/src/app/shared/pipes/amount-shortener.pipe.ts @@ -4,12 +4,14 @@ import { Pipe, PipeTransform } from '@angular/core'; name: 'amountShortener' }) export class AmountShortenerPipe implements PipeTransform { - transform(num: number, ...args: number[]): unknown { + transform(num: number, ...args: any[]): unknown { + const digits = args[0] || 1; + const unit = args[1] || undefined; + if (num < 1000) { - return num; + return num.toFixed(digits); } - const digits = args[0] || 1; const lookup = [ { value: 1, symbol: '' }, { value: 1e3, symbol: 'k' }, @@ -20,7 +22,12 @@ export class AmountShortenerPipe implements PipeTransform { { value: 1e18, symbol: 'E' } ]; const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; - var item = lookup.slice().reverse().find((item) => num >= item.value); - return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0'; + const item = lookup.slice().reverse().find((item) => num >= item.value); + + if (unit !== undefined) { + return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + ' ' + item.symbol + unit : '0'; + } else { + return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0'; + } } } \ No newline at end of file diff --git a/frontend/src/app/shared/pipes/asm-styler/asm-styler.pipe.ts b/frontend/src/app/shared/pipes/asm-styler/asm-styler.pipe.ts index 14626a1de..54a02e405 100644 --- a/frontend/src/app/shared/pipes/asm-styler/asm-styler.pipe.ts +++ b/frontend/src/app/shared/pipes/asm-styler/asm-styler.pipe.ts @@ -264,6 +264,7 @@ export class AsmStylerPipe implements PipeTransform { case 'LESSTHAN': case 'GREATERTHAN': case 'LESSTHANOREQUAL': + case 'GREATERTHANOREQUAL': case 'MIN': case 'MAX': case 'WITHIN': @@ -279,12 +280,13 @@ export class AsmStylerPipe implements PipeTransform { case 'CHECKSIG': case 'CHECKSIGVERIFY': case 'CHECKMULTISIG': - case 'CHCEKMULTISIGVERIFY': + case 'CHECKMULTISIGVERIFY': + case 'CHECKSIGADD': style = 'crypto'; break; - case 'CHECKLOCKTIMEVERIFY': - case 'CHECKSEQUENCEVERIFY': + case 'CLTV': + case 'CSV': style = 'locktime'; break; diff --git a/frontend/src/app/shared/pipes/no-sanitize.pipe.ts b/frontend/src/app/shared/pipes/no-sanitize.pipe.ts new file mode 100644 index 000000000..039eafb76 --- /dev/null +++ b/frontend/src/app/shared/pipes/no-sanitize.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +@Pipe({ name: 'noSanitize' }) +export class NoSanitizePipe implements PipeTransform { + constructor(private domSanitizer: DomSanitizer) { } + + transform(html: string): SafeHtml { + return this.domSanitizer.bypassSecurityTrustHtml(html); + } +} diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 54476206c..aee1456e4 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -50,7 +50,6 @@ $dropdown-link-active-bg: #11131f; html, body { height: 100%; - scroll-behavior: smooth; } body { @@ -66,6 +65,11 @@ body { .container-xl { padding-bottom: 60px; } +.full-height { + @media (max-width: 767.98px) { + min-height: 100vh; + } +} :focus { outline: none !important; @@ -655,10 +659,6 @@ h1, h2, h3 { margin-top: 0.75rem !important; } -.tooltip-inner { - max-width: inherit; -} - .alert-mempool { color: #ffffff; background-color: #653b9c; @@ -698,6 +698,16 @@ th { margin-right: 0px; text-align: right; } + + .nav-pills { + @extend .nav-pills; + display: inline-block; + } + + .description { + direction: rtl; + } + .dropdown { margin-right: 1rem; margin-left: 0; @@ -712,12 +722,29 @@ th { left: 0px; right: auto; } - .fa-arrow-alt-circle-right { - @extend .fa-arrow-alt-circle-right; + .fa-circle-right { + @extend .fa-circle-right; -webkit-transform: scaleX(-1); transform: scaleX(-1); } + .btn.ml-2 { + margin-right: 0.5rem !important; + } + + .pool-name { + @extend .pool-name; + padding-right: 10px; + } + + .endpoint-container { + @extend .endpoint-container; + .section-header { + @extend .section-header; + text-align: left; + } + } + .table td { text-align: right; .fiat { @@ -809,6 +836,14 @@ th { } } + .full-container { + @extend .full-container; + .formRadioGroup { + @extend .formRadioGroup; + direction: ltr; + } + } + .mempool-graph { @extend .mempool-graph; direction: ltr; diff --git a/production/install b/production/install index e4d3f14b8..4db418693 100755 --- a/production/install +++ b/production/install @@ -321,7 +321,7 @@ LIQUIDTESTNET_ASSET_REGISTRY_DB_NAME=asset_registry_testnet_db # packages needed for mempool ecosystem DEBIAN_PKG=() DEBIAN_PKG+=(zsh vim curl screen openssl python3) -DEBIAN_PKG+=(build-essential git git-lfs clang cmake) +DEBIAN_PKG+=(build-essential git git-lfs clang cmake jq) DEBIAN_PKG+=(autotools-dev autoconf automake pkg-config bsdmainutils) DEBIAN_PKG+=(libevent-dev libdb-dev libssl-dev libtool-dev autotools-dev) DEBIAN_PKG+=(libboost-system-dev libboost-filesystem-dev libboost-chrono-dev libboost-test-dev libboost-thread-dev) @@ -330,7 +330,7 @@ DEBIAN_PKG+=(nodejs npm mariadb-server nginx-core python-certbot-nginx rsync ufw # packages needed for mempool ecosystem FREEBSD_PKG=() FREEBSD_PKG+=(zsh sudo git screen curl wget calc neovim) -FREEBSD_PKG+=(openssh-portable py38-pip rust llvm90) +FREEBSD_PKG+=(openssh-portable py38-pip rust llvm90 jq) FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf) FREEBSD_PKG+=(nginx rsync py38-certbot-nginx mariadb105-server keybase) diff --git a/production/newsyslog-mempool-backend.conf b/production/newsyslog-mempool-backend.conf index 8a3aab320..5c96da47a 100644 --- a/production/newsyslog-mempool-backend.conf +++ b/production/newsyslog-mempool-backend.conf @@ -1,2 +1,2 @@ -/var/log/mempool 640 10 * 168 J -/var/log/mempool.debug 640 10 1000 * J +/var/log/mempool 640 10 * @T00 C +/var/log/mempool.debug 640 10 * @T00 C diff --git a/production/nginx-cache-warmer b/production/nginx-cache-warmer index f9b89b7fa..3c3204493 100755 --- a/production/nginx-cache-warmer +++ b/production/nginx-cache-warmer @@ -1,5 +1,7 @@ #!/usr/bin/env zsh hostname=$(hostname) +slugs=(`curl -sSL https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json | jq -r '.slugs[]'`) + while true do for url in / \ '/api/v1/statistics/2h' \ @@ -34,17 +36,37 @@ do for url in / \ '/api/v1/mining/hashrate/pools/3y' \ '/api/v1/mining/hashrate/pools/all' \ '/api/v1/mining/reward-stats/144' \ + '/api/v1/mining/blocks-extras' \ + '/api/v1/mining/blocks/fees/24h' \ + '/api/v1/mining/blocks/fees/3d' \ + '/api/v1/mining/blocks/fees/1w' \ + '/api/v1/mining/blocks/fees/1m' \ + '/api/v1/mining/blocks/fees/3m' \ + '/api/v1/mining/blocks/fees/6m' \ + '/api/v1/mining/blocks/fees/1y' \ + '/api/v1/mining/blocks/fees/2y' \ + '/api/v1/mining/blocks/fees/3y' \ + '/api/v1/mining/blocks/fees/all' \ + '/api/v1/mining/blocks/rewards/24h' \ + '/api/v1/mining/blocks/rewards/3d' \ + '/api/v1/mining/blocks/rewards/1w' \ + '/api/v1/mining/blocks/rewards/1m' \ + '/api/v1/mining/blocks/rewards/3m' \ + '/api/v1/mining/blocks/rewards/6m' \ + '/api/v1/mining/blocks/rewards/1y' \ + '/api/v1/mining/blocks/rewards/2y' \ + '/api/v1/mining/blocks/rewards/3y' \ + '/api/v1/mining/blocks/rewards/all' \ do curl -s "https://${hostname}${url}" >/dev/null done - counter=1 - while [ $counter -le 134 ] + for slug in $slugs do - curl -s "https://${hostname}/api/v1/mining/pool/${counter}/hashrate" >/dev/null - curl -s "https://${hostname}/api/v1/mining/pool/${counter}" >/dev/null - ((counter++)) + curl -s "https://${hostname}/api/v1/mining/pool/${slug}" >/dev/null + curl -s "https://${hostname}/api/v1/mining/pool/${slug}/hashrate" >/dev/null + curl -s "https://${hostname}/api/v1/mining/pool/${slug}/blocks" >/dev/null done sleep 10 diff --git a/production/nginx/location-api.conf b/production/nginx/location-api.conf index 0a40ddc36..253033206 100644 --- a/production/nginx/location-api.conf +++ b/production/nginx/location-api.conf @@ -1,7 +1,7 @@ location /api/v1/statistics { try_files /dev/null @mempool-api-v1-warmcache; } -location /api/v1/mining/pools { +location /api/v1/mining { try_files /dev/null @mempool-api-v1-warmcache; } location /api/v1 {