From 1f6504898a6ceee4da59579444d09f2ad7fa40d9 Mon Sep 17 00:00:00 2001 From: Antoni Spaanderman <56turtle56@gmail.com> Date: Sat, 5 Feb 2022 16:50:10 +0100 Subject: [PATCH 01/65] detect lightning htlc and unilateral close --- .../address-labels.component.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) 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 bde749025..958b8cd81 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -46,15 +46,21 @@ export class AddressLabelsComponent implements OnInit { return; } - [ - // {regexp: /^OP_DUP OP_HASH160/, label: 'HTLC'}, - {regexp: /^OP_IF OP_PUSHBYTES_33 \w{33} OP_ELSE OP_PUSHBYTES_2 \w{2} OP_CSV OP_DROP/, label: 'Force Close'} - ].forEach((item) => { - if (item.regexp.test(this.vin.inner_witnessscript_asm)) { - this.lightning = item.label; - } - } - ); + // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs + 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_ENDIF$/) { + if (this.vin.witness[this.vin.witness.length - 2].length == 66) { + this.lightning = 'Revoked HTLC'; + } else { + this.lightning = 'HTLC'; + } + } if (this.lightning) { return; From 7565aa7a2592e5b3081bb31ca84e038bd1dfa77e Mon Sep 17 00:00:00 2001 From: Antoni Spaanderman <56turtle56@gmail.com> Date: Sat, 5 Feb 2022 16:58:41 +0100 Subject: [PATCH 02/65] fix indentation + detect htlc with `option_anchors` --- .../address-labels.component.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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 958b8cd81..fb827e65b 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -48,19 +48,19 @@ export class AddressLabelsComponent implements OnInit { // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs 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'; - } + 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_ENDIF$/) { - if (this.vin.witness[this.vin.witness.length - 2].length == 66) { - this.lightning = 'Revoked HTLC'; - } else { - this.lightning = 'HTLC'; - } - } + } 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$/) { + if (this.vin.witness[this.vin.witness.length - 2].length == 66) { + this.lightning = 'Revoked HTLC'; + } else { + this.lightning = 'HTLC'; + } + } if (this.lightning) { return; From 148e340ea6e2c463cff674231d1ec9811647fcf4 Mon Sep 17 00:00:00 2001 From: Antoni Spaanderman <56turtle56@gmail.com> Date: Sat, 5 Feb 2022 17:26:50 +0100 Subject: [PATCH 03/65] actually test htlc, fix indentation (again) and detect multisig --- .../address-labels.component.ts | 69 ++++++++++--------- 1 file changed, 36 insertions(+), 33 deletions(-) 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 fb827e65b..40db539b1 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -54,51 +54,54 @@ export class AddressLabelsComponent implements OnInit { 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$/) { - if (this.vin.witness[this.vin.witness.length - 2].length == 66) { - this.lightning = 'Revoked HTLC'; - } else { - this.lightning = 'HTLC'; - } + } 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'; + } else { + this.lightning = 'HTLC'; + } } if (this.lightning) { return; } - if (this.vin.inner_witnessscript_asm.indexOf('OP_CHECKMULTISIG') > -1) { - const matches = this.getMatches(this.vin.inner_witnessscript_asm, /OP_PUSHNUM_([0-9])/g, 1); - this.multisig = true; - this.multisigM = parseInt(matches[0], 10); - this.multisigN = parseInt(matches[1], 10); + this.detectMultisig(this.vin.inner_witnessscript_asm); + } - if (this.multisigM === 1 && this.multisigN === 1) { - this.multisig = false; - } + this.detectMultisig(this.vin.inner_redeemscript_asm); + } + + detectMultisig(script: string) { + const ops = script.split(' '); + if (ops.pop() != 'OP_CHECKMULTISIG') { + return; + } + const opN = ops.pop(); + if (!opN.startsWith('OP_PUSHNUM_')) { + return; + } + const n = parseInt(opN.match(/[0-9]+/)[0]); + // pop n public keys + for (var i = 0; i < n; i++) { + if (ops.pop().length != 66) { + return; + } + if (ops.pop() != 'OP_PUSHBYTES_33') { + return; } } - - if (this.vin.inner_redeemscript_asm && this.vin.inner_redeemscript_asm.indexOf('OP_CHECKMULTISIG') > -1) { - const matches = this.getMatches(this.vin.inner_redeemscript_asm, /OP_PUSHNUM_([0-9])/g, 1); - this.multisig = true; - this.multisigM = matches[0]; - this.multisigN = matches[1]; + const opM = ops.pop(); + if (!opM.startsWith('OP_PUSHNUM_')) { + return; } + const m = parseInt(opN.match(/[0-9]+/)[0]); + + this.multisig = true; + this.multisigM = m; + this.multisigN = n; } handleVout() { } - - getMatches(str: string, regex: RegExp, index: number) { - if (!index) { - index = 1; - } - const matches = []; - let match; - while (match = regex.exec(str)) { - matches.push(match[index]); - } - return matches; - } - } From bb8bfa0e3ad14d797aa1ac8600e7b6874f0caca9 Mon Sep 17 00:00:00 2001 From: Antoni Spaanderman <56turtle56@gmail.com> Date: Sat, 5 Feb 2022 17:29:42 +0100 Subject: [PATCH 04/65] copy paste moment --- .../app/components/address-labels/address-labels.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 40db539b1..3ec2d079b 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -95,7 +95,7 @@ export class AddressLabelsComponent implements OnInit { if (!opM.startsWith('OP_PUSHNUM_')) { return; } - const m = parseInt(opN.match(/[0-9]+/)[0]); + const m = parseInt(opM.match(/[0-9]+/)[0]); this.multisig = true; this.multisigM = m; From 9f3a3bd4d7c0215ae55967358754e5e1c0b5db5f Mon Sep 17 00:00:00 2001 From: Antoni Spaanderman <56turtle56@gmail.com> Date: Sun, 6 Feb 2022 12:41:37 +0100 Subject: [PATCH 05/65] also detect uncompressed pubkeys + fix errors --- .../address-labels/address-labels.component.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 3ec2d079b..75bbe7cba 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -73,8 +73,11 @@ export class AddressLabelsComponent implements OnInit { } detectMultisig(script: string) { + if (!script) { + return; + } const ops = script.split(' '); - if (ops.pop() != 'OP_CHECKMULTISIG') { + if (ops.length < 3 || ops.pop() != 'OP_CHECKMULTISIG') { return; } const opN = ops.pop(); @@ -82,12 +85,15 @@ export class AddressLabelsComponent implements OnInit { return; } const n = parseInt(opN.match(/[0-9]+/)[0]); + if (ops.length < n * 2 + 1) { + return; + } // pop n public keys for (var i = 0; i < n; i++) { - if (ops.pop().length != 66) { + if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop())) { return; } - if (ops.pop() != 'OP_PUSHBYTES_33') { + if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop())) { return; } } From 738381702fea116aef8ebff4d445deda6929859b Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 12 Feb 2022 00:15:13 +0400 Subject: [PATCH 06/65] Display top featured assets on Liquid dashboard --- .../app/dashboard/dashboard.component.html | 56 +++++++++++++++---- .../app/dashboard/dashboard.component.scss | 23 ++++++++ .../src/app/dashboard/dashboard.component.ts | 16 +++++- 3 files changed, 83 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index b3cbd5fa1..01a32ac7c 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -47,17 +47,36 @@
- -
- -
-
+ + +
+ +
+
+
+ + + + + + + + +
+ + + + + {{ group.name }} + {{ group.ticker }} +
+
@@ -158,6 +177,21 @@ + + + + + + + + +
+
+
+
+
+
+
diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss index 39ca2101a..23c9b226e 100644 --- a/frontend/src/app/dashboard/dashboard.component.scss +++ b/frontend/src/app/dashboard/dashboard.component.scss @@ -283,3 +283,26 @@ margin-right: -2px; font-size: 10px; } + +.assetIcon { + width: 28px; + height: 28px; +} + +.asset-title { + text-align: left; +} + +.asset-ticker { + color: grey; +} + +.asset-icon { + width: 54px; + height: 52px; +} + +.asset-table { + width: calc(100% - 20px); + margin-left: 1.25rem; +} diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 7a7e1fbf2..36ad73003 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; import { combineLatest, merge, Observable, of, timer } from 'rxjs'; -import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; +import { filter, map, scan, share, switchMap, take, tap } from 'rxjs/operators'; import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface'; import { ApiService } from '../services/api.service'; @@ -34,6 +34,7 @@ interface MempoolStatsData { }) export class DashboardComponent implements OnInit { collapseLevel: string; + featuredAssets$: Observable; network$: Observable; mempoolBlocksData$: Observable; mempoolInfoData$: Observable; @@ -124,6 +125,19 @@ export class DashboardComponent implements OnInit { }) ); + this.featuredAssets$ = this.apiService.listFeaturedAssets$() + .pipe( + take(5), + map((featured) => { + for (const feature of featured) { + if (feature.assets) { + feature.asset = feature.assets[0]; + } + } + return featured; + }) + ); + this.blocks$ = this.stateService.blocks$ .pipe( tap(([block]) => { From 294d7915e1d537691885099d68f0ddbff929b0de Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 13 Feb 2022 00:46:42 +0400 Subject: [PATCH 07/65] Liquid dashboard assets updates --- frontend/src/app/app.module.ts | 2 + frontend/src/app/bitcoin.utils.ts | 2 +- .../asset-circulation.component.html | 3 ++ .../asset-circulation.component.scss | 0 .../asset-circulation.component.ts | 53 +++++++++++++++++++ .../app/dashboard/dashboard.component.html | 13 +++-- .../app/dashboard/dashboard.component.scss | 13 +++-- .../src/app/dashboard/dashboard.component.ts | 4 +- 8 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 frontend/src/app/components/asset-circulation/asset-circulation.component.html create mode 100644 frontend/src/app/components/asset-circulation/asset-circulation.component.scss create mode 100644 frontend/src/app/components/asset-circulation/asset-circulation.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 97fc16204..28a3f60b7 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -67,6 +67,7 @@ import { PushTransactionComponent } from './components/push-transaction/push-tra import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component'; +import { AssetCirculationComponent } from './components/asset-circulation/asset-circulation.component'; @NgModule({ declarations: [ @@ -116,6 +117,7 @@ import { AssetGroupComponent } from './components/assets/asset-group/asset-group AssetsNavComponent, AssetsFeaturedComponent, AssetGroupComponent, + AssetCirculationComponent, ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index 577249940..ff2d7a885 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -69,7 +69,7 @@ export function calcSegwitFeeGains(tx: Transaction) { export function moveDec(num: number, n: number) { let frac, int, neg, ref; if (n === 0) { - return num; + return num.toString(); } ref = ('' + num).split('.'), int = ref[0], frac = ref[1]; int || (int = '0'); diff --git a/frontend/src/app/components/asset-circulation/asset-circulation.component.html b/frontend/src/app/components/asset-circulation/asset-circulation.component.html new file mode 100644 index 000000000..2f4dca8eb --- /dev/null +++ b/frontend/src/app/components/asset-circulation/asset-circulation.component.html @@ -0,0 +1,3 @@ + + {{ circulatingAmount }} + \ No newline at end of file diff --git a/frontend/src/app/components/asset-circulation/asset-circulation.component.scss b/frontend/src/app/components/asset-circulation/asset-circulation.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/asset-circulation/asset-circulation.component.ts b/frontend/src/app/components/asset-circulation/asset-circulation.component.ts new file mode 100644 index 000000000..6d1511c6e --- /dev/null +++ b/frontend/src/app/components/asset-circulation/asset-circulation.component.ts @@ -0,0 +1,53 @@ +import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; +import { combineLatest, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { moveDec } from 'src/app/bitcoin.utils'; +import { AssetsService } from 'src/app/services/assets.service'; +import { ElectrsApiService } from 'src/app/services/electrs-api.service'; +import { formatNumber } from '@angular/common'; +import { environment } from 'src/environments/environment'; + +@Component({ + selector: 'app-asset-circulation', + templateUrl: './asset-circulation.component.html', + styleUrls: ['./asset-circulation.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AssetCirculationComponent implements OnInit { + @Input() assetId: string; + + circulatingAmount$: Observable; + + constructor( + private electrsApiService: ElectrsApiService, + private assetsService: AssetsService, + @Inject(LOCALE_ID) private locale: string, + ) { } + + ngOnInit(): void { + this.circulatingAmount$ = combineLatest([ + this.electrsApiService.getAsset$(this.assetId), + this.assetsService.getAssetsMinimalJson$] + ) + .pipe( + map(([asset, assetsMinimal]) => { + const assetData = assetsMinimal[asset.asset_id]; + if (!asset.chain_stats.has_blinded_issuances) { + if (asset.asset_id === environment.nativeAssetId) { + return formatNumber(this.formatAmount(asset.chain_stats.peg_in_amount - asset.chain_stats.burned_amount + - asset.chain_stats.peg_out_amount, assetData[3]), this.locale, '1.2-2'); + } else { + return formatNumber(this.formatAmount(asset.chain_stats.issued_amount + - asset.chain_stats.burned_amount, assetData[3]), this.locale, '1.2-2'); + } + } else { + return $localize`:@@shared.confidential:Confidential`; + } + }), + ); + } + + formatAmount(value: number, precision = 0): number { + return parseFloat(moveDec(value, -precision)); + } +} diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 01a32ac7c..06f9cb0eb 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -47,7 +47,7 @@
- +
{{ group.name }} - {{ group.ticker }} + {{ group.ticker }} + @@ -180,13 +181,19 @@ - + + +
+
+
+
+
diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss index 23c9b226e..e518beaa7 100644 --- a/frontend/src/app/dashboard/dashboard.component.scss +++ b/frontend/src/app/dashboard/dashboard.component.scss @@ -285,8 +285,8 @@ } .assetIcon { - width: 28px; - height: 28px; + width: 40px; + height: 40px; } .asset-title { @@ -298,11 +298,16 @@ } .asset-icon { - width: 54px; - height: 52px; + width: 65px; + height: 65px; } .asset-table { width: calc(100% - 20px); margin-left: 1.25rem; } + +.circulating-amount { + text-align: right; + width: 100%; +} diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 36ad73003..b8a32210c 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -127,15 +127,15 @@ export class DashboardComponent implements OnInit { this.featuredAssets$ = this.apiService.listFeaturedAssets$() .pipe( - take(5), map((featured) => { + featured = featured.slice(0, 4); for (const feature of featured) { if (feature.assets) { feature.asset = feature.assets[0]; } } return featured; - }) + }), ); this.blocks$ = this.stateService.blocks$ From b6f89b1a3e480330bf1081e43c3302dd5b744a11 Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 16 Feb 2022 17:32:12 +0400 Subject: [PATCH 08/65] Moving ticker to circulating amount --- .../asset-circulation.component.html | 4 ++-- .../asset-circulation.component.scss | 3 +++ .../asset-circulation.component.ts | 21 +++++++++++++------ .../app/dashboard/dashboard.component.html | 1 - .../app/dashboard/dashboard.component.scss | 4 ---- .../src/app/dashboard/dashboard.component.ts | 12 +++++------ 6 files changed, 26 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/components/asset-circulation/asset-circulation.component.html b/frontend/src/app/components/asset-circulation/asset-circulation.component.html index 2f4dca8eb..1c6337721 100644 --- a/frontend/src/app/components/asset-circulation/asset-circulation.component.html +++ b/frontend/src/app/components/asset-circulation/asset-circulation.component.html @@ -1,3 +1,3 @@ - - {{ circulatingAmount }} + + {{ circulating.amount }} {{ circulating.ticker }} \ No newline at end of file diff --git a/frontend/src/app/components/asset-circulation/asset-circulation.component.scss b/frontend/src/app/components/asset-circulation/asset-circulation.component.scss index e69de29bb..5e43c829c 100644 --- a/frontend/src/app/components/asset-circulation/asset-circulation.component.scss +++ b/frontend/src/app/components/asset-circulation/asset-circulation.component.scss @@ -0,0 +1,3 @@ +.ticker { + color: grey; +} diff --git a/frontend/src/app/components/asset-circulation/asset-circulation.component.ts b/frontend/src/app/components/asset-circulation/asset-circulation.component.ts index 6d1511c6e..d1a56abb0 100644 --- a/frontend/src/app/components/asset-circulation/asset-circulation.component.ts +++ b/frontend/src/app/components/asset-circulation/asset-circulation.component.ts @@ -16,7 +16,7 @@ import { environment } from 'src/environments/environment'; export class AssetCirculationComponent implements OnInit { @Input() assetId: string; - circulatingAmount$: Observable; + circulatingAmount$: Observable<{ amount: string, ticker: string}>; constructor( private electrsApiService: ElectrsApiService, @@ -34,14 +34,23 @@ export class AssetCirculationComponent implements OnInit { const assetData = assetsMinimal[asset.asset_id]; if (!asset.chain_stats.has_blinded_issuances) { if (asset.asset_id === environment.nativeAssetId) { - return formatNumber(this.formatAmount(asset.chain_stats.peg_in_amount - asset.chain_stats.burned_amount - - asset.chain_stats.peg_out_amount, assetData[3]), this.locale, '1.2-2'); + return { + amount: formatNumber(this.formatAmount(asset.chain_stats.peg_in_amount - asset.chain_stats.burned_amount + - asset.chain_stats.peg_out_amount, assetData[3]), this.locale, '1.2-2'), + ticker: assetData[1] + }; } else { - return formatNumber(this.formatAmount(asset.chain_stats.issued_amount - - asset.chain_stats.burned_amount, assetData[3]), this.locale, '1.2-2'); + return { + amount: formatNumber(this.formatAmount(asset.chain_stats.issued_amount + - asset.chain_stats.burned_amount, assetData[3]), this.locale, '1.2-2'), + ticker: assetData[1] + }; } } else { - return $localize`:@@shared.confidential:Confidential`; + return { + amount: $localize`:@@shared.confidential:Confidential`, + ticker: '', + }; } }), ); diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 06f9cb0eb..7fb17ef83 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -72,7 +72,6 @@ {{ group.name }} - {{ group.ticker }} diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss index e518beaa7..4ceac7a6b 100644 --- a/frontend/src/app/dashboard/dashboard.component.scss +++ b/frontend/src/app/dashboard/dashboard.component.scss @@ -293,10 +293,6 @@ text-align: left; } -.asset-ticker { - color: grey; -} - .asset-icon { width: 65px; height: 65px; diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index b8a32210c..33fb5ea91 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; -import { combineLatest, merge, Observable, of, timer } from 'rxjs'; -import { filter, map, scan, share, switchMap, take, tap } from 'rxjs/operators'; +import { combineLatest, merge, Observable, of } from 'rxjs'; +import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface'; import { ApiService } from '../services/api.service'; @@ -128,13 +128,13 @@ export class DashboardComponent implements OnInit { this.featuredAssets$ = this.apiService.listFeaturedAssets$() .pipe( map((featured) => { - featured = featured.slice(0, 4); + const newArray = []; for (const feature of featured) { - if (feature.assets) { - feature.asset = feature.assets[0]; + if (feature.ticker !== 'L-BTC' && feature.asset) { + newArray.push(feature); } } - return featured; + return newArray.slice(0, 4); }), ); From fb2c0345a7c3ad0339e9c175b11334de718cdf06 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 17 Feb 2022 22:57:10 +0900 Subject: [PATCH 09/65] Show miner tag under blocks in the mining dashboard --- .../blockchain-blocks.component.html | 2 +- .../blockchain-blocks.component.scss | 13 ++++++++++- .../blockchain-blocks.component.ts | 23 +++++++++++++++---- .../blockchain/blockchain.component.html | 4 ++-- .../blockchain/blockchain.component.scss | 12 +--------- .../blockchain/blockchain.component.ts | 1 - 6 files changed, 34 insertions(+), 21 deletions(-) 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 1914b5d08..3c85787e7 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -21,7 +21,7 @@
-
+ diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss index a20b1cb35..5a9a7ea18 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss @@ -129,4 +129,15 @@ position: relative; top: 15px; z-index: 101; -} \ No newline at end of file +} + +.animated { + transition: all 0.15s ease-in-out; +} +.show { + opacity: 1; +} +.hide { + opacity: 0; + pointer-events : none; +} 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 a8d055602..42d045201 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -1,9 +1,10 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; import { StateService } from 'src/app/services/state.service'; -import { Router } from '@angular/router'; import { specialBlocks } from 'src/app/app.constants'; import { BlockExtended } from 'src/app/interfaces/node-api.interface'; +import { Location } from '@angular/common'; +import { config } from 'process'; @Component({ selector: 'app-blockchain-blocks', @@ -12,7 +13,6 @@ import { BlockExtended } from 'src/app/interfaces/node-api.interface'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class BlockchainBlocksComponent implements OnInit, OnDestroy { - @Input() showMiningInfo: boolean = false; specialBlocks = specialBlocks; network = ''; blocks: BlockExtended[] = []; @@ -32,6 +32,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { arrowLeftPx = 30; blocksFilled = false; transition = '1s'; + showMiningInfo = false; gradientColors = { '': ['#9339f4', '#105fb0'], @@ -44,11 +45,23 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { constructor( public stateService: StateService, - private router: Router, private cd: ChangeDetectorRef, - ) { } + private location: Location, + private cdr: ChangeDetectorRef + ) { + } + + enabledMiningInfoIfNeeded(url) { + this.showMiningInfo = url === '/mining'; + this.cdr.detectChanges(); // Need to update the view asap + } ngOnInit() { + if (['', 'testnet', 'signet'].includes(this.stateService.network)) { + this.enabledMiningInfoIfNeeded(this.location.path()); + this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url)); + } + if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') { this.feeRounding = '1.0-1'; } diff --git a/frontend/src/app/components/blockchain/blockchain.component.html b/frontend/src/app/components/blockchain/blockchain.component.html index 19ee5676d..c49d08c5a 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.html +++ b/frontend/src/app/components/blockchain/blockchain.component.html @@ -1,8 +1,8 @@ -
+
- +
diff --git a/frontend/src/app/components/blockchain/blockchain.component.scss b/frontend/src/app/components/blockchain/blockchain.component.scss index a33fc58d2..6d415cd2a 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.scss +++ b/frontend/src/app/components/blockchain/blockchain.component.scss @@ -59,14 +59,4 @@ width: 300px; left: -150px; top: 0px; -} - -.animate { - transition: all 1s ease-in-out; -} -.move-left { - transform: translate(-40%, 0); - @media (max-width: 767.98px) { - transform: translate(-85%, 0); - } -} +} \ No newline at end of file diff --git a/frontend/src/app/components/blockchain/blockchain.component.ts b/frontend/src/app/components/blockchain/blockchain.component.ts index b47eee833..5c00c5ef7 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.ts +++ b/frontend/src/app/components/blockchain/blockchain.component.ts @@ -8,7 +8,6 @@ import { StateService } from 'src/app/services/state.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class BlockchainComponent implements OnInit { - showMiningInfo: boolean = false; network: string; constructor( From e816f536372587f0842b50f71d6fe66e97195caa Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 18 Feb 2022 00:37:37 +0400 Subject: [PATCH 10/65] Flip Liquid dashboard locations --- .../app/dashboard/dashboard.component.html | 54 ++++++++++--------- .../app/dashboard/dashboard.component.scss | 8 ++- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 7fb17ef83..a871f6141 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -16,38 +16,40 @@
- +
- +
-
-
Transaction Fees
-
-
- + +
+
Transaction Fees
+
+
+ +
-
-
- -
+
+ +
+
- +
- +
- - + + + + + + +
+
+
+ +
+
+
@@ -76,17 +89,6 @@
-
-
-
-
-
-
-
- -
-
-
diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss index 4ceac7a6b..6fb2ee125 100644 --- a/frontend/src/app/dashboard/dashboard.component.scss +++ b/frontend/src/app/dashboard/dashboard.component.scss @@ -291,19 +291,17 @@ .asset-title { text-align: left; + vertical-align: middle; } .asset-icon { width: 65px; height: 65px; -} - -.asset-table { - width: calc(100% - 20px); - margin-left: 1.25rem; + vertical-align: middle; } .circulating-amount { text-align: right; width: 100%; + vertical-align: middle; } From 0740049cbcee9d0bba9c3f27b6663017f1234a9a Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn <100320+knorrium@users.noreply.github.com> Date: Thu, 17 Feb 2022 20:51:04 -0800 Subject: [PATCH 11/65] Update docker default min loglevel to info --- README.md | 4 ++-- docker/backend/start.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index af4ae224f..5b994955c 100644 --- a/README.md +++ b/README.md @@ -91,11 +91,11 @@ JSON: "PRICE_FEED_UPDATE_INTERVAL": 600, "USE_SECOND_NODE_FOR_MINFEE": false, "EXTERNAL_ASSETS": ["https://mempool.space/resources/pools.json"], - "STDOUT_LOG_MIN_PRIORITY": "debug" + "STDOUT_LOG_MIN_PRIORITY": "info" }, ``` -docker-compose overrides:: +docker-compose overrides: ``` MEMPOOL_NETWORK: "" MEMPOOL_BACKEND: "" diff --git a/docker/backend/start.sh b/docker/backend/start.sh index 019f16217..d26a93b08 100644 --- a/docker/backend/start.sh +++ b/docker/backend/start.sh @@ -17,7 +17,7 @@ __MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=1100} __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600} __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false} __MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[\"https://mempool.space/resources/pools.json\"]} -__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=debug} +__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} # CORE_RPC __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} From 008a4b51cc137c15322a66cc92756bcd63cc0339 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 18 Feb 2022 22:25:31 +0900 Subject: [PATCH 12/65] Remove duplicated ChangeDetectorRef in blockchains blocks component --- .../blockchain-blocks/blockchain-blocks.component.html | 8 ++++---- .../blockchain-blocks/blockchain-blocks.component.ts | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) 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 3c85787e7..516ad5ba6 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -21,10 +21,10 @@
- +
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 42d045201..7a0123a78 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -47,13 +47,12 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { public stateService: StateService, private cd: ChangeDetectorRef, private location: Location, - private cdr: ChangeDetectorRef ) { } enabledMiningInfoIfNeeded(url) { this.showMiningInfo = url === '/mining'; - this.cdr.detectChanges(); // Need to update the view asap + this.cd.markForCheck(); // Need to update the view asap } ngOnInit() { From af8d4a8514bad17ae7af1a5a42ed35ae52ca5853 Mon Sep 17 00:00:00 2001 From: wiz Date: Sun, 20 Feb 2022 08:43:59 +0900 Subject: [PATCH 13/65] Add nix-bitcoin to Community Integrations on About page --- .../app/components/about/about.component.html | 8 ++++---- frontend/src/resources/profile/nix-bitcoin.png | Bin 0 -> 18094 bytes 2 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 frontend/src/resources/profile/nix-bitcoin.png diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index b9050d41e..0b68183d2 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -106,6 +106,10 @@ Citadel + + + NixOS + Electrum @@ -142,10 +146,6 @@ Marina - - - Satpile -
diff --git a/frontend/src/resources/profile/nix-bitcoin.png b/frontend/src/resources/profile/nix-bitcoin.png new file mode 100644 index 0000000000000000000000000000000000000000..cd8d3e9d72628ec0697f768d563eb6cd77f3462c GIT binary patch literal 18094 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 BTlW9} literal 0 HcmV?d00001 From 6f939e1bad60c6c419538664682e6620c5e04782 Mon Sep 17 00:00:00 2001 From: wiz Date: Sun, 20 Feb 2022 08:30:35 +0900 Subject: [PATCH 14/65] Tweak text on About page so it can be displayed on all networks --- frontend/src/app/components/about/about.component.html | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index b9050d41e..7a39a40dd 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -8,13 +8,10 @@
-
+
The Mempool Open Source Project
-

Building a mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, without any advertising, altcoins, or third-party trackers.

+

Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.

- -
-
+

+ +

Enterprise Sponsors 🚀

@@ -73,9 +79,6 @@
- - - Navigate to https://mempool.space/sponsor to sponsor
@@ -177,7 +180,7 @@

- +

Project Contributors

@@ -252,7 +255,7 @@ Third-party Licenses Terms of Service
- + From edb8f5ecd17037f1b8936129e1a4fe4edd54cbe5 Mon Sep 17 00:00:00 2001 From: wiz Date: Sun, 20 Feb 2022 12:02:35 +0900 Subject: [PATCH 16/65] Inline all Enterprise Sponsor logos on About page --- .../app/components/about/about.component.html | 103 +++++++++++++++++- .../src/resources/profile/blockstream.svg | 43 -------- frontend/src/resources/profile/exodus.svg | 16 --- frontend/src/resources/profile/foundry.svg | 9 -- frontend/src/resources/profile/gemini.svg | 6 - frontend/src/resources/profile/spiral.svg | 10 -- frontend/src/resources/profile/unchained.svg | 24 ---- 7 files changed, 97 insertions(+), 114 deletions(-) delete mode 100644 frontend/src/resources/profile/blockstream.svg delete mode 100644 frontend/src/resources/profile/exodus.svg delete mode 100644 frontend/src/resources/profile/foundry.svg delete mode 100644 frontend/src/resources/profile/gemini.svg delete mode 100644 frontend/src/resources/profile/spiral.svg delete mode 100644 frontend/src/resources/profile/unchained.svg diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index b9050d41e..b6546f240 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -35,27 +35,118 @@

Enterprise Sponsors 🚀

diff --git a/frontend/src/resources/profile/blockstream.svg b/frontend/src/resources/profile/blockstream.svg deleted file mode 100644 index 6dc32eab9..000000000 --- a/frontend/src/resources/profile/blockstream.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/resources/profile/exodus.svg b/frontend/src/resources/profile/exodus.svg deleted file mode 100644 index 082cba7a5..000000000 --- a/frontend/src/resources/profile/exodus.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - Exodus_logo - - - - - - - - diff --git a/frontend/src/resources/profile/foundry.svg b/frontend/src/resources/profile/foundry.svg deleted file mode 100644 index 3647e994a..000000000 --- a/frontend/src/resources/profile/foundry.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/src/resources/profile/gemini.svg b/frontend/src/resources/profile/gemini.svg deleted file mode 100644 index 6aeecf1b9..000000000 --- a/frontend/src/resources/profile/gemini.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/src/resources/profile/spiral.svg b/frontend/src/resources/profile/spiral.svg deleted file mode 100644 index c21f5d70c..000000000 --- a/frontend/src/resources/profile/spiral.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/frontend/src/resources/profile/unchained.svg b/frontend/src/resources/profile/unchained.svg deleted file mode 100644 index 2ab4bc9da..000000000 --- a/frontend/src/resources/profile/unchained.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - From b70df576d407eb00e10c2f76adbaeee51dbcb9d5 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 20 Feb 2022 13:33:46 +0400 Subject: [PATCH 17/65] Fixing enterprise sponsors margin --- frontend/src/app/components/about/about.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index 4a14307cb..5362484bc 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -123,7 +123,7 @@ display: block; transition: 150ms all; } - img { + img, svg { margin: 40px 29px 10px; } } From 3b7d36e9e96fc2800ce477defe3634ac062411f2 Mon Sep 17 00:00:00 2001 From: wiz Date: Sun, 20 Feb 2022 20:11:31 +0900 Subject: [PATCH 18/65] Fix mouseover CSS for inline SVG on About page --- .../app/components/about/about.component.scss | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index 5362484bc..8b9466732 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -9,7 +9,7 @@ border-radius: 50%; margin: 25px; line-height: 32px; - } + } .intro { margin: 25px auto 30px; @@ -41,7 +41,7 @@ } .alliances, - .enterprise-sponsor, + .enterprise-sponsor, .community-integrations-sponsor, .maintainers { margin-top: 68px; @@ -58,7 +58,7 @@ .wrapper { margin: 20px auto; } - .btn-primary { + .btn-primary { max-width: 250px; margin: auto; height: 45px; @@ -68,7 +68,7 @@ .alliances { margin-bottom: 100px; - a { + a { &:nth-child(3) { position: relative; top: 10px; @@ -88,17 +88,17 @@ margin: 50px 30px 0px; } } - .liquid { + .liquid { top: 7px; position: relative; } - .copa { + .copa { height: auto; top: 23px; position: relative; width: 300px; } - .bisq { + .bisq { top: 3px; position: relative; } @@ -115,11 +115,11 @@ display: inline-block; &:hover { text-decoration: none; - img { + img, svg { transform: scale(1.1); } } - img, span{ + img, svg, span { display: block; transition: 150ms all; } From 38b37a3ee7d9bb4227bf70aca85cd8af62e23fd9 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 18 Feb 2022 15:26:15 +0900 Subject: [PATCH 19/65] Re-define sub mining routes properly and use router outlet --- frontend/src/app/app-routing.module.ts | 119 +++++++++++++----- frontend/src/app/app.module.ts | 4 + .../hashrate-chart.component.html | 1 + .../hashrate-chart.component.scss | 0 .../hashrate-chart.component.ts | 15 +++ .../mining-start/mining-start.component.html | 1 + .../mining-start/mining-start.component.ts | 14 +++ 7 files changed, 124 insertions(+), 30 deletions(-) create mode 100644 frontend/src/app/components/hashrate-chart/hashrate-chart.component.html create mode 100644 frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss create mode 100644 frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts create mode 100644 frontend/src/app/components/mining-start/mining-start.component.html create mode 100644 frontend/src/app/components/mining-start/mining-start.component.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index f0aa73e3d..c8a1d98e6 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -29,6 +29,8 @@ import { AssetsComponent } from './components/assets/assets.component'; import { PoolComponent } from './components/pool/pool.component'; import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component'; import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component'; +import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component'; +import { MiningStartComponent } from './components/mining-start/mining-start.component'; let routes: Routes = [ { @@ -70,16 +72,35 @@ let routes: Routes = [ component: LatestBlocksComponent, }, { - path: 'mining/difficulty', - component: DifficultyChartComponent, - }, - { - path: 'mining/pools', - component: PoolRankingComponent, - }, - { - path: 'mining/pool/:poolId', - component: PoolComponent, + path: 'mining', + component: MiningStartComponent, + children: [ + { + path: 'difficulty', + component: DifficultyChartComponent, + }, + { + path: 'hashrate', + component: HashrateChartComponent, + }, + { + path: 'pools', + component: PoolRankingComponent, + }, + { + path: 'pool', + children: [ + { + path: ':poolId', + component: PoolComponent, + }, + { + path: ':poolId/hashrate', + component: HashrateChartComponent, + }, + ] + }, + ] }, { path: 'graphs', @@ -170,16 +191,35 @@ let routes: Routes = [ component: LatestBlocksComponent, }, { - path: 'mining/difficulty', - component: DifficultyChartComponent, - }, - { - path: 'mining/pools', - component: PoolRankingComponent, - }, - { - path: 'mining/pool/:poolId', - component: PoolComponent, + path: 'mining', + component: MiningStartComponent, + children: [ + { + path: 'difficulty', + component: DifficultyChartComponent, + }, + { + path: 'hashrate', + component: HashrateChartComponent, + }, + { + path: 'pools', + component: PoolRankingComponent, + }, + { + path: 'pool', + children: [ + { + path: ':poolId', + component: PoolComponent, + }, + { + path: ':poolId/hashrate', + component: HashrateChartComponent, + }, + ] + }, + ] }, { path: 'graphs', @@ -264,16 +304,35 @@ let routes: Routes = [ component: LatestBlocksComponent, }, { - path: 'mining/difficulty', - component: DifficultyChartComponent, - }, - { - path: 'mining/pools', - component: PoolRankingComponent, - }, - { - path: 'mining/pool/:poolId', - component: PoolComponent, + path: 'mining', + component: MiningStartComponent, + children: [ + { + path: 'difficulty', + component: DifficultyChartComponent, + }, + { + path: 'hashrate', + component: HashrateChartComponent, + }, + { + path: 'pools', + component: PoolRankingComponent, + }, + { + path: 'pool', + children: [ + { + path: ':poolId', + component: PoolComponent, + }, + { + path: ':poolId/hashrate', + component: HashrateChartComponent, + }, + ] + }, + ] }, { path: 'graphs', diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 4fe24ceb8..9e8fea464 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -71,6 +71,8 @@ import { AssetGroupComponent } from './components/assets/asset-group/asset-group import { AssetCirculationComponent } from './components/asset-circulation/asset-circulation.component'; import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component'; import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component'; +import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component'; +import { MiningStartComponent } from './components/mining-start/mining-start.component'; @NgModule({ declarations: [ @@ -124,6 +126,8 @@ import { DifficultyChartComponent } from './components/difficulty-chart/difficul AssetCirculationComponent, MiningDashboardComponent, DifficultyChartComponent, + HashrateChartComponent, + MiningStartComponent, ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html new file mode 100644 index 000000000..e55205844 --- /dev/null +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -0,0 +1 @@ +

hashrate-chart works!

diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts new file mode 100644 index 000000000..cfbb6ba31 --- /dev/null +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-hashrate-chart', + templateUrl: './hashrate-chart.component.html', + styleUrls: ['./hashrate-chart.component.scss'] +}) +export class HashrateChartComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/frontend/src/app/components/mining-start/mining-start.component.html b/frontend/src/app/components/mining-start/mining-start.component.html new file mode 100644 index 000000000..0680b43f9 --- /dev/null +++ b/frontend/src/app/components/mining-start/mining-start.component.html @@ -0,0 +1 @@ + diff --git a/frontend/src/app/components/mining-start/mining-start.component.ts b/frontend/src/app/components/mining-start/mining-start.component.ts new file mode 100644 index 000000000..6850cfa54 --- /dev/null +++ b/frontend/src/app/components/mining-start/mining-start.component.ts @@ -0,0 +1,14 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-mining-start', + templateUrl: './mining-start.component.html', +}) +export class MiningStartComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} From 6fe8f6fa1eb95fffd616e87c1dd3333bd30a76b0 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Sat, 19 Feb 2022 20:45:02 +0900 Subject: [PATCH 20/65] Generate daily average hashrate data --- backend/src/api/blocks.ts | 5 +- backend/src/api/database-migration.ts | 19 ++++++- backend/src/api/mining.ts | 52 ++++++++++++++++++- backend/src/index.ts | 11 +++- backend/src/repositories/BlocksRepository.ts | 34 ++++++++++++ .../src/repositories/HashratesRepository.ts | 42 +++++++++++++++ backend/src/routes.ts | 1 - 7 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 backend/src/repositories/HashratesRepository.ts diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 1452b6fc8..483fd3f3e 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -170,10 +170,7 @@ class Blocks { * Index all blocks metadata for the mining dashboard */ public async $generateBlockDatabase() { - if (this.blockIndexingStarted === true || - !Common.indexingEnabled() || - memPool.hasPriority() - ) { + if (this.blockIndexingStarted) { return; } diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index b44585580..154068164 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -6,7 +6,7 @@ import logger from '../logger'; const sleep = (ms: number) => new Promise(res => setTimeout(res, ms)); class DatabaseMigration { - private static currentVersion = 6; + private static currentVersion = 7; private queryTimeout = 120000; private statisticsAddedIndexed = false; @@ -116,6 +116,12 @@ class DatabaseMigration { 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'); } + + if (databaseSchemaVersion < 7 && isBitcoin === true) { + await this.$executeQuery(connection, 'DROP table IF EXISTS hashrates;'); + await this.$executeQuery(connection, this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates')); + } + connection.release(); } catch (e) { connection.release(); @@ -398,6 +404,17 @@ class DatabaseMigration { FOREIGN KEY (pool_id) REFERENCES pools (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } + + private getCreateDailyStatsTableQuery(): string { + return `CREATE TABLE IF NOT EXISTS hashrates ( + hashrate_timestamp timestamp NOT NULL, + avg_hashrate double unsigned DEFAULT '0', + pool_id smallint unsigned NULL, + PRIMARY KEY (hashrate_timestamp), + INDEX (pool_id), + FOREIGN KEY (pool_id) REFERENCES pools (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } } export default new DatabaseMigration(); diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index beca52893..fc13c2f5e 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -1,9 +1,13 @@ import { PoolInfo, PoolStats } from '../mempool.interfaces'; import BlocksRepository, { EmptyBlocks } from '../repositories/BlocksRepository'; import PoolsRepository from '../repositories/PoolsRepository'; +import HashratesRepository from '../repositories/HashratesRepository'; import bitcoinClient from './bitcoin/bitcoin-client'; +import logger from '../logger'; class Mining { + hashrateIndexingStarted = false; + constructor() { } @@ -45,7 +49,7 @@ class Mining { poolsStatistics['blockCount'] = blockCount; const blockHeightTip = await bitcoinClient.getBlockCount(); - const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(120, blockHeightTip); + const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(144, blockHeightTip); poolsStatistics['lastEstimatedHashrate'] = lastBlockHashrate; return poolsStatistics; @@ -82,6 +86,52 @@ class Mining { oldestIndexedBlockTimestamp: oldestBlock.getTime(), } } + + /** + * + */ + public async $generateNetworkHashrateHistory() : Promise { + if (this.hashrateIndexingStarted) { + return; + } + this.hashrateIndexingStarted = true; + + const totalIndexed = await BlocksRepository.$blockCount(null, null); + const indexedTimestamp = await HashratesRepository.$getAllTimestamp(); + + const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f + const lastMidnight = new Date(); + lastMidnight.setUTCHours(0); lastMidnight.setUTCMinutes(0); lastMidnight.setUTCSeconds(0); lastMidnight.setUTCMilliseconds(0); + let toTimestamp = Math.round(lastMidnight.getTime() / 1000); + + while (toTimestamp > genesisTimestamp) { + const fromTimestamp = toTimestamp - 86400; + if (indexedTimestamp.includes(fromTimestamp)) { + toTimestamp -= 86400; + continue; + } + + const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp( + null, fromTimestamp, toTimestamp + ); + let lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, blockStats.lastBlockHeight); + + if (toTimestamp % 864000 === 0) { + const progress = Math.round((totalIndexed - blockStats.lastBlockHeight) / totalIndexed * 100); + const formattedDate = new Date(fromTimestamp * 1000).toUTCString(); + logger.debug(`Counting blocks and hashrate for ${formattedDate}. Progress: ${progress}%`); + } + + await HashratesRepository.$saveDailyStat({ + hashrateTimestamp: fromTimestamp, + avgHashrate: lastBlockHashrate, + poolId: null, + }); + + toTimestamp -= 86400; + } + } + } export default new Mining(); diff --git a/backend/src/index.ts b/backend/src/index.ts index 23c70f59d..d09196a45 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -26,6 +26,7 @@ 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'; class Server { private wss: WebSocket.Server | undefined; @@ -138,7 +139,7 @@ class Server { } await blocks.$updateBlocks(); await memPool.$updateMempool(); - blocks.$generateBlockDatabase(); + this.runIndexingWhenReady(); setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS); this.currentBackendRetryInterval = 5; @@ -157,6 +158,14 @@ class Server { } } + async runIndexingWhenReady() { + if (!Common.indexingEnabled() || mempool.hasPriority()) { + return; + } + await blocks.$generateBlockDatabase(); + await mining.$generateNetworkHashrateHistory(); + } + setUpWebsocketHandling() { if (this.wss) { websocketHandler.setWebsocketServer(this.wss); diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index ac0ea25bc..937320a3a 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -149,6 +149,40 @@ class BlocksRepository { return rows[0].blockCount; } + /** + * Get blocks count between two dates + * @param poolId + * @param from - The oldest timestamp + * @param to - The newest timestamp + * @returns + */ + public async $blockCountBetweenTimestamp(poolId: number | null, from: number, to: number): Promise { + const params: any[] = []; + let query = `SELECT + count(height) as blockCount, + max(height) as lastBlockHeight + FROM blocks`; + + if (poolId) { + query += ` WHERE pool_id = ?`; + params.push(poolId); + } + + if (poolId) { + query += ` AND`; + } else { + query += ` WHERE`; + } + query += ` UNIX_TIMESTAMP(blockTimestamp) BETWEEN '${from}' AND '${to}'`; + + // logger.debug(query); + const connection = await DB.pool.getConnection(); + const [rows] = await connection.query(query, params); + connection.release(); + + return rows[0]; + } + /** * Get the oldest indexed block */ diff --git a/backend/src/repositories/HashratesRepository.ts b/backend/src/repositories/HashratesRepository.ts new file mode 100644 index 000000000..f55700812 --- /dev/null +++ b/backend/src/repositories/HashratesRepository.ts @@ -0,0 +1,42 @@ +import { DB } from '../database'; +import logger from '../logger'; + +class HashratesRepository { + /** + * Save indexed block data in the database + */ + public async $saveDailyStat(dailyStat: any) { + const connection = await DB.pool.getConnection(); + + try { + const query = `INSERT INTO + hashrates(hashrate_timestamp, avg_hashrate, pool_id) + VALUE (FROM_UNIXTIME(?), ?, ?)`; + + const params: any[] = [ + dailyStat.hashrateTimestamp, dailyStat.avgHashrate, + dailyStat.poolId + ]; + + // logger.debug(query); + await connection.query(query, params); + } catch (e: any) { + logger.err('$saveHashrateInDatabase() error' + (e instanceof Error ? e.message : e)); + } + + connection.release(); + } + + /** + * Returns an array of all timestamp we've already indexed + */ + public async $getAllTimestamp(): Promise { + const connection = await DB.pool.getConnection(); + const [rows]: any[] = await connection.query(`SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp from hashrates`); + connection.release(); + + return rows.map(val => val.timestamp); + } +} + +export default new HashratesRepository(); diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 4a9cb1f8f..df5e7cb62 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -22,7 +22,6 @@ import elementsParser from './api/liquid/elements-parser'; import icons from './api/liquid/icons'; import miningStats from './api/mining'; import axios from 'axios'; -import PoolsRepository from './repositories/PoolsRepository'; import mining from './api/mining'; import BlocksRepository from './repositories/BlocksRepository'; From 358604ad85c1a805c8790c8eb230e024308529bf Mon Sep 17 00:00:00 2001 From: nymkappa Date: Sat, 19 Feb 2022 22:09:35 +0900 Subject: [PATCH 21/65] Added hashrate chart --- backend/src/api/mining.ts | 24 ++- backend/src/index.ts | 4 +- .../src/repositories/HashratesRepository.ts | 19 +- backend/src/routes.ts | 12 ++ .../hashrate-chart.component.html | 54 +++++- .../hashrate-chart.component.scss | 10 ++ .../hashrate-chart.component.ts | 166 +++++++++++++++++- frontend/src/app/services/api.service.ts | 7 + 8 files changed, 284 insertions(+), 12 deletions(-) diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index fc13c2f5e..6f90ab357 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -87,6 +87,19 @@ class Mining { } } + /** + * Return the historical hashrates and oldest indexed block timestamp + */ + public async $getHistoricalHashrates(interval: string | null): Promise { + const hashrates = await HashratesRepository.$get(interval); + const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp()); + + return { + hashrates: hashrates, + oldestIndexedBlockTimestamp: oldestBlock.getTime(), + } + } + /** * */ @@ -97,7 +110,7 @@ class Mining { this.hashrateIndexingStarted = true; const totalIndexed = await BlocksRepository.$blockCount(null, null); - const indexedTimestamp = await HashratesRepository.$getAllTimestamp(); + const indexedTimestamp = (await HashratesRepository.$get(null)).map(hashrate => hashrate.timestamp); const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f const lastMidnight = new Date(); @@ -114,7 +127,12 @@ class Mining { const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp( null, fromTimestamp, toTimestamp ); - let lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, blockStats.lastBlockHeight); + + let lastBlockHashrate = 0; + if (blockStats.blockCount > 0) { + lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, + blockStats.lastBlockHeight); + } if (toTimestamp % 864000 === 0) { const progress = Math.round((totalIndexed - blockStats.lastBlockHeight) / totalIndexed * 100); @@ -130,6 +148,8 @@ class Mining { toTimestamp -= 86400; } + + logger.info(`Hashrates indexing completed`); } } diff --git a/backend/src/index.ts b/backend/src/index.ts index d09196a45..4fe66bc72 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -285,7 +285,9 @@ class Server { .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/difficulty/:interval', routes.$getHistoricalDifficulty) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate', routes.$getHistoricalHashrate) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate); } if (config.BISQ.ENABLED) { diff --git a/backend/src/repositories/HashratesRepository.ts b/backend/src/repositories/HashratesRepository.ts index f55700812..837569cd8 100644 --- a/backend/src/repositories/HashratesRepository.ts +++ b/backend/src/repositories/HashratesRepository.ts @@ -1,3 +1,4 @@ +import { Common } from '../api/common'; import { DB } from '../database'; import logger from '../logger'; @@ -30,12 +31,22 @@ class HashratesRepository { /** * Returns an array of all timestamp we've already indexed */ - public async $getAllTimestamp(): Promise { + public async $get(interval: string | null): Promise { + interval = Common.getSqlInterval(interval); + const connection = await DB.pool.getConnection(); - const [rows]: any[] = await connection.query(`SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp from hashrates`); + + let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate + FROM hashrates`; + + if (interval) { + query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; + } + + const [rows]: any[] = await connection.query(query); connection.release(); - - return rows.map(val => val.timestamp); + + return rows; } } diff --git a/backend/src/routes.ts b/backend/src/routes.ts index df5e7cb62..1bf1c3434 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -586,6 +586,18 @@ class Routes { } } + public async $getHistoricalHashrate(req: Request, res: Response) { + try { + const stats = await mining.$getHistoricalHashrates(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 getBlock(req: Request, res: Response) { try { const result = await bitcoinApi.$getBlock(req.params.hash); 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 e55205844..04534f176 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -1 +1,53 @@ -

hashrate-chart works!

+
+ +
+
+
+
+ +
+
+
+ + + + + + +
+
+
+ + + +
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 e69de29bb..c3a63e9fa 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss @@ -0,0 +1,10 @@ +.main-title { + position: relative; + color: #ffffff91; + margin-top: -13px; + font-size: 10px; + text-transform: uppercase; + font-weight: 500; + text-align: center; + padding-bottom: 3px; +} 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 cfbb6ba31..4739c2c30 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -1,15 +1,173 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; +import { EChartsOption } 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'; @Component({ selector: 'app-hashrate-chart', templateUrl: './hashrate-chart.component.html', - styleUrls: ['./hashrate-chart.component.scss'] + styleUrls: ['./hashrate-chart.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 38%; + left: calc(50% - 15px); + z-index: 100; + } + `], }) export class HashrateChartComponent implements OnInit { + @Input() widget: boolean = false; - constructor() { } + radioGroupForm: FormGroup; - ngOnInit(): void { + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg' + }; + + hashrateObservable$: Observable; + isLoading = true; + formatNumber = formatNumber; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private seoService: SeoService, + private apiService: ApiService, + private formBuilder: FormBuilder, + ) { + this.seoService.setTitle($localize`:@@mining.hashrate:hashrate`); + this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); + this.radioGroupForm.controls.dateSpan.setValue('1y'); } + ngOnInit(): void { + this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges + .pipe( + startWith('1y'), + switchMap((timespan) => { + return this.apiService.getHistoricalHashrate$(timespan) + .pipe( + tap(data => { + this.prepareChartOptions(data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate])); + this.isLoading = false; + }), + map(data => { + const availableTimespanDay = ( + (new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp / 1000) + ) / 3600 / 24; + return { + availableTimespanDay: availableTimespanDay, + data: data.hashrates + }; + }), + ); + }), + share() + ); + } + + prepareChartOptions(data) { + this.chartOptions = { + title: { + text: this.widget? '' : $localize`:@@mining.hashrate:Hashrate`, + left: 'center', + textStyle: { + color: '#FFF', + }, + }, + tooltip: { + show: true, + trigger: 'axis', + }, + axisPointer: { + type: 'line', + }, + xAxis: { + type: 'time', + splitNumber: this.isMobile() ? 5 : 10, + }, + yAxis: { + type: 'value', + axisLabel: { + formatter: (val) => { + const powerOfTen = { + exa: Math.pow(10, 18), + peta: Math.pow(10, 15), + terra: Math.pow(10, 12), + giga: Math.pow(10, 9), + mega: Math.pow(10, 6), + kilo: Math.pow(10, 3), + } + + let selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' }; + if (val < powerOfTen.mega) { + selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling + } else if (val < powerOfTen.giga) { + selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' }; + } else if (val < powerOfTen.terra) { + selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' }; + } else if (val < powerOfTen.peta) { + selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' }; + } else if (val < powerOfTen.exa) { + selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' }; + } + + const newVal = val / selectedPowerOfTen.divider; + return `${newVal} ${selectedPowerOfTen.unit}` + } + }, + splitLine: { + lineStyle: { + type: 'dotted', + color: '#ffffff66', + opacity: 0.25, + } + }, + }, + series: { + showSymbol: false, + data: data, + type: 'line', + smooth: false, + lineStyle: { + width: 3, + }, + areaStyle: {}, + }, + dataZoom: this.widget ? null : [{ + type: 'inside', + realtime: true, + zoomLock: true, + zoomOnMouseWheel: true, + moveOnMouseMove: true, + maxSpan: 100, + minSpan: 10, + }, { + showDetail: false, + show: true, + type: 'slider', + brushSelect: false, + realtime: true, + bottom: 0, + selectedDataBackground: { + lineStyle: { + color: '#fff', + opacity: 0.45, + }, + areaStyle: { + opacity: 0, + } + }, + }], + }; + } + + isMobile() { + return (window.innerWidth <= 767.98); + } } diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 9a6bbc0b8..cf0ebd414 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -156,4 +156,11 @@ export class ApiService { (interval !== undefined ? `/${interval}` : '') ); } + + getHistoricalHashrate$(interval: string | undefined): Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` + + (interval !== undefined ? `/${interval}` : '') + ); + } } From e61df324ea55c81b9ca89cf0d6f13d6c6a878fd7 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 21 Feb 2022 12:22:20 +0900 Subject: [PATCH 22/65] Index new hashrates once every 24 hours --- backend/src/api/database-migration.ts | 4 ++ backend/src/api/mining.ts | 46 +++++++++++------ backend/src/repositories/BlocksRepository.ts | 7 +++ .../src/repositories/HashratesRepository.ts | 17 +++++++ .../difficulty-chart.component.ts | 21 ++++---- .../hashrate-chart.component.html | 49 ++++++------------- .../hashrate-chart.component.ts | 16 +++--- 7 files changed, 92 insertions(+), 68 deletions(-) diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 154068164..098394664 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -261,6 +261,10 @@ class DatabaseMigration { } } + if (version < 7) { + queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`); + } + return queries; } diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index 6f90ab357..792e93f45 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -14,7 +14,7 @@ class Mining { /** * Generate high level overview of the pool ranks and general stats */ - public async $getPoolsStats(interval: string | null) : Promise { + public async $getPoolsStats(interval: string | null): Promise { const poolsStatistics = {}; const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval); @@ -30,8 +30,8 @@ class Mining { link: poolInfo.link, blockCount: poolInfo.blockCount, rank: rank++, - emptyBlocks: 0, - } + emptyBlocks: 0 + }; for (let i = 0; i < emptyBlocks.length; ++i) { if (emptyBlocks[i].poolId === poolInfo.poolId) { poolStat.emptyBlocks++; @@ -84,32 +84,41 @@ class Mining { return { adjustments: difficultyAdjustments, oldestIndexedBlockTimestamp: oldestBlock.getTime(), - } + }; } /** * Return the historical hashrates and oldest indexed block timestamp */ - public async $getHistoricalHashrates(interval: string | null): Promise { + public async $getHistoricalHashrates(interval: string | null): Promise { const hashrates = await HashratesRepository.$get(interval); const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp()); return { hashrates: hashrates, oldestIndexedBlockTimestamp: oldestBlock.getTime(), - } + }; } /** - * + * Generate daily hashrate data */ - public async $generateNetworkHashrateHistory() : Promise { + public async $generateNetworkHashrateHistory(): Promise { + // We only run this once a day + const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp(); + const now = new Date().getTime() / 1000; + if (now - latestTimestamp < 86400) { + return; + } + + logger.info(`Indexing hashrates`); + if (this.hashrateIndexingStarted) { return; } this.hashrateIndexingStarted = true; - const totalIndexed = await BlocksRepository.$blockCount(null, null); + const oldestIndexedBlockHeight = await BlocksRepository.$getOldestIndexedBlockHeight(); const indexedTimestamp = (await HashratesRepository.$get(null)).map(hashrate => hashrate.timestamp); const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f @@ -128,16 +137,18 @@ class Mining { null, fromTimestamp, toTimestamp ); - let lastBlockHashrate = 0; - if (blockStats.blockCount > 0) { - lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, - blockStats.lastBlockHeight); + if (blockStats.blockCount === 0) { // We are done indexing, no blocks left + break; } - if (toTimestamp % 864000 === 0) { - const progress = Math.round((totalIndexed - blockStats.lastBlockHeight) / totalIndexed * 100); + let lastBlockHashrate = 0; + lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, + blockStats.lastBlockHeight); + + if (toTimestamp % 864000 === 0) { // Log every 10 days during initial indexing const formattedDate = new Date(fromTimestamp * 1000).toUTCString(); - logger.debug(`Counting blocks and hashrate for ${formattedDate}. Progress: ${progress}%`); + const blocksLeft = blockStats.lastBlockHeight - oldestIndexedBlockHeight; + logger.debug(`Counting blocks and hashrate for ${formattedDate}. ${blocksLeft} blocks left`); } await HashratesRepository.$saveDailyStat({ @@ -149,6 +160,9 @@ class Mining { toTimestamp -= 86400; } + await HashratesRepository.$setLatestRunTimestamp(); + this.hashrateIndexingStarted = false; + logger.info(`Hashrates indexing completed`); } diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 937320a3a..9c7e9b778 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -281,6 +281,13 @@ class BlocksRepository { return rows; } + + public async $getOldestIndexedBlockHeight(): Promise { + const connection = await DB.pool.getConnection(); + const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`); + connection.release(); + return rows[0].minHeight; + } } export default new BlocksRepository(); diff --git a/backend/src/repositories/HashratesRepository.ts b/backend/src/repositories/HashratesRepository.ts index 837569cd8..2a898e5bd 100644 --- a/backend/src/repositories/HashratesRepository.ts +++ b/backend/src/repositories/HashratesRepository.ts @@ -43,11 +43,28 @@ class HashratesRepository { query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; } + query += ` ORDER by hashrate_timestamp DESC`; + const [rows]: any[] = await connection.query(query); connection.release(); return rows; } + + public async $setLatestRunTimestamp() { + const connection = await DB.pool.getConnection(); + const query = `UPDATE state SET number = ? WHERE name = 'last_hashrates_indexing'`; + await connection.query(query, [Math.round(new Date().getTime() / 1000)]); + connection.release(); + } + + public async $getLatestRunTimestamp(): Promise { + const connection = await DB.pool.getConnection(); + const query = `SELECT number FROM state WHERE name = 'last_hashrates_indexing'`; + const [rows] = await connection.query(query); + connection.release(); + return rows[0]['number']; + } } export default new HashratesRepository(); diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts index 350e3c4be..a8865ec09 100644 --- a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts +++ b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts @@ -134,17 +134,18 @@ export class DifficultyChartComponent implements OnInit { } } }, - series: [ - { - data: data, - type: 'line', - smooth: false, - lineStyle: { - width: 3, - }, - areaStyle: {} + series: { + showSymbol: false, + data: data, + type: 'line', + smooth: false, + lineStyle: { + width: 2, }, - ], + areaStyle: { + opacity: 0.25 + }, + }, }; } 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 04534f176..cfcd15bfe 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -4,50 +4,29 @@
- -
-
+ +
+
-
- - -
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 4739c2c30..8a3413db5 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -66,15 +66,15 @@ export class HashrateChartComponent implements OnInit { }; }), ); - }), - share() - ); + }), + share() + ); } prepareChartOptions(data) { this.chartOptions = { title: { - text: this.widget? '' : $localize`:@@mining.hashrate:Hashrate`, + text: this.widget ? '' : $localize`:@@mining.hashrate:Hashrate`, left: 'center', textStyle: { color: '#FFF', @@ -102,7 +102,7 @@ export class HashrateChartComponent implements OnInit { giga: Math.pow(10, 9), mega: Math.pow(10, 6), kilo: Math.pow(10, 3), - } + }; let selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' }; if (val < powerOfTen.mega) { @@ -135,9 +135,11 @@ export class HashrateChartComponent implements OnInit { type: 'line', smooth: false, lineStyle: { - width: 3, + width: 2, + }, + areaStyle: { + opacity: 0.25 }, - areaStyle: {}, }, dataZoom: this.widget ? null : [{ type: 'inside', From 53a8d5b24639402eaebf4d8c759fe604abf7ac84 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 21 Feb 2022 12:23:32 +0900 Subject: [PATCH 23/65] Add network hashrate to mining dashboard --- .../mining-dashboard/mining-dashboard.component.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 f8f52fc9a..35a26099b 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html @@ -14,6 +14,18 @@ + +
+
Hashrate (1y)
+
+ +
+
+
Difficulty (1y)
From ac118141ce0feb9cba40c6e4d7bd68094d1c354f Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 21 Feb 2022 14:02:41 +0900 Subject: [PATCH 24/65] Make hashrate chart more responsive --- .../hashrate-chart.component.html | 15 ++++---- .../hashrate-chart.component.scss | 34 +++++++++++++++++++ .../hashrate-chart.component.ts | 10 +++++- 3 files changed, 51 insertions(+), 8 deletions(-) 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 cfcd15bfe..263df95b2 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -1,11 +1,6 @@ -
+
-
-
-
-
- -
+
+ +
+
+
+
+
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 c3a63e9fa..30a810ca4 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss @@ -8,3 +8,37 @@ text-align: center; padding-bottom: 3px; } + +.full-container { + width: 100%; + height: calc(100% - 100px); + @media (max-width: 992px) { + height: calc(100% - 140px); + }; + @media (max-width: 576px) { + height: calc(100% - 180px); + }; +} + +.chart { + width: 100%; + height: 100%; + padding: 1.25rem; +} + +.formRadioGroup { + margin-top: 6px; + display: flex; + flex-direction: column; + @media (min-width: 830px) { + flex-direction: row; + float: right; + margin-top: 0px; + } + .btn-sm { + font-size: 9px; + @media (min-width: 830px) { + font-size: 14px; + } + } +} 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 8a3413db5..24f0befd3 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -22,12 +22,16 @@ import { FormBuilder, FormGroup } from '@angular/forms'; }) export class HashrateChartComponent implements OnInit { @Input() widget: boolean = false; + @Input() right: number | string = 10; + @Input() left: number | string = 75; radioGroupForm: FormGroup; chartOptions: EChartsOption = {}; chartInitOptions = { - renderer: 'svg' + renderer: 'svg', + width: 'auto', + height: 'auto', }; hashrateObservable$: Observable; @@ -73,6 +77,10 @@ export class HashrateChartComponent implements OnInit { prepareChartOptions(data) { this.chartOptions = { + grid: { + right: this.right, + left: this.left, + }, title: { text: this.widget ? '' : $localize`:@@mining.hashrate:Hashrate`, left: 'center', From e4721e8574ebbf5890f3580556a684fc3c989ae9 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 21 Feb 2022 14:36:48 +0900 Subject: [PATCH 25/65] Improve hashrate indexing logs --- backend/src/api/mining.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index 792e93f45..db73117ce 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -118,18 +118,21 @@ class Mining { } this.hashrateIndexingStarted = true; - const oldestIndexedBlockHeight = await BlocksRepository.$getOldestIndexedBlockHeight(); + const totalDayIndexed = (await BlocksRepository.$blockCount(null, null)) / 144; const indexedTimestamp = (await HashratesRepository.$get(null)).map(hashrate => hashrate.timestamp); - + let startedAt = new Date().getTime() / 1000; const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f const lastMidnight = new Date(); lastMidnight.setUTCHours(0); lastMidnight.setUTCMinutes(0); lastMidnight.setUTCSeconds(0); lastMidnight.setUTCMilliseconds(0); let toTimestamp = Math.round(lastMidnight.getTime() / 1000); + let indexedThisRun = 0; + let totalIndexed = 0; while (toTimestamp > genesisTimestamp) { const fromTimestamp = toTimestamp - 86400; if (indexedTimestamp.includes(fromTimestamp)) { toTimestamp -= 86400; + ++totalIndexed; continue; } @@ -145,10 +148,14 @@ class Mining { lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, blockStats.lastBlockHeight); - if (toTimestamp % 864000 === 0) { // Log every 10 days during initial indexing + const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); + if (elapsedSeconds > 1) { + const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const formattedDate = new Date(fromTimestamp * 1000).toUTCString(); - const blocksLeft = blockStats.lastBlockHeight - oldestIndexedBlockHeight; - logger.debug(`Counting blocks and hashrate for ${formattedDate}. ${blocksLeft} blocks left`); + const daysLeft = Math.round(totalDayIndexed - totalIndexed); + logger.debug(`Getting hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ~${daysLeft} days left to index`); + startedAt = new Date().getTime() / 1000; + indexedThisRun = 0; } await HashratesRepository.$saveDailyStat({ @@ -158,6 +165,8 @@ class Mining { }); toTimestamp -= 86400; + ++indexedThisRun; + ++totalIndexed; } await HashratesRepository.$setLatestRunTimestamp(); From e5907159b8db04c0e811a3193e2d9aafcfbbec12 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 21 Feb 2022 15:55:27 +0900 Subject: [PATCH 26/65] Refactor power of ten conversion into one wrapper --- frontend/src/app/bitcoin.utils.ts | 29 +++++++++++++++++++ .../difficulty-chart.component.ts | 10 ++----- .../hashrate-chart.component.ts | 24 ++------------- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index ff2d7a885..72fde7471 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -130,3 +130,32 @@ export const formatNumber = (s, precision = null) => { // Utilities for segwitFeeGains const witnessSize = (vin: Vin) => vin.witness.reduce((S, w) => S + (w.length / 2), 0); const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0; + +// Power of ten wrapper +export function selectPowerOfTen(val: number) { + const powerOfTen = { + exa: Math.pow(10, 18), + peta: Math.pow(10, 15), + terra: Math.pow(10, 12), + giga: Math.pow(10, 9), + mega: Math.pow(10, 6), + kilo: Math.pow(10, 3), + }; + + let selectedPowerOfTen; + if (val < powerOfTen.mega) { + selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling + } else if (val < powerOfTen.giga) { + selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' }; + } else if (val < powerOfTen.terra) { + selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' }; + } else if (val < powerOfTen.peta) { + selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' }; + } else if (val < powerOfTen.exa) { + selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' }; + } else { + selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' }; + } + + return selectedPowerOfTen; +} \ No newline at end of file diff --git a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts index a8865ec09..6ce15599c 100644 --- a/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts +++ b/frontend/src/app/components/difficulty-chart/difficulty-chart.component.ts @@ -6,6 +6,7 @@ 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 { selectPowerOfTen } from 'src/app/bitcoin.utils'; @Component({ selector: 'app-difficulty-chart', @@ -70,15 +71,8 @@ export class DifficultyChartComponent implements OnInit { const tableData = []; for (let i = 0; i < data.adjustments.length - 1; ++i) { + const selectedPowerOfTen: any = selectPowerOfTen(data.adjustments[i].difficulty); const change = (data.adjustments[i].difficulty / data.adjustments[i + 1].difficulty - 1) * 100; - let selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' }; - if (data.adjustments[i].difficulty < powerOfTen.mega) { - selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling - } else if (data.adjustments[i].difficulty < powerOfTen.giga) { - selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' }; - } else if (data.adjustments[i].difficulty < powerOfTen.terra) { - selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' }; - } tableData.push(Object.assign(data.adjustments[i], { change: change, 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 24f0befd3..0f5a6a98e 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -6,6 +6,7 @@ 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 { selectPowerOfTen } from 'src/app/bitcoin.utils'; @Component({ selector: 'app-hashrate-chart', @@ -103,28 +104,7 @@ export class HashrateChartComponent implements OnInit { type: 'value', axisLabel: { formatter: (val) => { - const powerOfTen = { - exa: Math.pow(10, 18), - peta: Math.pow(10, 15), - terra: Math.pow(10, 12), - giga: Math.pow(10, 9), - mega: Math.pow(10, 6), - kilo: Math.pow(10, 3), - }; - - let selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' }; - if (val < powerOfTen.mega) { - selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling - } else if (val < powerOfTen.giga) { - selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' }; - } else if (val < powerOfTen.terra) { - selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' }; - } else if (val < powerOfTen.peta) { - selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' }; - } else if (val < powerOfTen.exa) { - selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' }; - } - + const selectedPowerOfTen: any = selectPowerOfTen(val); const newVal = val / selectedPowerOfTen.divider; return `${newVal} ${selectedPowerOfTen.unit}` } From bb1c5d0b31948710dc69737d26c448134a4ac882 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 21 Feb 2022 16:38:18 +0900 Subject: [PATCH 27/65] Add `--reindex` command line parameter to force full re-indexing --- backend/src/api/database-migration.ts | 23 +++++++++++++++++++++++ backend/src/index.ts | 6 ++++++ 2 files changed, 29 insertions(+) diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 098394664..3e9762316 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -419,6 +419,29 @@ class DatabaseMigration { FOREIGN KEY (pool_id) REFERENCES pools (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } + + public async $truncateIndexedData(tables: string[]) { + const allowedTables = ['blocks', 'hashrates']; + + const connection = await DB.pool.getConnection(); + try { + for (const table of tables) { + if (!allowedTables.includes(table)) { + logger.info(`Table ${table} cannot to be re-indexed (not allowed)`); + continue; + }; + + await this.$executeQuery(connection, `TRUNCATE ${table}`, true); + if (table === 'hashrates') { + await this.$executeQuery(connection, 'UPDATE state set number = 0 where name = "last_hashrates_indexing"', true); + } + logger.info(`Table ${table} has been truncated`); + } + } catch (e) { + logger.warn(`Unable to erase indexed data`); + } + connection.release(); + } } export default new DatabaseMigration(); diff --git a/backend/src/index.ts b/backend/src/index.ts index 4fe66bc72..64ec25382 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -89,6 +89,12 @@ class Server { if (config.DATABASE.ENABLED) { await 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') ...`); + await Common.sleep(5000); + await databaseMigration.$truncateIndexedData(tables); + } await databaseMigration.$initializeOrMigrateDatabase(); await poolsParser.migratePoolsJson(); } catch (e) { From 537e50c682b464ae69a451ff64c4e67fdbe25085 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 21 Feb 2022 16:54:43 +0900 Subject: [PATCH 28/65] Reduce log spam during hashrate indexing --- backend/src/api/mining.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index db73117ce..2411420cb 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -111,13 +111,13 @@ class Mining { return; } - logger.info(`Indexing hashrates`); - if (this.hashrateIndexingStarted) { return; } this.hashrateIndexingStarted = true; + logger.info(`Indexing hashrates`); + const totalDayIndexed = (await BlocksRepository.$blockCount(null, null)) / 144; const indexedTimestamp = (await HashratesRepository.$get(null)).map(hashrate => hashrate.timestamp); let startedAt = new Date().getTime() / 1000; @@ -149,7 +149,7 @@ class Mining { blockStats.lastBlockHeight); const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); - if (elapsedSeconds > 1) { + if (elapsedSeconds > 10) { const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const formattedDate = new Date(fromTimestamp * 1000).toUTCString(); const daysLeft = Math.round(totalDayIndexed - totalIndexed); From 649ad2e859e9a6ebdd8b3fcb292112aaa91c2fb2 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 21 Feb 2022 17:34:07 +0900 Subject: [PATCH 29/65] Hashrates indexing waits for blocks indexing - Batch hashrates I/O ops --- backend/src/api/blocks.ts | 3 +++ backend/src/api/mining.ts | 18 ++++++++++----- backend/src/index.ts | 6 ++--- .../src/repositories/HashratesRepository.ts | 22 +++++++++---------- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 483fd3f3e..de461e095 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -20,6 +20,7 @@ class Blocks { private previousDifficultyRetarget = 0; private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; private blockIndexingStarted = false; + public blockIndexingCompleted = false; constructor() { } @@ -240,6 +241,8 @@ class Blocks { logger.err('An error occured in $generateBlockDatabase(). Skipping block indexing. ' + e); console.log(e); } + + this.blockIndexingCompleted = true; } public async $updateBlocks() { diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index 2411420cb..1d5142080 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -4,6 +4,7 @@ import PoolsRepository from '../repositories/PoolsRepository'; import HashratesRepository from '../repositories/HashratesRepository'; import bitcoinClient from './bitcoin/bitcoin-client'; import logger from '../logger'; +import blocks from './blocks'; class Mining { hashrateIndexingStarted = false; @@ -111,7 +112,7 @@ class Mining { return; } - if (this.hashrateIndexingStarted) { + if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) { return; } this.hashrateIndexingStarted = true; @@ -128,6 +129,8 @@ class Mining { let indexedThisRun = 0; let totalIndexed = 0; + const hashrates: any[] = []; + while (toTimestamp > genesisTimestamp) { const fromTimestamp = toTimestamp - 86400; if (indexedTimestamp.includes(fromTimestamp)) { @@ -137,9 +140,7 @@ class Mining { } const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp( - null, fromTimestamp, toTimestamp - ); - + null, fromTimestamp, toTimestamp); if (blockStats.blockCount === 0) { // We are done indexing, no blocks left break; } @@ -158,23 +159,28 @@ class Mining { indexedThisRun = 0; } - await HashratesRepository.$saveDailyStat({ + hashrates.push({ hashrateTimestamp: fromTimestamp, avgHashrate: lastBlockHashrate, poolId: null, }); + if (hashrates.length > 100) { + await HashratesRepository.$saveHashrates(hashrates); + hashrates.length = 0; + } + toTimestamp -= 86400; ++indexedThisRun; ++totalIndexed; } + await HashratesRepository.$saveHashrates(hashrates); await HashratesRepository.$setLatestRunTimestamp(); this.hashrateIndexingStarted = false; logger.info(`Hashrates indexing completed`); } - } export default new Mining(); diff --git a/backend/src/index.ts b/backend/src/index.ts index 64ec25382..d051766fa 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -164,12 +164,12 @@ class Server { } } - async runIndexingWhenReady() { + runIndexingWhenReady() { if (!Common.indexingEnabled() || mempool.hasPriority()) { return; } - await blocks.$generateBlockDatabase(); - await mining.$generateNetworkHashrateHistory(); + blocks.$generateBlockDatabase(); + mining.$generateNetworkHashrateHistory(); } setUpWebsocketHandling() { diff --git a/backend/src/repositories/HashratesRepository.ts b/backend/src/repositories/HashratesRepository.ts index 2a898e5bd..0e8f1477e 100644 --- a/backend/src/repositories/HashratesRepository.ts +++ b/backend/src/repositories/HashratesRepository.ts @@ -6,21 +6,19 @@ class HashratesRepository { /** * Save indexed block data in the database */ - public async $saveDailyStat(dailyStat: any) { + public async $saveHashrates(hashrates: any) { + let query = `INSERT INTO + hashrates(hashrate_timestamp, avg_hashrate, pool_id) VALUES`; + + for (const hashrate of hashrates) { + query += ` (FROM_UNIXTIME(${hashrate.hashrateTimestamp}), ${hashrate.avgHashrate}, ${hashrate.poolId}),`; + } + query = query.slice(0, -1); + const connection = await DB.pool.getConnection(); - try { - const query = `INSERT INTO - hashrates(hashrate_timestamp, avg_hashrate, pool_id) - VALUE (FROM_UNIXTIME(?), ?, ?)`; - - const params: any[] = [ - dailyStat.hashrateTimestamp, dailyStat.avgHashrate, - dailyStat.poolId - ]; - // logger.debug(query); - await connection.query(query, params); + await connection.query(query); } catch (e: any) { logger.err('$saveHashrateInDatabase() error' + (e instanceof Error ? e.message : e)); } From 6e62c628553e805f0bd3a1063d401d125ada589b Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 21 Feb 2022 18:01:09 +0900 Subject: [PATCH 30/65] Add /api/v1/mining/hashrate/* apis to the cache warmer --- production/nginx-cache-warmer | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/production/nginx-cache-warmer b/production/nginx-cache-warmer index e3ecf6c91..8c679980c 100755 --- a/production/nginx-cache-warmer +++ b/production/nginx-cache-warmer @@ -21,6 +21,12 @@ do for url in / \ '/api/v1/mining/pools/2y' \ '/api/v1/mining/pools/3y' \ '/api/v1/mining/pools/all' \ + '/api/v1/mining/hashrate/3m' \ + '/api/v1/mining/hashrate/6m' \ + '/api/v1/mining/hashrate/1y' \ + '/api/v1/mining/hashrate/2y' \ + '/api/v1/mining/hashrate/3y' \ + '/api/v1/mining/hashrate/all' \ do curl -s "https://${hostname}${url}" >/dev/null From 413cf3ccaafa68c7c2f40250d051b6ac5feaea87 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 21 Feb 2022 18:19:03 +0900 Subject: [PATCH 31/65] Fix 'active' menu when using mining dashboard --- .../src/app/components/master-page/master-page.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index bab0f42e9..28db64bbd 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -31,7 +31,7 @@ -