From 74b420c258b960332aa77f87a602fa09bb577425 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 9 Oct 2024 17:05:29 +0900 Subject: [PATCH 01/91] Use adjusted block time for difficulty and ETA calculation --- .../src/app/components/difficulty/difficulty.component.html | 2 +- frontend/src/app/services/eta.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/difficulty/difficulty.component.html b/frontend/src/app/components/difficulty/difficulty.component.html index e9bf36515..c70656e04 100644 --- a/frontend/src/app/components/difficulty/difficulty.component.html +++ b/frontend/src/app/components/difficulty/difficulty.component.html @@ -45,7 +45,7 @@
- ~ + ~
Average block time
diff --git a/frontend/src/app/services/eta.service.ts b/frontend/src/app/services/eta.service.ts index 6834237b6..bae442aa3 100644 --- a/frontend/src/app/services/eta.service.ts +++ b/frontend/src/app/services/eta.service.ts @@ -55,7 +55,7 @@ export class EtaService { return { hashratePercentage: acceleratingHashrateFraction * 100, - ETA: Date.now() + da.timeAvg * mempoolPosition.block, + ETA: Date.now() + da.adjustedTimeAvg * mempoolPosition.block, acceleratedETA: this.calculateETAFromShares([ { block: mempoolPosition.block, hashrateShare: (1 - acceleratingHashrateFraction) }, { block: 0, hashrateShare: acceleratingHashrateFraction }, @@ -216,7 +216,7 @@ export class EtaService { } // at max depth, the transaction is guaranteed to be mined in the next block if it hasn't already Q += ((max + 1) * (1-tailProb)); - const eta = da.timeAvg * Q; // T x Q + const eta = da.adjustedTimeAvg * Q; // T x Q return { now, From 7a8ae7c9a6ad352937e1958e61ecf3e611cc2444 Mon Sep 17 00:00:00 2001 From: natsoni Date: Thu, 17 Oct 2024 16:33:36 +0200 Subject: [PATCH 02/91] Fix input/output overflow in transaction list --- .../transactions-list/transactions-list.component.html | 4 ++-- .../transactions-list/transactions-list.component.ts | 4 ++++ frontend/src/app/interfaces/electrs.interface.ts | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 217eab7d7..58669e75a 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -81,7 +81,7 @@
- +
@@ -257,7 +257,7 @@ - +
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index 7bb1604c6..b19fe1ae3 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -252,6 +252,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { const hasAnnex = tx.vin[i].witness?.[tx.vin[i].witness.length - 1].startsWith('50'); if (tx.vin[i].witness.length > (hasAnnex ? 2 : 1) && tx.vin[i].witness[tx.vin[i].witness.length - (hasAnnex ? 3 : 2)].includes('0063036f7264')) { tx.vin[i].isInscription = true; + tx.largeInput = true; } } } @@ -262,6 +263,9 @@ export class TransactionsListComponent implements OnInit, OnChanges { } } } + + tx.largeInput = tx.largeInput || tx.vin.some(vin => (vin?.prevout?.value > 1000000000)); + tx.largeOutput = tx.vout.some(vout => (vout?.value > 1000000000)); }); if (this.blockTime && this.transactions?.length && this.currency) { diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index 95a749b60..ce9508479 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -32,6 +32,8 @@ export interface Transaction { price?: Price; sigops?: number; flags?: bigint; + largeInput?: boolean; + largeOutput?: boolean; } export interface TransactionChannels { From f2e7cf7441af2ac83db87121dfcb44b4e3dada85 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 7 Aug 2024 11:40:01 +0200 Subject: [PATCH 03/91] [btcpay] better handling for invoice expiration --- .../bitcoin-invoice.component.html | 4 ++ .../bitcoin-invoice.component.ts | 62 +++++++++++-------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html index 790f046f7..932586c6d 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html @@ -10,6 +10,10 @@ } +
+ +
+
diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts index 067061678..e248079f9 100644 --- a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts @@ -1,9 +1,8 @@ import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; -import { ActivatedRoute } from '@angular/router'; -import { Subscription, of, timer } from 'rxjs'; -import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators'; +import { Subscription, tap, catchError, of } from 'rxjs'; +import { retry } from 'rxjs/operators'; import { ServicesApiServices } from '../../services/services-api.service'; @Component({ @@ -18,30 +17,17 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy { @Output() completed = new EventEmitter(); paymentForm: FormGroup; - requestSubscription: Subscription | undefined; paymentStatusSubscription: Subscription | undefined; paymentStatus = 1; // 1 - Waiting for invoice | 2 - Pending payment | 3 - Payment completed - paramMapSubscription: Subscription | undefined; - invoiceSubscription: Subscription | undefined; - invoiceTimeout; // Wait for angular to load all the things before making a request + paymentErrorMessage: string = ''; constructor( private formBuilder: FormBuilder, private apiService: ServicesApiServices, - private sanitizer: DomSanitizer, - private activatedRoute: ActivatedRoute + private sanitizer: DomSanitizer ) { } ngOnDestroy() { - if (this.requestSubscription) { - this.requestSubscription.unsubscribe(); - } - if (this.paramMapSubscription) { - this.paramMapSubscription.unsubscribe(); - } - if (this.invoiceSubscription) { - this.invoiceSubscription.unsubscribe(); - } if (this.paymentStatusSubscription) { this.paymentStatusSubscription.unsubscribe(); } @@ -72,15 +58,39 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy { } else { this.paymentStatus = 4; } + + this.monitorPendingInvoice(); + } + + monitorPendingInvoice(): void { + if (!this.invoice) { + return; + } + if (this.paymentStatusSubscription) { + this.paymentStatusSubscription.unsubscribe(); + } this.paymentStatusSubscription = this.apiService.getPaymentStatus$(this.invoice.btcpayInvoiceId).pipe( - retry({ delay: () => timer(2000)}), - repeat({delay: 2000}), - filter((response) => response.status !== 204 && response.status !== 404), - take(1), - ).subscribe(() => { - this.paymentStatus = 3; - this.completed.emit(); - }); + tap(result => { + if (result.status === 204) { // Manually trigger an error in that case so we can retry + throw result; + } else if (result.status === 200) { // Invoice settled + this.paymentStatus = 3; + this.completed.emit(); + } + }), + catchError(err => { + if (err.status === 204 || err.status === 504) { + throw err; // Will trigger the retry + } else if (err.status === 400) { + this.paymentErrorMessage = 'Invoice has expired'; + } else if (err.status === 404) { + this.paymentErrorMessage = 'Invoice is no longer valid'; + } + this.paymentStatus = -1; + return of(null); + }), + retry({ delay: 1000 }), + ).subscribe(); } get availableMethods(): string[] { From cdc4a430cdef5881d01e7429a979c6bb9f23cc18 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Thu, 31 Oct 2024 09:52:34 +0100 Subject: [PATCH 04/91] [refactor] cleaning users.full_name --- frontend/src/app/services/services-api.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 2b0f884ff..1f51c4afa 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -18,7 +18,6 @@ export interface IUser { subscription_tag: string; status: 'pending' | 'verified' | 'disabled'; features: string | null; - fullName: string | null; countryCode: string | null; imageMd5: string; ogRank: number | null; From 5a3ee725b81775b9a1f6c66734277ff61ada3499 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 13 Nov 2024 14:43:04 +0100 Subject: [PATCH 05/91] Use timestamp component --- .../app/components/blocks-list/blocks-list.component.html | 2 +- .../federation-utxos-list.component.html | 3 +-- .../recent-pegs-list/recent-pegs-list.component.html | 3 +-- frontend/src/app/components/pool/pool.component.html | 2 +- frontend/src/app/components/tracker/tracker.component.html | 2 +- .../transaction-details/transaction-details.component.html | 5 +---- .../transactions-list/transactions-list.component.html | 2 +- .../src/app/lightning/channel/channel-preview.component.html | 2 +- .../app/lightning/justice-list/justice-list.component.html | 2 +- 9 files changed, 9 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.html b/frontend/src/app/components/blocks-list/blocks-list.component.html index 807d429bf..622f56f69 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.html +++ b/frontend/src/app/components/blocks-list/blocks-list.component.html @@ -49,7 +49,7 @@
- ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }} + - ‎{{ utxo.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }} -
()
+ {{ utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate < 0 ? -(utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate) : utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate }} blocks diff --git a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html index b21d83b4e..97c1d96cd 100644 --- a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html +++ b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html @@ -53,8 +53,7 @@ - ‎{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }} -
()
+ diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index b74ecdf81..b3c6430a8 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -194,7 +194,7 @@
{{ block.height }} - ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }} + diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index 2d9bd4982..797694919 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -88,7 +88,7 @@
Confirmed at
- ‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }} +
()
diff --git a/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html index 7a355f38d..eee29397e 100644 --- a/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html +++ b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html @@ -61,10 +61,7 @@ Timestamp - ‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }} -
- () -
+ } @else { diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 5ad1c798c..9f1d8ad3b 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -6,7 +6,7 @@
- ‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }} + diff --git a/frontend/src/app/lightning/channel/channel-preview.component.html b/frontend/src/app/lightning/channel/channel-preview.component.html index 108fe2e95..4d71bcef0 100644 --- a/frontend/src/app/lightning/channel/channel-preview.component.html +++ b/frontend/src/app/lightning/channel/channel-preview.component.html @@ -21,7 +21,7 @@ Created - {{ channel.created | date:'yyyy-MM-dd HH:mm' }} + Capacity diff --git a/frontend/src/app/lightning/justice-list/justice-list.component.html b/frontend/src/app/lightning/justice-list/justice-list.component.html index 482ac9646..9f341b0c8 100644 --- a/frontend/src/app/lightning/justice-list/justice-list.component.html +++ b/frontend/src/app/lightning/justice-list/justice-list.component.html @@ -19,7 +19,7 @@ - ‎{{ channel.closing_date | date:'yyyy-MM-dd HH:mm' }} + From 72ddb8c6a48ac274964846c99055e51a6ead1c63 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Thu, 14 Nov 2024 16:46:18 +0100 Subject: [PATCH 06/91] [accelerator] display payment errors, auto reload after 10 secs instead of 3 secs --- .../accelerate-checkout.component.html | 16 ++++++++++++---- .../accelerate-checkout.component.ts | 6 +++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index df67de65c..644d3e9d5 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -1,10 +1,18 @@
@if (accelerateError) { -
-
-

Sorry, something went wrong!

+ @if (accelerateError.includes('Payment declined')) { +
+
+

{{ accelerateError }}

+
-
+ } @else { +
+
+

Sorry, something went wrong!

+
+
+ }
diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 9d2d2ad46..763332ceb 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -543,7 +543,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { // Reset everything by reloading the page :D, can be improved const urlParams = new URLSearchParams(window.location.search); window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); - }, 3000); + }, 10000); } } }); @@ -643,7 +643,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { // Reset everything by reloading the page :D, can be improved const urlParams = new URLSearchParams(window.location.search); window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); - }, 3000); + }, 10000); } } }); @@ -738,7 +738,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { // Reset everything by reloading the page :D, can be improved const urlParams = new URLSearchParams(window.location.search); window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); - }, 3000); + }, 10000); } } }); From 9e5b7436d4031c8a41ced340ab10e84771bde0e7 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 13 Nov 2024 17:00:32 +0100 Subject: [PATCH 07/91] Add timezone selector --- frontend/src/app/app.constants.ts | 37 +++++++++++- .../timezone-selector.component.html | 8 +++ .../timezone-selector.component.scss | 0 .../timezone-selector.component.ts | 58 +++++++++++++++++++ frontend/src/app/services/state.service.ts | 4 ++ .../global-footer.component.html | 2 +- .../timestamp/timestamp.component.html | 2 +- .../timestamp/timestamp.component.ts | 5 ++ frontend/src/app/shared/shared.module.ts | 3 + 9 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 frontend/src/app/components/timezone-selector/timezone-selector.component.html create mode 100644 frontend/src/app/components/timezone-selector/timezone-selector.component.scss create mode 100644 frontend/src/app/components/timezone-selector/timezone-selector.component.ts diff --git a/frontend/src/app/app.constants.ts b/frontend/src/app/app.constants.ts index cef630984..7cc4b4dff 100644 --- a/frontend/src/app/app.constants.ts +++ b/frontend/src/app/app.constants.ts @@ -439,4 +439,39 @@ export const fiatCurrencies = { code: 'ZAR', indexed: true, }, -}; \ No newline at end of file +}; + +export interface Timezone { + offset: string; + name: string; +} + +export const timezones: Timezone[] = [ + { offset: '-12', name: 'Anywhere on Earth (AoE)' }, + { offset: '-11', name: 'Samoa Standard Time (SST)' }, + { offset: '-10', name: 'Hawaii-Aleutian Standard Time (HST)' }, + { offset: '-9', name: 'Alaska Standard Time (AKST)' }, + { offset: '-8', name: 'Pacific Standard Time (PST)' }, + { offset: '-7', name: 'Mountain Standard Time (MST)' }, + { offset: '-6', name: 'Central Standard Time (CST)' }, + { offset: '-5', name: 'Eastern Standard Time (EST)' }, + { offset: '-4', name: 'Atlantic Standard Time (AST)' }, + { offset: '-3', name: 'Argentina Time (ART)' }, + { offset: '-2', name: 'Fernando de Noronha Time (FNT)' }, + { offset: '-1', name: 'Azores Time (AZOT)' }, + { offset: '+0', name: 'Greenwich Mean Time (GMT)' }, + { offset: '+1', name: 'Central European Time (CET)' }, + { offset: '+2', name: 'Eastern European Time (EET)' }, + { offset: '+3', name: 'Moscow Standard Time (MSK)' }, + { offset: '+4', name: 'Armenia Time (AMT)' }, + { offset: '+5', name: 'Pakistan Standard Time (PKT)' }, + { offset: '+6', name: 'Xinjiang Time (XJT)' }, + { offset: '+7', name: 'Indochina Time (ICT)' }, + { offset: '+8', name: 'Hong Kong Time (HKT)' }, + { offset: '+9', name: 'Japan Standard Time (JST)' }, + { offset: '+10', name: 'Australian Eastern Standard Time (AEST)' }, + { offset: '+11', name: 'Norfolk Time (NFT)' }, + { offset: '+12', name: 'New Zealand Standard Time (NZST)' }, + { offset: '+13', name: 'Tonga Time (TOT)' }, + { offset: '+14', name: 'Line Islands Time (LINT)' } +]; \ No newline at end of file diff --git a/frontend/src/app/components/timezone-selector/timezone-selector.component.html b/frontend/src/app/components/timezone-selector/timezone-selector.component.html new file mode 100644 index 000000000..bd959ac3d --- /dev/null +++ b/frontend/src/app/components/timezone-selector/timezone-selector.component.html @@ -0,0 +1,8 @@ +
+ +
diff --git a/frontend/src/app/components/timezone-selector/timezone-selector.component.scss b/frontend/src/app/components/timezone-selector/timezone-selector.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/timezone-selector/timezone-selector.component.ts b/frontend/src/app/components/timezone-selector/timezone-selector.component.ts new file mode 100644 index 000000000..44c04354e --- /dev/null +++ b/frontend/src/app/components/timezone-selector/timezone-selector.component.ts @@ -0,0 +1,58 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { StorageService } from '@app/services/storage.service'; +import { StateService } from '@app/services/state.service'; +import { timezones } from '@app/app.constants'; + + +@Component({ + selector: 'app-timezone-selector', + templateUrl: './timezone-selector.component.html', + styleUrls: ['./timezone-selector.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TimezoneSelectorComponent implements OnInit { + timezoneForm: UntypedFormGroup; + timezones = timezones; + localTimezoneOffset: string = ''; + localTimezoneName: string; + + constructor( + private formBuilder: UntypedFormBuilder, + private stateService: StateService, + private storageService: StorageService, + ) { } + + ngOnInit() { + this.setLocalTimezone(); + this.timezoneForm = this.formBuilder.group({ + mode: ['local'], + }); + this.stateService.timezone$.subscribe((mode) => { + this.timezoneForm.get('mode')?.setValue(mode); + }); + } + + changeMode() { + const newMode = this.timezoneForm.get('mode')?.value; + this.storageService.setValue('timezone-preference', newMode); + this.stateService.timezone$.next(newMode); + } + + setLocalTimezone() { + const offset = new Date().getTimezoneOffset(); + const sign = offset <= 0 ? "+" : "-"; + const absOffset = Math.abs(offset); + const hours = String(Math.floor(absOffset / 60)); + const minutes = String(absOffset % 60).padStart(2, '0'); + if (minutes === '00') { + this.localTimezoneOffset = `${sign}${hours}`; + } else { + this.localTimezoneOffset = `${sign}${hours.padStart(2, '0')}:${minutes}`; + } + + const timezone = this.timezones.find(tz => tz.offset === this.localTimezoneOffset); + this.timezones = this.timezones.filter(tz => tz.offset !== this.localTimezoneOffset && tz.offset !== '+0'); + this.localTimezoneName = timezone ? timezone.name : ''; + } +} diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 2feb266d1..0d006b552 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -186,6 +186,7 @@ export class StateService { live2Chart$ = new Subject(); viewAmountMode$: BehaviorSubject<'btc' | 'sats' | 'fiat'>; + timezone$: BehaviorSubject; connectionState$ = new BehaviorSubject<0 | 1 | 2>(2); isTabHidden$: Observable; @@ -347,6 +348,9 @@ export class StateService { const viewAmountModePreference = this.storageService.getValue('view-amount-mode') as 'btc' | 'sats' | 'fiat'; this.viewAmountMode$ = new BehaviorSubject<'btc' | 'sats' | 'fiat'>(viewAmountModePreference || 'btc'); + const timezonePreference = this.storageService.getValue('timezone-preference'); + this.timezone$ = new BehaviorSubject(timezonePreference || 'local'); + this.backend$.subscribe(backend => { this.backend = backend; }); diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index d82bb8062..7f51c3ede 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -30,7 +30,7 @@
- +
diff --git a/frontend/src/app/shared/components/timestamp/timestamp.component.html b/frontend/src/app/shared/components/timestamp/timestamp.component.html index 7b77cb1a3..097867b42 100644 --- a/frontend/src/app/shared/components/timestamp/timestamp.component.html +++ b/frontend/src/app/shared/components/timestamp/timestamp.component.html @@ -1,6 +1,6 @@ - - ‎{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }} + ‎{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' : (stateService.timezone$ | async) }}
()
diff --git a/frontend/src/app/shared/components/timestamp/timestamp.component.ts b/frontend/src/app/shared/components/timestamp/timestamp.component.ts index aace6efbf..5ca6a750b 100644 --- a/frontend/src/app/shared/components/timestamp/timestamp.component.ts +++ b/frontend/src/app/shared/components/timestamp/timestamp.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; +import { StateService } from '@app/services/state.service'; @Component({ selector: 'app-timestamp', @@ -16,6 +17,10 @@ export class TimestampComponent implements OnChanges { seconds: number | undefined = undefined; + constructor( + public stateService: StateService, + ) { } + ngOnChanges(): void { if (this.unixTime) { this.seconds = this.unixTime; diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index a855f11b5..ce5ac0f65 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -36,6 +36,7 @@ import { FiatSelectorComponent } from '@components/fiat-selector/fiat-selector.c import { RateUnitSelectorComponent } from '@components/rate-unit-selector/rate-unit-selector.component'; import { ThemeSelectorComponent } from '@components/theme-selector/theme-selector.component'; import { AmountSelectorComponent } from '@components/amount-selector/amount-selector.component'; +import { TimezoneSelectorComponent } from '@components/timezone-selector/timezone-selector.component'; import { BrowserOnlyDirective } from '@app/shared/directives/browser-only.directive'; import { ServerOnlyDirective } from '@app/shared/directives/server-only.directive'; import { ColoredPriceDirective } from '@app/shared/directives/colored-price.directive'; @@ -134,6 +135,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/ ThemeSelectorComponent, RateUnitSelectorComponent, AmountSelectorComponent, + TimezoneSelectorComponent, ScriptpubkeyTypePipe, RelativeUrlPipe, NoSanitizePipe, @@ -283,6 +285,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/ RateUnitSelectorComponent, ThemeSelectorComponent, AmountSelectorComponent, + TimezoneSelectorComponent, ScriptpubkeyTypePipe, RelativeUrlPipe, Hex2asciiPipe, From c8e967cc0c1563287fb1fc6096b96ac7efcbddb0 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Fri, 29 Nov 2024 16:04:54 +0100 Subject: [PATCH 08/91] [blocks] save pools-v2.json hash in blocks table --- backend/src/api/database-migration.ts | 8 +++++- backend/src/api/pools-parser.ts | 9 ------- backend/src/repositories/BlocksRepository.ts | 28 +++++++++++--------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 0c17ab9f1..b9d8530bb 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository'; import { RowDataPacket } from 'mysql2'; class DatabaseMigration { - private static currentVersion = 91; + private static currentVersion = 92; private queryTimeout = 3600_000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -775,6 +775,12 @@ class DatabaseMigration { await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)'); await this.updateToSchemaVersion(91); } + + // blocks pools-v2.json hash + if (databaseSchemaVersion < 92) { + await this.$executeQuery('ALTER TABLE `blocks` ADD definition_hash varchar(255) NOT NULL DEFAULT "5f32a67401929169f225f5db43c9efa795d1b159"'); + await this.updateToSchemaVersion(92); + } } /** diff --git a/backend/src/api/pools-parser.ts b/backend/src/api/pools-parser.ts index 289389d5e..2fd55d6c5 100644 --- a/backend/src/api/pools-parser.ts +++ b/backend/src/api/pools-parser.ts @@ -19,15 +19,6 @@ class PoolsParser { 'addresses': '[]', 'slug': 'unknown' }; - private uniqueLogs: string[] = []; - - private uniqueLog(loggerFunction: any, msg: string): void { - if (this.uniqueLogs.includes(msg)) { - return; - } - this.uniqueLogs.push(msg); - loggerFunction(msg); - } public setMiningPools(pools): void { for (const pool of pools) { diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 424a668c7..f673e574b 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -15,6 +15,7 @@ import blocks from '../api/blocks'; import BlocksAuditsRepository from './BlocksAuditsRepository'; import transactionUtils from '../api/transaction-utils'; import { parseDATUMTemplateCreator } from '../utils/bitcoin-script'; +import poolsUpdater from '../tasks/pools-updater'; interface DatabaseBlock { id: string; @@ -114,16 +115,16 @@ class BlocksRepository { try { const query = `INSERT INTO blocks( - height, hash, blockTimestamp, size, - weight, tx_count, coinbase_raw, difficulty, - pool_id, fees, fee_span, median_fee, - reward, version, bits, nonce, - merkle_root, previous_block_hash, avg_fee, avg_fee_rate, - median_timestamp, header, coinbase_address, coinbase_addresses, - coinbase_signature, utxoset_size, utxoset_change, avg_tx_size, - total_inputs, total_outputs, total_input_amt, total_output_amt, - fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight, - median_fee_amt, coinbase_signature_ascii + height, hash, blockTimestamp, size, + weight, tx_count, coinbase_raw, difficulty, + pool_id, fees, fee_span, median_fee, + reward, version, bits, nonce, + merkle_root, previous_block_hash, avg_fee, avg_fee_rate, + median_timestamp, header, coinbase_address, coinbase_addresses, + coinbase_signature, utxoset_size, utxoset_change, avg_tx_size, + total_inputs, total_outputs, total_input_amt, total_output_amt, + fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight, + median_fee_amt, coinbase_signature_ascii, definition_hash ) VALUE ( ?, ?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, @@ -134,7 +135,7 @@ class BlocksRepository { ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, - ?, ? + ?, ?, ? )`; const poolDbId = await PoolsRepository.$getPoolByUniqueId(block.extras.pool.id); @@ -181,6 +182,7 @@ class BlocksRepository { block.extras.segwitTotalWeight, block.extras.medianFeeAmt, truncatedCoinbaseSignatureAscii, + poolsUpdater.currentSha ]; await DB.query(query, params); @@ -1013,9 +1015,9 @@ class BlocksRepository { public async $savePool(id: string, poolId: number): Promise { try { await DB.query(` - UPDATE blocks SET pool_id = ? + UPDATE blocks SET pool_id = ?, definition_hash = ? WHERE hash = ?`, - [poolId, id] + [poolId, poolsUpdater.currentSha, id] ); } catch (e) { logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e)); From ac997f3d9e8456548f58b4b29bd253da3a2c8b9d Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Fri, 29 Nov 2024 17:13:28 +0100 Subject: [PATCH 09/91] [blocks] add 2 endpoints to retreive pools-v2.json hashes --- backend/src/api/bitcoin/bitcoin.routes.ts | 31 +++++++++++++++++++++++ backend/src/api/blocks.ts | 15 ++++++++++- backend/src/api/database-migration.ts | 1 + backend/src/mempool.interfaces.ts | 2 ++ backend/src/tasks/pools-updater.ts | 2 +- 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index d2d298e09..c95d89d0f 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -21,6 +21,7 @@ import transactionRepository from '../../repositories/TransactionRepository'; import rbfCache from '../rbf-cache'; import { calculateMempoolTxCpfp } from '../cpfp'; import { handleError } from '../../utils/api'; +import poolsUpdater from '../../tasks/pools-updater'; class BitcoinRoutes { public initRoutes(app: Application) { @@ -46,6 +47,8 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) + .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/definition/list', this.getBlockDefinitionHashes) + .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/definition/current', this.getCurrentBlockDefinitionHash) .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) @@ -657,6 +660,34 @@ class BitcoinRoutes { } } + private async getBlockDefinitionHashes(req: Request, res: Response) { + try { + const result = await blocks.$getBlockDefinitionHashes(); + if (!result) { + handleError(req, res, 503, `Service Temporarily Unavailable`); + return; + } + res.setHeader('content-type', 'application/json'); + res.send(result); + } catch (e) { + handleError(req, res, 500, e instanceof Error ? e.message : e); + } + } + + private async getCurrentBlockDefinitionHash(req: Request, res: Response) { + try { + const currentSha = await poolsUpdater.getShaFromDb(); + if (!currentSha) { + handleError(req, res, 503, `Service Temporarily Unavailable`); + return; + } + res.setHeader('content-type', 'text/plain'); + res.send(currentSha); + } catch (e) { + handleError(req, res, 500, e instanceof Error ? e.message : e); + } + } + private getBlockTipHeight(req: Request, res: Response) { try { const result = blocks.getCurrentBlockHeight(); diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index e621056ab..2b468a8f8 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -33,8 +33,8 @@ import AccelerationRepository from '../repositories/AccelerationRepository'; import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp'; import mempool from './mempool'; import CpfpRepository from '../repositories/CpfpRepository'; -import accelerationApi from './services/acceleration'; import { parseDATUMTemplateCreator } from '../utils/bitcoin-script'; +import database from '../database'; class Blocks { private blocks: BlockExtended[] = []; @@ -1462,6 +1462,19 @@ class Blocks { // not a fatal error, we'll try again next time the indexer runs } } + + public async $getBlockDefinitionHashes(): Promise { + try { + const [rows]: any = await database.query(`SELECT DISTINCT(definition_hash) FROM blocks`); + if (rows && rows.length) { + return rows.map(r => r.definition_hash); + } + } catch (e) { + // we just return an empty array + } + logger.debug(`Unable to retreive list of blocks.definition_hash from db`); + return []; + } } export default new Blocks(); diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index b9d8530bb..47fd696e4 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -779,6 +779,7 @@ class DatabaseMigration { // blocks pools-v2.json hash if (databaseSchemaVersion < 92) { await this.$executeQuery('ALTER TABLE `blocks` ADD definition_hash varchar(255) NOT NULL DEFAULT "5f32a67401929169f225f5db43c9efa795d1b159"'); + await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `definition_hash` (`definition_hash`)'); await this.updateToSchemaVersion(92); } } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index dc703af21..e1da4be6f 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -325,6 +325,8 @@ export interface BlockExtension { // Requires coinstatsindex, will be set to NULL otherwise utxoSetSize: number | null; totalInputAmt: number | null; + // pools-v2.json git hash + definitionHash: string | undefined; } /** diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts index 652383a2a..502e3613f 100644 --- a/backend/src/tasks/pools-updater.ts +++ b/backend/src/tasks/pools-updater.ts @@ -121,7 +121,7 @@ class PoolsUpdater { /** * Fetch our latest pools-v2.json sha from the db */ - private async getShaFromDb(): Promise { + public async getShaFromDb(): Promise { try { const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); return (rows.length > 0 ? rows[0].string : null); From 4ff2aad94aeb7edef637cb76cf54f29254db91a3 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Fri, 29 Nov 2024 17:55:06 +0100 Subject: [PATCH 10/91] [blocks] return list of block hash filtered by definition hash --- backend/src/api/bitcoin/bitcoin.routes.ts | 28 +++++++++++++++++++---- backend/src/api/blocks.ts | 27 ++++++++++++++++++---- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index c95d89d0f..eb5cca6f8 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -47,13 +47,15 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) - .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/definition/list', this.getBlockDefinitionHashes) - .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/definition/current', this.getCurrentBlockDefinitionHash) .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) // Temporarily add txs/package endpoint for all backends until esplora supports it .post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage) + // Internal routes + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/list', this.getBlockDefinitionHashes) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/current', this.getCurrentBlockDefinitionHash) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/:definitionHash', this.getBlocksByDefinitionHash) ; if (config.MEMPOOL.BACKEND !== 'esplora') { @@ -660,7 +662,7 @@ class BitcoinRoutes { } } - private async getBlockDefinitionHashes(req: Request, res: Response) { + private async getBlockDefinitionHashes(req: Request, res: Response): Promise { try { const result = await blocks.$getBlockDefinitionHashes(); if (!result) { @@ -674,7 +676,7 @@ class BitcoinRoutes { } } - private async getCurrentBlockDefinitionHash(req: Request, res: Response) { + private async getCurrentBlockDefinitionHash(req: Request, res: Response): Promise { try { const currentSha = await poolsUpdater.getShaFromDb(); if (!currentSha) { @@ -688,6 +690,24 @@ class BitcoinRoutes { } } + private async getBlocksByDefinitionHash(req: Request, res: Response): Promise { + try { + if (typeof(req.params.definitionHash) !== 'string') { + res.status(400).send('Parameter "hash" must be a valid string'); + return; + } + const blocksHash = await blocks.$getBlocksByDefinitionHash(req.params.definitionHash as string); + if (!blocksHash) { + handleError(req, res, 503, `Service Temporarily Unavailable`); + return; + } + res.setHeader('content-type', 'application/json'); + res.send(blocksHash); + } catch (e) { + handleError(req, res, 500, e instanceof Error ? e.message : e); + } + } + private getBlockTipHeight(req: Request, res: Response) { try { const result = blocks.getCurrentBlockHeight(); diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 2b468a8f8..102601594 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -1463,17 +1463,34 @@ class Blocks { } } - public async $getBlockDefinitionHashes(): Promise { + public async $getBlockDefinitionHashes(): Promise { try { const [rows]: any = await database.query(`SELECT DISTINCT(definition_hash) FROM blocks`); - if (rows && rows.length) { + if (rows && Array.isArray(rows)) { return rows.map(r => r.definition_hash); + } else { + logger.debug(`Unable to retreive list of blocks.definition_hash from db (no result)`); + return null; } } catch (e) { - // we just return an empty array + logger.debug(`Unable to retreive list of blocks.definition_hash from db (exception: ${e})`); + return null; + } + } + + public async $getBlocksByDefinitionHash(definitionHash: string): Promise { + try { + const [rows]: any = await database.query(`SELECT hash FROM blocks WHERE definition_hash = ?`, [definitionHash]); + if (rows && Array.isArray(rows)) { + return rows.map(r => r.hash); + } else { + logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (no result)`); + return null; + } + } catch (e) { + logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (exception: ${e})`); + return null; } - logger.debug(`Unable to retreive list of blocks.definition_hash from db`); - return []; } } From a2bc6f5bbab04a90bfc4ce098858e1d1c73581be Mon Sep 17 00:00:00 2001 From: wiz Date: Fri, 6 Dec 2024 17:20:16 +0900 Subject: [PATCH 11/91] Trim string for Hawaii Standard Time --- frontend/src/app/app.constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/app.constants.ts b/frontend/src/app/app.constants.ts index 7cc4b4dff..0fe519a01 100644 --- a/frontend/src/app/app.constants.ts +++ b/frontend/src/app/app.constants.ts @@ -449,7 +449,7 @@ export interface Timezone { export const timezones: Timezone[] = [ { offset: '-12', name: 'Anywhere on Earth (AoE)' }, { offset: '-11', name: 'Samoa Standard Time (SST)' }, - { offset: '-10', name: 'Hawaii-Aleutian Standard Time (HST)' }, + { offset: '-10', name: 'Hawaii Standard Time (HST)' }, { offset: '-9', name: 'Alaska Standard Time (AKST)' }, { offset: '-8', name: 'Pacific Standard Time (PST)' }, { offset: '-7', name: 'Mountain Standard Time (MST)' }, From 2372d8cff368bf8d5829ab1dd37c7f0892f980bf Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 9 Dec 2024 12:01:46 +0100 Subject: [PATCH 12/91] Unify database schema for all backend types --- backend/src/api/database-migration.ts | 319 +++++++++++++++++++++++++- 1 file changed, 318 insertions(+), 1 deletion(-) diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 6cb361ffd..dc8c7291a 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository'; import { RowDataPacket } from 'mysql2'; class DatabaseMigration { - private static currentVersion = 93; + private static currentVersion = 94; private queryTimeout = 3600_000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -801,6 +801,323 @@ class DatabaseMigration { `); await this.updateToSchemaVersion(93); } + + // Unify database schema for all mempool netwoks + // versions above 94 should not use network-specific flags + if (databaseSchemaVersion < 94) { + + if (!isBitcoin) { + // Apply all the bitcoin specific migrations to non-bitcoin networks: liquid, liquidtestnet and testnet4 (!) + // Version 5 + await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"'); + + // Version 6 + await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`'); + await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL'); + await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)'); + await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""'); + await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL'); + + // Version 7 + await this.$executeQuery('DROP table IF EXISTS hashrates;'); + await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates')); + + // Version 8 + await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`'); + await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST'); + await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"'); + + // Version 9 + await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)'); + await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)'); + + // Version 10 + await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)'); + + // Version 11 + await this.$executeQuery(`ALTER TABLE blocks + ADD avg_fee INT UNSIGNED NULL, + ADD avg_fee_rate INT UNSIGNED NULL + `); + await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"'); + + // Version 12 + await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + + // Version 13 + await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + + // Version 14 + await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`'); + await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"'); + + // Version 17 + await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL'); + + // Version 18 + await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);'); + + // Version 20 + await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries')); + + // Version 22 + await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`'); + await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments')); + + // Version 24 + await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`'); + await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits')); + + // Version 25 + await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats')); + await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes')); + await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels')); + await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats')); + + // Version 26 + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"'); + + // Version 27 + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_capacity bigint(20) unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_fee_rate int(11) unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"'); + + // Version 28 + await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`); + + // Version 29 + await this.$executeQuery(this.getCreateGeoNamesTableQuery(), await this.$checkIfTableExists('geo_names')); + await this.$executeQuery('ALTER TABLE `nodes` ADD as_number int(11) unsigned NULL DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD city_id int(11) unsigned NULL DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD country_id int(11) unsigned NULL DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD accuracy_radius int(11) unsigned NULL DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL'); + + // Version 30 + await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL'); + + // Version 31 + await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE'); + await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`'); + await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices')); + + // Version 32 + await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"'); + + // Version 33 + await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL'); + + // Version 34 + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"'); + + // Version 35 + await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);'); + + // Version 36 + await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"'); + + // Version 37 + await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets')); + + // Version 38 + await this.$executeQuery(`TRUNCATE lightning_stats`); + await this.$executeQuery(`TRUNCATE node_stats`); + await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL'); + await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL'); + await this.updateToSchemaVersion(38); + + // Version 39 + await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`'); + await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)'); + + // Version 40 + await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);'); + + // Version 41 + await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1'); + + // Version 42 + await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0'); + + // Version 43 + await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records')); + + // Version 44 + await this.$executeQuery('UPDATE blocks_summaries SET template = NULL'); + + // Version 45 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"'); + + // Version 48 + await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"'); + + // Version 57 + await this.$executeQuery(`ALTER TABLE nodes MODIFY updated_at datetime NULL`); + + // Version 60 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"'); + + // Version 61 + if (! await this.$checkIfTableExists('blocks_templates')) { + await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"'); + } + await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"'); + await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)'); + await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template'); + + // Version 62 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_fees BIGINT UNSIGNED DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_weight BIGINT UNSIGNED DEFAULT NULL'); + + // Version 63 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"'); + + // Version 64 + await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL'); + + // Version 65 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"'); + + // Version 67 + await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD version INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD INDEX `version` (`version`)'); + await this.$executeQuery('ALTER TABLE `blocks_templates` ADD version INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)'); + + // Version 76 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"'); + + // Version 81 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)'); + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"'); + + // Version 83 + await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL'); + + // Version 84 + await this.$executeQuery(` + ALTER TABLE \`pools\` + ADD INDEX \`slug\` (\`slug\`), + ADD INDEX \`unique_id\` (\`unique_id\`) + `); + + // Version 85 + await this.$executeQuery(` + ALTER TABLE \`channels\` + ADD INDEX \`created\` (\`created\`), + ADD INDEX \`capacity\` (\`capacity\`), + ADD INDEX \`closing_reason\` (\`closing_reason\`), + ADD INDEX \`closing_resolved\` (\`closing_resolved\`) + `); + + // Version 86 + await this.$executeQuery(` + ALTER TABLE \`nodes\` + ADD INDEX \`status\` (\`status\`), + ADD INDEX \`channels\` (\`channels\`), + ADD INDEX \`country_id\` (\`country_id\`), + ADD INDEX \`as_number\` (\`as_number\`), + ADD INDEX \`first_seen\` (\`first_seen\`) + `); + + // Version 87 + await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)'); + await this.updateToSchemaVersion(87); + + // Version 88 + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)'); + + // Version 89 + await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)'); + + // Version 90 + await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)'); + + // Version 91 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)'); + } + + if (config.MEMPOOL.NETWORK !== 'liquid') { + // Apply all the liquid specific migrations to all other networks + // Version 68 + await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);'); + await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses')); + await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos')); + + // Version 71 + await this.$executeQuery('ALTER TABLE `federation_txos` ADD timelock INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `federation_txos` ADD expiredAt INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `federation_txos` ADD emergencyKey TINYINT NOT NULL DEFAULT 0'); + + // Version 92 + await this.$executeQuery(` + ALTER TABLE \`elements_pegs\` + ADD INDEX \`block\` (\`block\`), + ADD INDEX \`datetime\` (\`datetime\`), + ADD INDEX \`amount\` (\`amount\`), + ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`), + ADD INDEX \`bitcointxid\` (\`bitcointxid\`) + `); + + // Version 93 + await this.$executeQuery(` + ALTER TABLE \`federation_txos\` + ADD INDEX \`unspent\` (\`unspent\`), + ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`), + ADD INDEX \`blocktime\` (\`blocktime\`), + ADD INDEX \`emergencyKey\` (\`emergencyKey\`), + ADD INDEX \`expiredAt\` (\`expiredAt\`) + `); + } + + if (config.MEMPOOL.NETWORK !== 'mainnet') { + // Apply all the mainnet specific migrations to all other networks + // Version 69 + await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations')); + + // Version 70 + await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;'); + + // Version 77 + await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL'); + } + await this.updateToSchemaVersion(94); + } } /** From ad360db71f720d158c0ca30d86837e5b244cee84 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Tue, 10 Dec 2024 15:16:19 +0100 Subject: [PATCH 13/91] [accelerator] add sca for googlepay payments --- .../accelerate-checkout.component.ts | 34 +++++++++++++++++++ .../src/app/services/services-api.service.ts | 4 +-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 1a5ace34f..d6ac7f54f 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -612,10 +612,18 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.processing = false; return; } + const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2)); + if (!verificationToken) { + console.error(`SCA verification failed`); + this.accelerateError = 'SCA Verification Failed. Payment Declined.'; + this.processing = false; + return; + } const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); this.servicesApiService.accelerateWithGooglePay$( this.tx.txid, tokenResult.token, + verificationToken, cardTag, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, costUSD @@ -743,6 +751,32 @@ export class AccelerateCheckout implements OnInit, OnDestroy { ); } + /** + * Required in SCA Mandated Regions: Learn more at https://developer.squareup.com/docs/sca-overview + */ + async $verifyBuyer(payments, token, details, amount) { + const verificationDetails = { + amount: amount, + currencyCode: 'USD', + intent: 'CHARGE', + billingContact: { + givenName: details.card?.billing?.givenName, + familyName: details.card?.billing?.familyName, + phone: details.card?.billing?.phone, + addressLines: details.card?.billing?.addressLines, + city: details.card?.billing?.city, + state: details.card?.billing?.state, + countryCode: details.card?.billing?.countryCode, + }, + }; + + const verificationResults = await payments.verifyBuyer( + token, + verificationDetails, + ); + return verificationResults.token; + } + /** * BTCPay */ diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 2ecfe06ff..bec9d88a1 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -143,8 +143,8 @@ export class ServicesApiServices { return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD }); } - accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, userApprovedUSD: number) { - return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD }); + accelerateWithGooglePay$(txInput: string, token: string, verificationToken: string, cardTag: string, referenceId: string, userApprovedUSD: number) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD }); } getAccelerations$(): Observable { From 58b4c0792437f9f3a2cd407f6da48d751b0fa391 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 10 Dec 2024 22:19:57 +0000 Subject: [PATCH 14/91] fix liquid monitoring url routes --- frontend/src/app/liquid/liquid-master-page.module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/liquid/liquid-master-page.module.ts b/frontend/src/app/liquid/liquid-master-page.module.ts index 17c2c8c41..d90643b4d 100644 --- a/frontend/src/app/liquid/liquid-master-page.module.ts +++ b/frontend/src/app/liquid/liquid-master-page.module.ts @@ -142,12 +142,12 @@ const routes: Routes = [ if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) { routes[0].children.push({ - path: 'nodes', + path: 'monitoring', data: { networks: ['bitcoin', 'liquid'] }, component: ServerHealthComponent }); routes[0].children.push({ - path: 'network', + path: 'nodes', data: { networks: ['bitcoin', 'liquid'] }, component: ServerStatusComponent }); From 392f6a01c442f361e5bc6a129b60d32c6dfc342a Mon Sep 17 00:00:00 2001 From: natsoni Date: Thu, 12 Dec 2024 19:22:03 +0100 Subject: [PATCH 15/91] Fix package broadcast table css --- .../push-transaction.component.scss | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/frontend/src/app/components/push-transaction/push-transaction.component.scss b/frontend/src/app/components/push-transaction/push-transaction.component.scss index e69de29bb..ffdd5811b 100644 --- a/frontend/src/app/components/push-transaction/push-transaction.component.scss +++ b/frontend/src/app/components/push-transaction/push-transaction.component.scss @@ -0,0 +1,34 @@ +.accept-results { + td, th { + &.allowed { + width: 10%; + text-align: center; + } + &.txid { + width: 50%; + } + &.rate { + width: 20%; + text-align: right; + white-space: wrap; + } + &.reason { + width: 20%; + text-align: right; + white-space: wrap; + } + } + + @media (max-width: 950px) { + table-layout: auto; + + td, th { + &.allowed { + width: 100px; + } + &.txid { + max-width: 200px; + } + } + } +} \ No newline at end of file From 4fe246ecf1820e6cd37d04e250f8a7bc48d16033 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 13 Dec 2024 16:16:18 +0000 Subject: [PATCH 16/91] fix monitoring git hash urls --- backend/src/api/bitcoin/esplora-api.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 2aea8e73c..8035d92c0 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -20,6 +20,7 @@ interface FailoverHost { preferred?: boolean, checked: boolean, lastChecked?: number, + publicDomain: string, hashes: { frontend?: string, backend?: string, @@ -58,6 +59,7 @@ class FailoverRouter { rtts: [], rtt: Infinity, failures: 0, + publicDomain: 'https://' + this.extractPublicDomain(domain), hashes: { lastUpdated: 0, }, @@ -71,6 +73,7 @@ class FailoverRouter { socket: !!config.ESPLORA.UNIX_SOCKET_PATH, preferred: true, checked: false, + publicDomain: `http://${this.localHostname}`, hashes: { lastUpdated: 0, }, @@ -242,7 +245,7 @@ class FailoverRouter { // methods for retrieving git hashes by host private async $updateFrontendGitHash(host: FailoverHost): Promise { try { - const url = host.socket ? `http://${this.localHostname}/resources/config.js` : `${host.host.slice(0, -4)}/resources/config.js`; + const url = `${host.publicDomain}/resources/config.js`; const response = await this.pollConnection.get(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT }); const match = response.data.match(/GIT_COMMIT_HASH\s*=\s*['"](.*?)['"]/); if (match && match[1]?.length) { @@ -255,7 +258,7 @@ class FailoverRouter { private async $updateBackendGitHash(host: FailoverHost): Promise { try { - const url = host.socket ? `http://${this.localHostname}/api/v1/backend-info` : `${host.host}/v1/backend-info`; + const url = `${host.publicDomain}/api/v1/backend-info`; const response = await this.pollConnection.get(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT }); if (response.data?.gitCommit) { host.hashes.backend = response.data.gitCommit; @@ -265,6 +268,21 @@ class FailoverRouter { } } + // returns the public mempool domain corresponding to an esplora server url + // (a bit of a hack to avoid manually specifying frontend & backend URLs for each esplora server) + private extractPublicDomain(url: string): string { + // force the url to start with a valid protocol + const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`; + // parse as URL and extract the hostname + try { + const parsed = new URL(urlWithProtocol); + return parsed.hostname; + } catch (e) { + // fallback to the original url + return url; + } + } + private async $query(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise { let axiosConfig; let url; From 8b73bdfba9680f9aac9371f8831d73c3307dda33 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 13 Dec 2024 22:09:14 +0000 Subject: [PATCH 17/91] fix unfurler meta titles --- unfurler/src/index.ts | 6 ++++-- unfurler/src/routes.ts | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/unfurler/src/index.ts b/unfurler/src/index.ts index 755232b50..661394cb7 100644 --- a/unfurler/src/index.ts +++ b/unfurler/src/index.ts @@ -30,6 +30,7 @@ class Server { secureHost = true; secureMempoolHost = true; canonicalHost: string; + networkName: string; seoQueueLength: number = 0; unfurlQueueLength: number = 0; @@ -41,6 +42,7 @@ class Server { this.secureHost = config.SERVER.HOST.startsWith('https'); this.secureMempoolHost = config.MEMPOOL.HTTP_HOST.startsWith('https'); this.network = config.MEMPOOL.NETWORK || 'bitcoin'; + this.networkName = networks[this.network].networkName || capitalize(this.network); let canonical; switch(config.MEMPOOL.NETWORK) { @@ -339,7 +341,7 @@ class Server { if (matchedRoute.render) { ogImageUrl = `${config.SERVER.HOST}/render/${lang || 'en'}/preview${path}`; - ogTitle = `${this.network ? capitalize(this.network) + ' ' : ''}${matchedRoute.networkMode !== 'mainnet' ? capitalize(matchedRoute.networkMode) + ' ' : ''}${matchedRoute.title}`; + ogTitle = `${this.networkName} ${matchedRoute.networkMode !== 'mainnet' ? capitalize(matchedRoute.networkMode) + ' ' : ''}${matchedRoute.title}`; } else { ogTitle = networks[this.network].title; } @@ -394,7 +396,7 @@ class Server { if (matchedRoute.render) { ogImageUrl = `${config.SERVER.HOST}/render/${lang || 'en'}/preview${path}`; - ogTitle = `${this.network ? capitalize(this.network) + ' ' : ''}${matchedRoute.networkMode !== 'mainnet' ? capitalize(matchedRoute.networkMode) + ' ' : ''}${matchedRoute.title}`; + ogTitle = `${this.networkName} ${matchedRoute.networkMode !== 'mainnet' ? capitalize(matchedRoute.networkMode) + ' ' : ''}${matchedRoute.title}`; } if (matchedRoute.sip) { diff --git a/unfurler/src/routes.ts b/unfurler/src/routes.ts index c6be7e129..dcea29cde 100644 --- a/unfurler/src/routes.ts +++ b/unfurler/src/routes.ts @@ -270,6 +270,7 @@ export const networks = { routes: {} // no routes supported }, onbtc: { + networkName: 'ONBTC', title: 'National Bitcoin Office of El Salvador', description: 'The National Bitcoin Office (ONBTC) of El Salvador under President @nayibbukele', fallbackImg: '/resources/onbtc/onbtc-preview.jpg', @@ -290,6 +291,7 @@ export const networks = { } }, bitb: { + networkName: 'BITB', title: 'BITB | Bitwise Bitcoin ETF', description: 'BITB provides low-cost access to bitcoin through a professionally managed fund', fallbackImg: '/resources/bitb/bitb-preview.jpg', @@ -311,6 +313,7 @@ export const networks = { } }, meta: { + networkName: 'Metaplanet', title: 'Metaplanet Inc.', description: 'Secure the Future with Bitcoin', fallbackImg: '/resources/meta/meta-preview.png', From 47044db043120ee389611fdd392e37ef396aa6b3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 19 Dec 2024 12:50:17 +0000 Subject: [PATCH 18/91] standardize API error strings & validation --- .../src/api/bitcoin/bitcoin-core.routes.ts | 90 ++++---- backend/src/api/bitcoin/bitcoin.routes.ts | 217 +++++++++++++----- backend/src/api/explorer/channels.routes.ts | 19 +- backend/src/api/explorer/general.routes.ts | 6 +- backend/src/api/explorer/nodes.routes.ts | 28 +-- backend/src/api/liquid/liquid.routes.ts | 30 +-- backend/src/api/mining/mining-routes.ts | 52 ++--- backend/src/api/services/services-routes.ts | 3 +- .../src/api/statistics/statistics.routes.ts | 4 +- backend/src/index.ts | 4 +- 10 files changed, 284 insertions(+), 169 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-core.routes.ts b/backend/src/api/bitcoin/bitcoin-core.routes.ts index 2c3dd08f6..7e1dcea74 100644 --- a/backend/src/api/bitcoin/bitcoin-core.routes.ts +++ b/backend/src/api/bitcoin/bitcoin-core.routes.ts @@ -3,6 +3,10 @@ import logger from '../../logger'; import bitcoinClient from './bitcoin-client'; import config from '../../config'; +const BLOCKHASH_REGEX = /^[a-f0-9]{64}$/i; +const TXID_REGEX = /^[a-f0-9]{64}$/i; +const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i; + /** * Define a set of routes used by the accelerator server * Those routes are not designed to be public @@ -10,7 +14,7 @@ import config from '../../config'; class BitcoinBackendRoutes { private static tag = 'BitcoinBackendRoutes'; - public initRoutes(app: Application) { + public initRoutes(app: Application): void { app .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry) .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction) @@ -26,10 +30,10 @@ class BitcoinBackendRoutes { /** * Disable caching for bitcoin core routes - * - * @param req - * @param res - * @param next + * + * @param req + * @param res + * @param next */ private disableCache(req: Request, res: Response, next: NextFunction): void { res.setHeader('Pragma', 'no-cache'); @@ -40,16 +44,16 @@ class BitcoinBackendRoutes { /** * Exeption handler to return proper details to the accelerator server - * - * @param e - * @param fnName - * @param res + * + * @param e + * @param fnName + * @param res */ private static handleException(e: any, fnName: string, res: Response): void { if (typeof(e.code) === 'number') { - res.status(400).send(JSON.stringify(e, ['code', 'message'])); - } else { - const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`; + res.status(400).send(JSON.stringify(e, ['code'])); + } else { + const err = `unknown exception in ${fnName}`; logger.err(err, BitcoinBackendRoutes.tag); res.status(500).send(err); } @@ -58,13 +62,13 @@ class BitcoinBackendRoutes { private async $getMempoolEntry(req: Request, res: Response): Promise { const txid = req.query.txid; try { - if (typeof(txid) !== 'string' || txid.length !== 64) { - res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); + if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { + res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); return; } const mempoolEntry = await bitcoinClient.getMempoolEntry(txid); if (!mempoolEntry) { - res.status(404).send(`no mempool entry found for txid ${txid}`); + res.status(404).send(); return; } res.status(200).send(mempoolEntry); @@ -76,13 +80,13 @@ class BitcoinBackendRoutes { private async $decodeRawTransaction(req: Request, res: Response): Promise { const rawTx = req.body.rawTx; try { - if (typeof(rawTx) !== 'string') { - res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`); + if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) { + res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`); return; } const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx); if (!decodedTx) { - res.status(400).send(`unable to decode rawTx ${rawTx}`); + res.status(400).send(`unable to decode rawTx`); return; } res.status(200).send(decodedTx); @@ -95,23 +99,23 @@ class BitcoinBackendRoutes { const txid = req.query.txid; const verbose = req.query.verbose; try { - if (typeof(txid) !== 'string' || txid.length !== 64) { - res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); + if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { + res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); return; } if (typeof(verbose) !== 'string') { - res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`); + res.status(400).send(`invalid param verbose. must be a string representing an integer`); return; } const verboseNumber = parseInt(verbose, 10); if (typeof(verboseNumber) !== 'number') { - res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`); + res.status(400).send(`invalid param verbose. must be a valid integer`); return; } const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber); if (!decodedTx) { - res.status(400).send(`unable to get raw transaction for txid ${txid}`); + res.status(400).send(`unable to get raw transaction`); return; } res.status(200).send(decodedTx); @@ -123,13 +127,13 @@ class BitcoinBackendRoutes { private async $sendRawTransaction(req: Request, res: Response): Promise { const rawTx = req.body.rawTx; try { - if (typeof(rawTx) !== 'string') { - res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`); + if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) { + res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`); return; } const txHex = await bitcoinClient.sendRawTransaction(rawTx); if (!txHex) { - res.status(400).send(`unable to send rawTx ${rawTx}`); + res.status(400).send(`unable to send rawTx`); return; } res.status(200).send(txHex); @@ -141,13 +145,13 @@ class BitcoinBackendRoutes { private async $testMempoolAccept(req: Request, res: Response): Promise { const rawTxs = req.body.rawTxs; try { - if (typeof(rawTxs) !== 'object') { - res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`); + if (typeof(rawTxs) !== 'object' || !Array.isArray(rawTxs) || rawTxs.some((tx) => typeof(tx) !== 'string' || !RAW_TX_REGEX.test(tx))) { + res.status(400).send(`invalid param rawTxs. must be an array of strings of hexadecimal characters`); return; } const txHex = await bitcoinClient.testMempoolAccept(rawTxs); if (typeof(txHex) !== 'object' || txHex.length === 0) { - res.status(400).send(`testmempoolaccept failed for raw txs ${JSON.stringify(rawTxs)}, got an empty result`); + res.status(400).send(`testmempoolaccept failed for raw txs, got an empty result`); return; } res.status(200).send(txHex); @@ -160,18 +164,18 @@ class BitcoinBackendRoutes { const txid = req.query.txid; const verbose = req.query.verbose; try { - if (typeof(txid) !== 'string' || txid.length !== 64) { - res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); + if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { + res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); return; } if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) { - res.status(400).send(`invalid param verbose ${verbose}. must be a string ('true' | 'false')`); + res.status(400).send(`invalid param verbose. must be a string ('true' | 'false')`); return; } - + const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false); if (!ancestors) { - res.status(400).send(`unable to get mempool ancestors for txid ${txid}`); + res.status(400).send(`unable to get mempool ancestors`); return; } res.status(200).send(ancestors); @@ -184,23 +188,23 @@ class BitcoinBackendRoutes { const blockHash = req.query.hash; const verbosity = req.query.verbosity; try { - if (typeof(blockHash) !== 'string' || blockHash.length !== 64) { - res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`); + if (typeof(blockHash) !== 'string' || blockHash.length !== 64 || !BLOCKHASH_REGEX.test(blockHash)) { + res.status(400).send(`invalid param blockHash. must be 64 hexadecimal characters`); return; } if (typeof(verbosity) !== 'string') { - res.status(400).send(`invalid param verbosity ${verbosity}. must be a string representing an integer`); + res.status(400).send(`invalid param verbosity. must be a string representing an integer`); return; } const verbosityNumber = parseInt(verbosity, 10); if (typeof(verbosityNumber) !== 'number') { - res.status(400).send(`invalid param verbosity ${verbosity}. must be a valid integer`); + res.status(400).send(`invalid param verbosity. must be a valid integer`); return; } const block = await bitcoinClient.getBlock(blockHash, verbosityNumber); if (!block) { - res.status(400).send(`unable to get block for block hash ${blockHash}`); + res.status(400).send(`unable to get block`); return; } res.status(200).send(block); @@ -213,18 +217,18 @@ class BitcoinBackendRoutes { const blockHeight = req.query.height; try { if (typeof(blockHeight) !== 'string') { - res.status(400).send(`invalid param blockHeight ${blockHeight}, must be a string representing an integer`); + res.status(400).send(`invalid param blockHeight, must be a string representing an integer`); return; } const blockHeightNumber = parseInt(blockHeight, 10); if (typeof(blockHeightNumber) !== 'number') { - res.status(400).send(`invalid param blockHeight ${blockHeight}. must be a valid integer`); + res.status(400).send(`invalid param blockHeight. must be a valid integer`); return; } const block = await bitcoinClient.getBlockHash(blockHeightNumber); if (!block) { - res.status(400).send(`unable to get block hash for block height ${blockHeightNumber}`); + res.status(400).send(`unable to get block hash`); return; } res.status(200).send(block); @@ -247,4 +251,4 @@ class BitcoinBackendRoutes { } } -export default new BitcoinBackendRoutes \ No newline at end of file +export default new BitcoinBackendRoutes; \ No newline at end of file diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index d2d298e09..339c4cff9 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -22,6 +22,11 @@ import rbfCache from '../rbf-cache'; import { calculateMempoolTxCpfp } from '../cpfp'; import { handleError } from '../../utils/api'; +const TXID_REGEX = /^[a-f0-9]{64}$/i; +const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i; +const ADDRESS_REGEX = /^[a-z0-9]{2,120}$/i; +const SCRIPT_HASH_REGEX = /^([a-f0-9]{2})+$/i; + class BitcoinRoutes { public initRoutes(app: Application) { app @@ -90,7 +95,7 @@ class BitcoinRoutes { res.set('Content-Type', 'application/json'); res.send(result); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get init data'); } } @@ -109,7 +114,7 @@ class BitcoinRoutes { const result = mempoolBlocks.getMempoolBlocks(); res.json(result); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get mempool blocks'); } } @@ -121,7 +126,10 @@ class BitcoinRoutes { const txIds: string[] = []; for (const _txId in req.query.txId) { if (typeof req.query.txId[_txId] === 'string') { - txIds.push(req.query.txId[_txId].toString()); + const txid = req.query.txId[_txId].toString(); + if (TXID_REGEX.test(txid)) { + txIds.push(txid); + } } } @@ -140,18 +148,22 @@ class BitcoinRoutes { handleError(req, res, 400, 'Too many txids requested'); return; } + if (txids.some((txid) => !TXID_REGEX.test(txid))) { + handleError(req, res, 400, 'Invalid txids format'); + return; + } try { const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); res.json(batchedOutspends); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get batched outspends'); } } private async $getCpfpInfo(req: Request, res: Response) { - if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { - handleError(req, res, 501, `Invalid transaction ID.`); + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); return; } @@ -184,7 +196,7 @@ class BitcoinRoutes { try { cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); } catch (e) { - handleError(req, res, 500, 'failed to get CPFP info'); + handleError(req, res, 500, 'Failed to get CPFP info'); return; } } @@ -205,6 +217,10 @@ class BitcoinRoutes { } private async getTransaction(req: Request, res: Response) { + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true); res.json(transaction); @@ -212,12 +228,17 @@ class BitcoinRoutes { let statusCode = 500; if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; + handleError(req, res, statusCode, 'No such mempool or blockchain transaction'); } - handleError(req, res, statusCode, e instanceof Error ? e.message : e); + handleError(req, res, statusCode, 'Failed to get transaction'); } } private async getRawTransaction(req: Request, res: Response) { + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true); res.setHeader('content-type', 'text/plain'); @@ -226,8 +247,9 @@ class BitcoinRoutes { let statusCode = 500; if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; + handleError(req, res, statusCode, 'No such mempool or blockchain transaction'); } - handleError(req, res, statusCode, e instanceof Error ? e.message : e); + handleError(req, res, statusCode, 'Failed to get raw transaction'); } } @@ -292,14 +314,18 @@ class BitcoinRoutes { } } catch (e: any) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { - handleError(req, res, 404, e.message); + handleError(req, res, 404, notFoundError); } else { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to process PSBT'); } } } private async getTransactionStatus(req: Request, res: Response) { + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true); res.json(transaction.status); @@ -307,36 +333,53 @@ class BitcoinRoutes { let statusCode = 500; if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; + handleError(req, res, statusCode, 'No such mempool or blockchain transaction'); } - handleError(req, res, statusCode, e instanceof Error ? e.message : e); + handleError(req, res, statusCode, 'Failed to get transaction status'); } } private async getStrippedBlockTransactions(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(transactions); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block summary'); } } private async getStrippedBlockTransaction(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } + if (!TXID_REGEX.test(req.params.txid)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid); if (!transaction) { - handleError(req, res, 404, `transaction not found in summary`); + handleError(req, res, 404, `Transaction not found in summary`); return; } res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(transaction); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get transaction from summary'); } } private async getBlock(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { const block = await blocks.$getBlock(req.params.hash); @@ -348,53 +391,69 @@ class BitcoinRoutes { } else if (blockAge > 30 * day) { cacheDuration = 10 * day; } else { - cacheDuration = 600 + cacheDuration = 600; } res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.json(block); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block'); } } private async getBlockHeader(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash); res.setHeader('content-type', 'text/plain'); res.send(blockHeader); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block header'); } } private async getBlockAuditSummary(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash); if (auditSummary) { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(auditSummary); } else { - handleError(req, res, 404, `audit not available`); + handleError(req, res, 404, `Audit not available`); return; } } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block audit summary'); } } private async $getBlockTxAuditSummary(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } + if (!TXID_REGEX.test(req.params.txid)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid); if (auditSummary) { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(auditSummary); } else { - handleError(req, res, 404, `transaction audit not available`); + handleError(req, res, 404, `Transaction audit not available`); return; } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get transaction audit summary'); } } @@ -408,7 +467,7 @@ class BitcoinRoutes { return await this.getLegacyBlocks(req, res); } } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get blocks'); } } @@ -450,7 +509,7 @@ class BitcoinRoutes { res.json(await blocks.$getBlocksBetweenHeight(from, to)); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get blocks'); } } @@ -485,11 +544,15 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(returnBlocks); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get blocks'); } } private async getBlockTransactions(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); @@ -510,7 +573,7 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block transactions'); } } @@ -519,7 +582,7 @@ class BitcoinRoutes { const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10)); res.send(blockHash); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block at height'); } } @@ -528,16 +591,20 @@ class BitcoinRoutes { handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } + if (!ADDRESS_REGEX.test(req.params.address)) { + handleError(req, res, 501, `Invalid address`); + return; + } try { const addressData = await bitcoinApi.$getAddress(req.params.address); res.json(addressData); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - handleError(req, res, 413, e instanceof Error ? e.message : e); + handleError(req, res, 413, e.message); return; } - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get address'); } } @@ -546,6 +613,10 @@ class BitcoinRoutes { handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } + if (!ADDRESS_REGEX.test(req.params.address)) { + handleError(req, res, 501, `Invalid address`); + return; + } try { let lastTxId: string = ''; @@ -556,10 +627,10 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - handleError(req, res, 413, e instanceof Error ? e.message : e); + handleError(req, res, 413, e.message); return; } - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get address transactions'); } } @@ -575,6 +646,10 @@ class BitcoinRoutes { handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } + if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) { + handleError(req, res, 501, `Invalid scripthash`); + return; + } try { // electrum expects scripthashes in little-endian @@ -583,10 +658,10 @@ class BitcoinRoutes { res.json(addressData); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - handleError(req, res, 413, e instanceof Error ? e.message : e); + handleError(req, res, 413, e.message); return; } - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get script hash'); } } @@ -595,6 +670,10 @@ class BitcoinRoutes { handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } + if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) { + handleError(req, res, 501, `Invalid scripthash`); + return; + } try { // electrum expects scripthashes in little-endian @@ -607,10 +686,10 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - handleError(req, res, 413, e instanceof Error ? e.message : e); + handleError(req, res, 413, e.message); return; } - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get script hash transactions'); } } @@ -623,10 +702,10 @@ class BitcoinRoutes { private async getAddressPrefix(req: Request, res: Response) { try { - const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); - res.send(blockHash); + const addressPrefix = await bitcoinApi.$getAddressPrefix(req.params.prefix); + res.send(addressPrefix); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get address prefix'); } } @@ -667,7 +746,7 @@ class BitcoinRoutes { res.setHeader('content-type', 'text/plain'); res.send(result.toString()); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get height at tip'); } } @@ -677,39 +756,55 @@ class BitcoinRoutes { res.setHeader('content-type', 'text/plain'); res.send(result); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get hash at tip'); } } private async getRawBlock(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { const result = await bitcoinApi.$getRawBlock(req.params.hash); res.setHeader('content-type', 'application/octet-stream'); res.send(result); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get raw block'); } } private async getTxIdsForBlock(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); res.json(result); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get txids for block'); } } private async validateAddress(req: Request, res: Response) { + if (!ADDRESS_REGEX.test(req.params.address)) { + handleError(req, res, 501, `Invalid address`); + return; + } try { const result = await bitcoinClient.validateAddress(req.params.address); res.json(result); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to validate address'); } } private async getRbfHistory(req: Request, res: Response) { + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const replacements = rbfCache.getRbfTree(req.params.txId) || null; const replaces = rbfCache.getReplaces(req.params.txId) || null; @@ -718,7 +813,7 @@ class BitcoinRoutes { replaces }); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get rbf history'); } } @@ -727,7 +822,7 @@ class BitcoinRoutes { const result = rbfCache.getRbfTrees(false); res.json(result); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get rbf trees'); } } @@ -736,11 +831,15 @@ class BitcoinRoutes { const result = rbfCache.getRbfTrees(true); res.json(result); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get full rbf replacements'); } } private async getCachedTx(req: Request, res: Response) { + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const result = rbfCache.getTx(req.params.txId); if (result) { @@ -749,16 +848,20 @@ class BitcoinRoutes { res.status(204).send(); } } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get cached tx'); } } private async getTransactionOutspends(req: Request, res: Response) { + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const result = await bitcoinApi.$getOutspends(req.params.txId); res.json(result); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get transaction outspends'); } } @@ -771,7 +874,7 @@ class BitcoinRoutes { handleError(req, res, 503, `Service Temporarily Unavailable`); } } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get difficulty change'); } } @@ -782,8 +885,8 @@ class BitcoinRoutes { const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); res.send(txIdResult); } catch (e: any) { - handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) - : (e.message || 'Error')); + handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code }) + : 'Failed to send raw transaction'); } } @@ -794,8 +897,8 @@ class BitcoinRoutes { const txIdResult = await bitcoinClient.sendRawTransaction(txHex); res.send(txIdResult); } catch (e: any) { - handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) - : (e.message || 'Error')); + handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code }) + : 'Failed to send raw transaction'); } } @@ -806,8 +909,8 @@ class BitcoinRoutes { const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); res.send(result); } catch (e: any) { - handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) - : (e.message || 'Error')); + handleError(req, res, 400, (e.message && e.code) ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code }) + : 'Failed to test transactions'); } } @@ -819,8 +922,8 @@ class BitcoinRoutes { const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined); res.send(result); } catch (e: any) { - handleError(req, res, 400, e.message && e.code ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) - : (e.message || 'Error')); + handleError(req, res, 400, (e.message && e.code) ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code }) + : 'Failed to submit package'); } } diff --git a/backend/src/api/explorer/channels.routes.ts b/backend/src/api/explorer/channels.routes.ts index 8b4c3e8c8..031aeea17 100644 --- a/backend/src/api/explorer/channels.routes.ts +++ b/backend/src/api/explorer/channels.routes.ts @@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express'; import channelsApi from './channels.api'; import { handleError } from '../../utils/api'; +const TXID_REGEX = /^[a-f0-9]{64}$/i; + class ChannelsRoutes { constructor() { } @@ -23,7 +25,7 @@ class ChannelsRoutes { const channels = await channelsApi.$searchChannelsById(req.params.search); res.json(channels); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to search channels by id'); } } @@ -39,7 +41,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channel); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get channel'); } } @@ -70,7 +72,7 @@ class ChannelsRoutes { res.header('X-Total-Count', channelsCount.toString()); res.json(channels); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get channels for node'); } } @@ -83,7 +85,10 @@ class ChannelsRoutes { const txIds: string[] = []; for (const _txId in req.query.txId) { if (typeof req.query.txId[_txId] === 'string') { - txIds.push(req.query.txId[_txId].toString()); + const txid = req.query.txId[_txId].toString(); + if (TXID_REGEX.test(txid)) { + txIds.push(txid); + } } } const channels = await channelsApi.$getChannelsByTransactionId(txIds); @@ -108,7 +113,7 @@ class ChannelsRoutes { res.json(result); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get channels by transaction ids'); } } @@ -120,7 +125,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channels); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get penalty closed channels'); } } @@ -133,7 +138,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channels); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get channel geodata'); } } diff --git a/backend/src/api/explorer/general.routes.ts b/backend/src/api/explorer/general.routes.ts index b4d0c635d..f974c9810 100644 --- a/backend/src/api/explorer/general.routes.ts +++ b/backend/src/api/explorer/general.routes.ts @@ -29,7 +29,7 @@ class GeneralLightningRoutes { channels: channels, }); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to search for nodes and channels'); } } @@ -43,7 +43,7 @@ class GeneralLightningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(statistics); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get lightning statistics'); } } @@ -52,7 +52,7 @@ class GeneralLightningRoutes { const statistics = await statisticsApi.$getLatestStatistics(); res.json(statistics); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get lightning statistics'); } } } diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index 9ca2fd1c3..811292b4b 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -32,7 +32,7 @@ class NodesRoutes { const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search); res.json(nodes); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to search for node'); } } @@ -188,7 +188,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(nodes); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get node group'); } } @@ -204,7 +204,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(node); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get node'); } } @@ -216,7 +216,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(statistics); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical node stats'); } } @@ -232,7 +232,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(node); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get fee histogram'); } } @@ -248,7 +248,7 @@ class NodesRoutes { topByChannels: topChannelsNodes, }); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get nodes ranking'); } } @@ -260,7 +260,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get top nodes by capacity'); } } @@ -272,7 +272,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get top nodes by channels'); } } @@ -284,7 +284,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get oldest nodes'); } } @@ -296,7 +296,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(nodesPerAs); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get ISP ranking'); } } @@ -308,7 +308,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(worldNodes); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get world nodes'); } } @@ -336,7 +336,7 @@ class NodesRoutes { nodes: nodes, }); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get nodes per country'); } } @@ -363,7 +363,7 @@ class NodesRoutes { nodes: nodes, }); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get nodes per ISP'); } } @@ -375,7 +375,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(nodesPerAs); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get nodes per country'); } } } diff --git a/backend/src/api/liquid/liquid.routes.ts b/backend/src/api/liquid/liquid.routes.ts index 9dafd0def..388038f7f 100644 --- a/backend/src/api/liquid/liquid.routes.ts +++ b/backend/src/api/liquid/liquid.routes.ts @@ -83,7 +83,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.json(pegs); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pegs by month'); } } @@ -95,7 +95,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.json(reserves); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get reserves by month'); } } @@ -107,7 +107,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(currentSupply); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pegs'); } } @@ -119,7 +119,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(currentReserves); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get reserves'); } } @@ -131,7 +131,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(auditStatus); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get federation audit status'); } } @@ -143,7 +143,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationAddresses); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get federation addresses'); } } @@ -155,7 +155,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationAddresses); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get federation addresses'); } } @@ -167,7 +167,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationUtxos); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get federation utxos'); } } @@ -179,7 +179,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(expiredUtxos); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get expired utxos'); } } @@ -191,7 +191,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationUtxos); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get federation utxos number'); } } @@ -203,7 +203,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(emergencySpentUtxos); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get emergency spent utxos'); } } @@ -215,7 +215,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(emergencySpentUtxos); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get emergency spent utxos stats'); } } @@ -227,7 +227,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(recentPegs); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pegs list'); } } @@ -239,7 +239,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(pegsVolume); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pegs volume daily'); } } @@ -251,7 +251,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(pegsCount); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pegs count'); } } diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index 9af43c087..ede047eed 100644 --- a/backend/src/api/mining/mining-routes.ts +++ b/backend/src/api/mining/mining-routes.ts @@ -72,7 +72,7 @@ class MiningRoutes { } res.status(200).send(response); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical prices'); } } @@ -87,7 +87,7 @@ class MiningRoutes { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { handleError(req, res, 404, e.message); } else { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pool'); } } } @@ -106,7 +106,7 @@ class MiningRoutes { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { handleError(req, res, 404, e.message); } else { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get blocks for pool'); } } } @@ -130,7 +130,7 @@ class MiningRoutes { res.json(pools); } } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pools'); } } @@ -144,7 +144,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(stats); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pools'); } } @@ -158,7 +158,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(hashrates); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pools historical hashrate'); } } @@ -175,7 +175,7 @@ class MiningRoutes { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { handleError(req, res, 404, e.message); } else { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pool historical hashrate'); } } } @@ -204,7 +204,7 @@ class MiningRoutes { currentDifficulty: currentDifficulty, }); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical hashrate'); } } @@ -218,7 +218,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFees); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical block fees'); } } @@ -236,7 +236,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFees); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical block fees'); } } @@ -250,7 +250,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockRewards); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical block rewards'); } } @@ -264,7 +264,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFeeRates); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical block fee rates'); } } @@ -282,7 +282,7 @@ class MiningRoutes { weights: blockWeights }); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical block size and weight'); } } @@ -294,7 +294,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical difficulty adjustments'); } } @@ -304,7 +304,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(response); } catch (e) { - res.status(500).end(); + handleError(req, res, 500, 'Failed to get reward stats'); } } @@ -318,7 +318,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate])); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical blocks health'); } } @@ -336,7 +336,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.json(audit); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block audit'); } } @@ -359,7 +359,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(result); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get height from timestamp'); } } @@ -372,7 +372,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15)); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block audit scores'); } } @@ -385,7 +385,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.json(audit || 'null'); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block audit score'); } } @@ -400,7 +400,7 @@ class MiningRoutes { } res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get accelerations by pool'); } } @@ -416,7 +416,7 @@ class MiningRoutes { const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get accelerations by height'); } } @@ -431,7 +431,7 @@ class MiningRoutes { } res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get recent accelerations'); } } @@ -446,7 +446,7 @@ class MiningRoutes { } res.status(200).send(await AccelerationRepository.$getAccelerationTotals(req.query.pool, req.query.interval)); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get acceleration totals'); } } @@ -461,7 +461,7 @@ class MiningRoutes { } res.status(200).send(Object.values(accelerationApi.getAccelerations() || {})); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get active accelerations'); } } @@ -473,7 +473,7 @@ class MiningRoutes { accelerationApi.accelerationRequested(req.params.txid); res.status(200).send(); } catch (e) { - handleError(req, res, 500, e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to request acceleration'); } } } diff --git a/backend/src/api/services/services-routes.ts b/backend/src/api/services/services-routes.ts index cff163174..520496249 100644 --- a/backend/src/api/services/services-routes.ts +++ b/backend/src/api/services/services-routes.ts @@ -1,6 +1,7 @@ import { Application, Request, Response } from 'express'; import config from '../../config'; import WalletApi from './wallets'; +import { handleError } from '../../utils/api'; class ServicesRoutes { public initRoutes(app: Application): void { @@ -18,7 +19,7 @@ class ServicesRoutes { const wallet = await WalletApi.getWallet(walletId); res.status(200).send(wallet); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get wallet'); } } } diff --git a/backend/src/api/statistics/statistics.routes.ts b/backend/src/api/statistics/statistics.routes.ts index 31db5198c..ec05bf032 100644 --- a/backend/src/api/statistics/statistics.routes.ts +++ b/backend/src/api/statistics/statistics.routes.ts @@ -1,7 +1,7 @@ import { Application, Request, Response } from 'express'; import config from '../../config'; import statisticsApi from './statistics-api'; - +import { handleError } from '../../utils/api'; class StatisticsRoutes { public initRoutes(app: Application) { app @@ -65,7 +65,7 @@ class StatisticsRoutes { } res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get statistics'); } } } diff --git a/backend/src/index.ts b/backend/src/index.ts index d939b7423..c179b66bc 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -324,7 +324,9 @@ class Server { setUpHttpApiRoutes(): void { bitcoinRoutes.initRoutes(this.app); - bitcoinCoreRoutes.initRoutes(this.app); + if (config.MEMPOOL.OFFICIAL) { + bitcoinCoreRoutes.initRoutes(this.app); + } pricesRoutes.initRoutes(this.app); if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) { statisticsRoutes.initRoutes(this.app); From 0a933d022cbe3929bd75ff9591120859f4248d3a Mon Sep 17 00:00:00 2001 From: natsoni Date: Thu, 19 Dec 2024 14:47:42 +0100 Subject: [PATCH 19/91] Fix inscription disappearing when loading more inputs --- .../transactions-list/transactions-list.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index b07546e5e..fce181c3b 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -351,8 +351,12 @@ export class TransactionsListComponent implements OnInit, OnChanges { this.electrsApiService.getTransaction$(tx.txid) .subscribe((newTx) => { tx['@vinLoaded'] = true; + let temp = tx.vin; tx.vin = newTx.vin; tx.fee = newTx.fee; + for (const [index, vin] of temp.entries()) { + newTx.vin[index].isInscription = vin.isInscription; + } this.ref.markForCheck(); }); } From 774c0b4f832e6f8dc9f7dfa46bde14b0a0b3d0e2 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 22 Dec 2024 10:12:00 +0000 Subject: [PATCH 20/91] revert difficulty widget to true avg block time --- .../src/app/components/difficulty/difficulty.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/difficulty/difficulty.component.html b/frontend/src/app/components/difficulty/difficulty.component.html index c70656e04..e9bf36515 100644 --- a/frontend/src/app/components/difficulty/difficulty.component.html +++ b/frontend/src/app/components/difficulty/difficulty.component.html @@ -45,7 +45,7 @@
- ~ + ~
Average block time
From ba1ee15286c1bb79401b56eaee2fbd6ca2b58adb Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 22 Dec 2024 12:27:29 +0000 Subject: [PATCH 21/91] [accelerator] improve SCA UX --- .../accelerate-checkout.component.html | 9 +- .../accelerate-checkout.component.scss | 7 + .../accelerate-checkout.component.ts | 221 ++++++++++-------- 3 files changed, 135 insertions(+), 102 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index df67de65c..cfe2beec0 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -1,4 +1,4 @@ -
+
@if (accelerateError) {
@@ -361,7 +361,7 @@

Payment to mempool.space for acceleration of txid {{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}

- @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) { + @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {

Your account will be debited no more than {{ cost | number }} sats

@@ -484,6 +484,11 @@
}
+ @if (tokenizing) { +
+
+
+ }
diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss index ad085ed20..75c6a397d 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss @@ -8,6 +8,13 @@ color: var(--green) } +.accelerate-checkout-inner { + &.input-disabled { + pointer-events: none; + opacity: 0.75; + } +} + .paymentMethod { padding: 10px; background-color: var(--secondary); diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index d6ac7f54f..236326e0d 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -76,6 +76,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { calculating = true; processing = false; + checkoutLocked = false; + tokenizing = false; selectedOption: 'wait' | 'accel'; cantPayReason = ''; quoteError = ''; // error fetching estimate or initial data @@ -504,55 +506,64 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.loadingApplePay = false; applePayButton.addEventListener('click', async event => { event.preventDefault(); - const tokenResult = await this.applePay.tokenize(); - if (tokenResult?.status === 'OK') { - const card = tokenResult.details?.card; - if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { - console.error(`Cannot retreive payment card details`); - this.accelerateError = 'apple_pay_no_card_details'; - this.processing = false; - return; - } - const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); - this.servicesApiService.accelerateWithApplePay$( - this.tx.txid, - tokenResult.token, - cardTag, - `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, - costUSD - ).subscribe({ - next: () => { + try { + // lock the checkout UI and show a loading spinner until the square modals are finished + this.checkoutLocked = true; + this.tokenizing = true; + const tokenResult = await this.applePay.tokenize(); + if (tokenResult?.status === 'OK') { + const card = tokenResult.details?.card; + if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { + console.error(`Cannot retreive payment card details`); + this.accelerateError = 'apple_pay_no_card_details'; this.processing = false; - this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); - this.audioService.playSound('ascend-chime-cartoon'); - if (this.applePay) { - this.applePay.destroy(); - } - setTimeout(() => { - this.moveToStep('paid'); - }, 1000); - }, - error: (response) => { - this.processing = false; - this.accelerateError = response.error; - if (!(response.status === 403 && response.error === 'not_available')) { - setTimeout(() => { - // Reset everything by reloading the page :D, can be improved - const urlParams = new URLSearchParams(window.location.search); - window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); - }, 3000); - } + return; } - }); - } else { - this.processing = false; - let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; - if (tokenResult.errors) { - errorMessage += ` and errors: ${JSON.stringify( - tokenResult.errors, - )}`; + const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); + this.servicesApiService.accelerateWithApplePay$( + this.tx.txid, + tokenResult.token, + cardTag, + `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, + costUSD + ).subscribe({ + next: () => { + this.processing = false; + this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); + this.audioService.playSound('ascend-chime-cartoon'); + if (this.applePay) { + this.applePay.destroy(); + } + setTimeout(() => { + this.moveToStep('paid'); + }, 1000); + }, + error: (response) => { + this.processing = false; + this.accelerateError = response.error; + if (!(response.status === 403 && response.error === 'not_available')) { + setTimeout(() => { + // Reset everything by reloading the page :D, can be improved + const urlParams = new URLSearchParams(window.location.search); + window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); + }, 3000); + } + } + }); + } else { + this.processing = false; + let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; + if (tokenResult.errors) { + errorMessage += ` and errors: ${JSON.stringify( + tokenResult.errors, + )}`; + } + throw new Error(errorMessage); } - throw new Error(errorMessage); + } finally { + // always unlock the checkout once we're finished + this.tokenizing = false; + this.checkoutLocked = false; } }); } catch (e) { @@ -603,63 +614,73 @@ export class AccelerateCheckout implements OnInit, OnDestroy { document.getElementById('google-pay-button').addEventListener('click', async event => { event.preventDefault(); - const tokenResult = await this.googlePay.tokenize(); - if (tokenResult?.status === 'OK') { - const card = tokenResult.details?.card; - if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { - console.error(`Cannot retreive payment card details`); - this.accelerateError = 'apple_pay_no_card_details'; - this.processing = false; - return; - } - const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2)); - if (!verificationToken) { - console.error(`SCA verification failed`); - this.accelerateError = 'SCA Verification Failed. Payment Declined.'; - this.processing = false; - return; - } - const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); - this.servicesApiService.accelerateWithGooglePay$( - this.tx.txid, - tokenResult.token, - verificationToken, - cardTag, - `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, - costUSD - ).subscribe({ - next: () => { + try { + // lock the checkout UI and show a loading spinner until the square modals are finished + this.checkoutLocked = true; + this.tokenizing = true; + const tokenResult = await this.googlePay.tokenize(); + tokenResult.token = 'ccof:customer-card-id-requires-verification'; + if (tokenResult?.status === 'OK') { + const card = tokenResult.details?.card; + if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { + console.error(`Cannot retreive payment card details`); + this.accelerateError = 'apple_pay_no_card_details'; this.processing = false; - this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); - this.audioService.playSound('ascend-chime-cartoon'); - if (this.googlePay) { - this.googlePay.destroy(); - } - setTimeout(() => { - this.moveToStep('paid'); - }, 1000); - }, - error: (response) => { - this.processing = false; - this.accelerateError = response.error; - if (!(response.status === 403 && response.error === 'not_available')) { - setTimeout(() => { - // Reset everything by reloading the page :D, can be improved - const urlParams = new URLSearchParams(window.location.search); - window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); - }, 3000); - } + return; } - }); - } else { - this.processing = false; - let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; - if (tokenResult.errors) { - errorMessage += ` and errors: ${JSON.stringify( - tokenResult.errors, - )}`; + const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2)); + if (!verificationToken) { + console.error(`SCA verification failed`); + this.accelerateError = 'SCA Verification Failed. Payment Declined.'; + this.processing = false; + return; + } + const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); + this.servicesApiService.accelerateWithGooglePay$( + this.tx.txid, + tokenResult.token, + verificationToken, + cardTag, + `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, + costUSD + ).subscribe({ + next: () => { + this.processing = false; + this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); + this.audioService.playSound('ascend-chime-cartoon'); + if (this.googlePay) { + this.googlePay.destroy(); + } + setTimeout(() => { + this.moveToStep('paid'); + }, 1000); + }, + error: (response) => { + this.processing = false; + this.accelerateError = response.error; + if (!(response.status === 403 && response.error === 'not_available')) { + setTimeout(() => { + // Reset everything by reloading the page :D, can be improved + const urlParams = new URLSearchParams(window.location.search); + window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); + }, 3000); + } + } + }); + } else { + this.processing = false; + let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; + if (tokenResult.errors) { + errorMessage += ` and errors: ${JSON.stringify( + tokenResult.errors, + )}`; + } + throw new Error(errorMessage); } - throw new Error(errorMessage); + } finally { + // always unlock the checkout once we're finished + this.tokenizing = false; + this.checkoutLocked = false; } }); } From ed28a24c8aedbb1874089c8d445872393b877842 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 22 Dec 2024 22:33:54 +0800 Subject: [PATCH 22/91] fix duplicated response header --- backend/src/api/bitcoin/bitcoin.routes.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 339c4cff9..ccdc3dc7c 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -229,6 +229,7 @@ class BitcoinRoutes { if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; handleError(req, res, statusCode, 'No such mempool or blockchain transaction'); + return; } handleError(req, res, statusCode, 'Failed to get transaction'); } @@ -248,6 +249,7 @@ class BitcoinRoutes { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; handleError(req, res, statusCode, 'No such mempool or blockchain transaction'); + return; } handleError(req, res, statusCode, 'Failed to get raw transaction'); } @@ -334,6 +336,7 @@ class BitcoinRoutes { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; handleError(req, res, statusCode, 'No such mempool or blockchain transaction'); + return; } handleError(req, res, statusCode, 'Failed to get transaction status'); } From c9b9485313f18c3672c9483a02c86e4239af5d30 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 22 Dec 2024 22:40:35 +0800 Subject: [PATCH 23/91] [faucet] add new error message when no utxo available --- frontend/src/app/components/faucet/faucet.component.ts | 2 +- .../shared/components/mempool-error/mempool-error.component.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/faucet/faucet.component.ts b/frontend/src/app/components/faucet/faucet.component.ts index 33d9a849e..6bf55ebac 100644 --- a/frontend/src/app/components/faucet/faucet.component.ts +++ b/frontend/src/app/components/faucet/faucet.component.ts @@ -24,7 +24,7 @@ export class FaucetComponent implements OnInit, OnDestroy { min: number; // minimum amount to request at once (in sats) max: number; // maximum amount to request at once address?: string; // faucet address - code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon'; + code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon' | 'faucet_not_available_no_utxo'; } | null = null; faucetForm: FormGroup; diff --git a/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts b/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts index f105a9655..7eb9bb5eb 100644 --- a/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts +++ b/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts @@ -26,6 +26,7 @@ export const MempoolErrors = { 'unauthorized': `You are not authorized to do this`, 'faucet_too_soon': `You cannot request any more coins right now. Try again later.`, 'faucet_not_available': `The faucet is not available right now. Try again later.`, + 'faucet_not_available_no_utxo': `The faucet is not available right now. Please try again once a new block has been mined.`, 'faucet_maximum_reached': `You are not allowed to request more coins`, 'faucet_address_not_allowed': `You cannot use this address`, 'faucet_below_minimum': `Requested amount is too small`, From 464fabf137a1deb6021b8c3bd9cc472d12c05c45 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 22 Dec 2024 12:27:29 +0000 Subject: [PATCH 24/91] [accelerator] reference counting for checkout lock --- .../accelerate-checkout.component.html | 4 +-- .../accelerate-checkout.component.ts | 35 +++++++++++++------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index cfe2beec0..150da04da 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -1,4 +1,4 @@ -
+
@if (accelerateError) {
@@ -484,7 +484,7 @@
}
- @if (tokenizing) { + @if (isTokenizing > 0) {
diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 236326e0d..be40b92b1 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -76,8 +76,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { calculating = true; processing = false; - checkoutLocked = false; - tokenizing = false; + isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked + isTokenizing = 0; // reference counter, 0 = false, >0 = true selectedOption: 'wait' | 'accel'; cantPayReason = ''; quoteError = ''; // error fetching estimate or initial data @@ -508,8 +508,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { event.preventDefault(); try { // lock the checkout UI and show a loading spinner until the square modals are finished - this.checkoutLocked = true; - this.tokenizing = true; + this.isCheckoutLocked++; + this.isTokenizing++; const tokenResult = await this.applePay.tokenize(); if (tokenResult?.status === 'OK') { const card = tokenResult.details?.card; @@ -520,6 +520,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return; } const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); + // keep checkout in loading state until the acceleration request completes + this.isTokenizing++; + this.isCheckoutLocked++; this.servicesApiService.accelerateWithApplePay$( this.tx.txid, tokenResult.token, @@ -535,12 +538,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.applePay.destroy(); } setTimeout(() => { + this.isTokenizing--; + this.isCheckoutLocked--; this.moveToStep('paid'); }, 1000); }, error: (response) => { this.processing = false; this.accelerateError = response.error; + this.isTokenizing--; + this.isCheckoutLocked--; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { // Reset everything by reloading the page :D, can be improved @@ -562,8 +569,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } } finally { // always unlock the checkout once we're finished - this.tokenizing = false; - this.checkoutLocked = false; + this.isTokenizing--; + this.isCheckoutLocked--; } }); } catch (e) { @@ -616,10 +623,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { event.preventDefault(); try { // lock the checkout UI and show a loading spinner until the square modals are finished - this.checkoutLocked = true; - this.tokenizing = true; + this.isCheckoutLocked++; + this.isTokenizing++; const tokenResult = await this.googlePay.tokenize(); - tokenResult.token = 'ccof:customer-card-id-requires-verification'; if (tokenResult?.status === 'OK') { const card = tokenResult.details?.card; if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { @@ -636,6 +642,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return; } const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); + // keep checkout in loading state until the acceleration request completes + this.isCheckoutLocked++; + this.isTokenizing++; this.servicesApiService.accelerateWithGooglePay$( this.tx.txid, tokenResult.token, @@ -646,6 +655,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { ).subscribe({ next: () => { this.processing = false; + this.isTokenizing--; + this.isCheckoutLocked--; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.googlePay) { @@ -658,6 +669,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { error: (response) => { this.processing = false; this.accelerateError = response.error; + this.isTokenizing--; + this.isCheckoutLocked--; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { // Reset everything by reloading the page :D, can be improved @@ -679,8 +692,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } } finally { // always unlock the checkout once we're finished - this.tokenizing = false; - this.checkoutLocked = false; + this.isTokenizing--; + this.isCheckoutLocked--; } }); } From f49152d09dc46a05d7bf122356bdbe2ed58dad66 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 22 Dec 2024 15:17:39 +0000 Subject: [PATCH 25/91] [accelerator] keep checkout locked until request completes --- .../accelerate-checkout.component.ts | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index be40b92b1..a69b7b107 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -156,7 +156,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerateError = null; this.timePaid = 0; this.btcpayInvoiceFailed = false; - this.moveToStep('summary'); + this.moveToStep('summary', true); } else { this.auth = auth; } @@ -165,11 +165,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('cash_request_id')) { // Redirected from cashapp - this.moveToStep('processing'); + this.moveToStep('processing', true); this.insertSquare(); this.setupSquare(); } else { - this.moveToStep('summary'); + this.moveToStep('summary', true); } this.conversionsSubscription = this.stateService.conversions$.subscribe( @@ -194,14 +194,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } if (changes.accelerating && this.accelerating) { if (this.step === 'processing' || this.step === 'paid') { - this.moveToStep('success'); + this.moveToStep('success', true); } else { // Edge case where the transaction gets accelerated by someone else or on another session this.closeModal(); } } } - moveToStep(step: CheckoutStep): void { + moveToStep(step: CheckoutStep, force: boolean = false): void { + if (this.isCheckoutLocked > 0 && !force) { + return; + } this.processing = false; this._step = step; if (this.timeoutTimer) { @@ -244,7 +247,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { closeModal(): void { this.completed.emit(true); - this.moveToStep('summary'); + this.moveToStep('summary', true); } /** @@ -395,7 +398,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.audioService.playSound('ascend-chime-cartoon'); this.showSuccess = true; this.estimateSubscription.unsubscribe(); - this.moveToStep('paid'); + this.moveToStep('paid', true); }, error: (response) => { this.processing = false; @@ -505,6 +508,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } this.loadingApplePay = false; applePayButton.addEventListener('click', async event => { + if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) { + return; + } event.preventDefault(); try { // lock the checkout UI and show a loading spinner until the square modals are finished @@ -540,16 +546,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy { setTimeout(() => { this.isTokenizing--; this.isCheckoutLocked--; - this.moveToStep('paid'); + this.moveToStep('paid', true); }, 1000); }, error: (response) => { this.processing = false; this.accelerateError = response.error; - this.isTokenizing--; - this.isCheckoutLocked--; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { + this.isTokenizing--; + this.isCheckoutLocked--; // Reset everything by reloading the page :D, can be improved const urlParams = new URLSearchParams(window.location.search); window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); @@ -620,6 +626,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.loadingGooglePay = false; document.getElementById('google-pay-button').addEventListener('click', async event => { + if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) { + return; + } event.preventDefault(); try { // lock the checkout UI and show a loading spinner until the square modals are finished @@ -655,15 +664,15 @@ export class AccelerateCheckout implements OnInit, OnDestroy { ).subscribe({ next: () => { this.processing = false; - this.isTokenizing--; - this.isCheckoutLocked--; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.googlePay) { this.googlePay.destroy(); } setTimeout(() => { - this.moveToStep('paid'); + this.isTokenizing--; + this.isCheckoutLocked--; + this.moveToStep('paid', true); }, 1000); }, error: (response) => { @@ -760,7 +769,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.cashAppPay.destroy(); } setTimeout(() => { - this.moveToStep('paid'); + this.moveToStep('paid', true); if (window.history.replaceState) { const urlParams = new URLSearchParams(window.location.search); window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, '')); @@ -834,7 +843,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); this.estimateSubscription.unsubscribe(); - this.moveToStep('paid'); + this.moveToStep('paid', true); } isLoggedIn(): boolean { From a5c67b5ca1bb13084e2e6e205a3a76f3cde9566b Mon Sep 17 00:00:00 2001 From: natsoni Date: Sun, 22 Dec 2024 20:16:07 +0100 Subject: [PATCH 26/91] Add websocket commands doc --- .../src/app/docs/api-docs/api-docs-data.ts | 2524 ++++++++++++++++- .../docs/api-docs/api-docs-nav.component.ts | 4 +- .../app/docs/api-docs/api-docs.component.html | 45 +- .../app/docs/api-docs/api-docs.component.scss | 18 + .../app/docs/api-docs/api-docs.component.ts | 28 +- 5 files changed, 2513 insertions(+), 106 deletions(-) diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index 1f83cabc9..c32baa3f7 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -13,96 +13,2442 @@ const emptyCodeSample = { const showJsExamplesDefault = { "": true, "testnet": true, "signet": true, "liquid": true, "liquidtestnet": false }; const showJsExamplesDefaultFalse = { "": false, "testnet": false, "signet": false, "liquid": false, "liquidtestnet": false }; -export const wsApiDocsData = { - showJsExamples: showJsExamplesDefault, - codeTemplate: { - curl: `/api/v1/ws`, - commonJS: ` - const { %{0}: { websocket } } = mempoolJS(); - - const ws = websocket.initClient({ - options: ['blocks', 'stats', 'mempool-blocks', 'live-2h-chart'], - }); - - ws.addEventListener('message', function incoming({data}) { - const res = JSON.parse(data.toString()); - if (res.block) { - document.getElementById("result-blocks").textContent = JSON.stringify(res.block, undefined, 2); - } - if (res.mempoolInfo) { - document.getElementById("result-mempool-info").textContent = JSON.stringify(res.mempoolInfo, undefined, 2); - } - if (res.transactions) { - document.getElementById("result-transactions").textContent = JSON.stringify(res.transactions, undefined, 2); - } - if (res["mempool-blocks"]) { - document.getElementById("result-mempool-blocks").textContent = JSON.stringify(res["mempool-blocks"], undefined, 2); - } - }); - `, - esModule: ` - const { %{0}: { websocket } } = mempoolJS(); - - const ws = websocket.initServer({ - options: ["blocks", "stats", "mempool-blocks", "live-2h-chart"], - }); - - ws.on("message", function incoming(data) { - const res = JSON.parse(data.toString()); - if (res.block) { - console.log(res.block); - } - if (res.mempoolInfo) { - console.log(res.mempoolInfo); - } - if (res.transactions) { - console.log(res.transactions); - } - if (res["mempool-blocks"]) { - console.log(res["mempool-blocks"]); - } - }); - `, - python: `import websocket -import _thread -import time -import rel -import json - -rel.safe_read() - -def on_message(ws, message): - print(json.loads(message)) - -def on_error(ws, error): - print(error) - -def on_close(ws, close_status_code, close_msg): - print("### closed ###") - -def on_open(ws): - message = { "action": "init" } - ws.send(json.dumps(message)) - message = { "action": "want", "data": ['blocks', 'stats', 'mempool-blocks', 'live-2h-chart', 'watch-mempool'] } - ws.send(json.dumps(message)) - -if __name__ == "__main__": - ws = websocket.WebSocketApp("wss://mempool.space/api/v1/ws", - on_open=on_open, - on_message=on_message, - on_error=on_error, - on_close=on_close) - - ws.run_forever(dispatcher=rel) # Set dispatcher to automatic reconnection - rel.signal(2, rel.abort) # Keyboard Interrupt - rel.dispatch() - `, +export const wsApiDocsData = [ + { + type: "category", + category: "general", + fragment: "general", + title: "General", + showConditions: bitcoinNetworks.concat(liquidNetworks) }, - codeSampleMainnet: emptyCodeSample, - codeSampleTestnet: emptyCodeSample, - codeSampleSignet: emptyCodeSample, - codeSampleLiquid: emptyCodeSample, -}; + { + type: "endpoint", + category: "general", + fragment: "live-data", + title: "Live Data", + description: { + default: "Subscribe to live data. Available: blocks, mempool-block, live-2h-chart, and stats." + }, + payload: '{ "action": "want", "data": ["mempool-blocks", "stats"] }', + showConditions: bitcoinNetworks.concat(liquidNetworks), + showJsExamples: false, + codeExample: { + default: { + codeTemplate: { + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "mempool-blocks": [ + { + "blockSize": 1801614, + "blockVSize": 997936.5, + "nTx": 3391, + "totalFees": 8170664, + "medianFee": 6.011217160720601, + "feeRange": [ + 4.584615384615384, + 5, + 5.100456621004566, + 6.002319288751449, + 7.235398230088496, + 10.377668308702791, + 200 + ] + }, + ... + { + "blockSize": 198543075, + "blockVSize": 101691348, + "nTx": 249402, + "totalFees": 135312667, + "medianFee": 1.2559438783834156, + "feeRange": [ + 1.000685629033809, + 1.0020213063577312, + 1.0019080827758888, + 1.0227913345013278, + 1.1188648002395873, + 1.2559438783834156, + 1.4077952614964329, + 1.4079805737077244, + 1.5106880342499638, + 2.003440424869914, + 2.2713888268854894 + ] + } + ], + "mempoolInfo": { + "loaded": true, + "size": 264505, + "bytes": 108875402, + "usage": 649908688, + "total_fee": 1.61036575, + "maxmempool": 300000000, + "mempoolminfee": 0.00001858, + "minrelaytxfee": 0.00001, + "incrementalrelayfee": 0.00001, + "unbroadcastcount": 0, + "fullrbf": true + }, + "vBytesPerSecond": 1651, + "fees": { + "fastestFee": 7, + "halfHourFee": 6, + "hourFee": 5, + "economyFee": 4, + "minimumFee": 2 + }, + "da": { + "progressPercent": 32.49007936507937, + "difficultyChange": 0.7843046881601534, + "estimatedRetargetDate": 1735514828279, + "remainingBlocks": 1361, + "remainingTime": 811481279, + "previousRetarget": 4.429396745461176, + "previousTime": 1734312810, + "nextRetargetHeight": 876960, + "timeAvg": 596239, + "adjustedTimeAvg": 596239, + "timeOffset": 0, + "expectedBlocks": 650.895 + } +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "mempool-blocks": [ + { + "blockSize": 1009960, + "blockVSize": 997827.25, + "nTx": 3545, + "totalFees": 2844117938, + "medianFee": 2524.178404298769, + "feeRange": [ + 2010.9044259140476, + 2011.0887096774193, + 2011.2914608327453, + 2441.5893066980025, + 3541.35960591133, + 3936.6254416961133, + 6031.746031746032 + ] + }, + ... + ], + "mempoolInfo": { + "loaded": true, + "size": 517666, + "bytes": 168219654, + "usage": 855583264, + "total_fee": 133.53837564, + "maxmempool": 4096000000, + "mempoolminfee": 0.00001, + "minrelaytxfee": 0.00001, + "incrementalrelayfee": 0.00001, + "unbroadcastcount": 0, + "fullrbf": true + }, + "vBytesPerSecond": 358, + "fees": { + "fastestFee": 2525, + "halfHourFee": 2268, + "hourFee": 2082, + "economyFee": 2, + "minimumFee": 1 + }, + "da": { + "progressPercent": 45.882936507936506, + "difficultyChange": -51.21445794134847, + "estimatedRetargetDate": 1736046916382, + "remainingBlocks": 1091, + "remainingTime": 1343241382, + "previousRetarget": 255.61790932023905, + "previousTime": 1733564813, + "nextRetargetHeight": 3538080, + "timeAvg": 1200000, + "adjustedTimeAvg": 1231202, + "timeOffset": 0, + "expectedBlocks": 1898.1033333333332 + } +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `{"mempool-blocks": [ + { + "blockSize": 1009960, + "blockVSize": 997827.25, + "nTx": 3545, + "totalFees": 2844117938, + "medianFee": 2524.178404298769, + "feeRange": [ + 2010.9044259140476, + 2011.0887096774193, + 2011.2914608327453, + 2441.5893066980025, + 3541.35960591133, + 3936.6254416961133, + 6031.746031746032 + ] + }, + ... + ], + "mempoolInfo": { + "loaded": true, + "size": 59, + "bytes": 9834, + "usage": 68832, + "total_fee": 0.00013935, + "maxmempool": 4096000000, + "mempoolminfee": 0.00001, + "minrelaytxfee": 0.00001, + "incrementalrelayfee": 0.00001, + "unbroadcastcount": 0, + "fullrbf": true + }, + "vBytesPerSecond": 28, + "da": { + "progressPercent": 68.60119047619048, + "difficultyChange": -2.913529439274176, + "estimatedRetargetDate": 1735095294116, + "remainingBlocks": 633, + "remainingTime": 391480116, + "previousRetarget": 2.0685719720386118, + "previousTime": 1733848494, + "nextRetargetHeight": 227808, + "timeAvg": 618452, + "adjustedTimeAvg": 618452, + "timeOffset": 0, + "expectedBlocks": 1425.5333333333333 + }, + "fees": { + "fastestFee": 1, + "halfHourFee": 1, + "hourFee": 1, + "economyFee": 1, + "minimumFee": 1 + }, +}` + }, + codeSampleLiquid: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "mempool-blocks": [ + { + "blockSize": 27409, + "blockVSize": 7675, + "nTx": 2, + "totalFees": 769, + "medianFee": 0, + "feeRange": [ + 0.10012450036039577, + 0.10012450036039577, + 0.10012450036039577, + 0.10012450036039577, + 0.10012450036039577, + 0.10012450036039577, + 0.10012450036039577 + ] + } + ], + "mempoolInfo": { + "loaded": true, + "size": 2, + "bytes": 7676, + "usage": 3568, + "total_fee": 0.00000769, + "maxmempool": 300000000, + "mempoolminfee": 0.000001, + "minrelaytxfee": 0.000001, + "unbroadcastcount": 0 + }, + "vBytesPerSecond": 60, + "fees": { + "fastestFee": 0.1, + "halfHourFee": 0.1, + "hourFee": 0.1, + "economyFee": 0.1, + "minimumFee": 0.1 + }, + "da": { + "progressPercent": 4.315476190476191, + "difficultyChange": null, + "estimatedRetargetDate": null, + "remainingBlocks": 1929, + "remainingTime": null, + "previousRetarget": null, + "previousTime": 1734698648, + "nextRetargetHeight": 3173184, + "timeAvg": 60448, + "adjustedTimeAvg": null, + "timeOffset": 0, + "expectedBlocks": 8.765 + } +}` + } + } + } + }, + { + type: "category", + category: "addresses", + fragment: "addresses", + title: "Addresses", + showConditions: bitcoinNetworks.concat(liquidNetworks) + }, + { + type: "endpoint", + category: "addresses", + fragment: "track-address", + title: "Track Address", + description: { + default: "Subscribe to a single address to receive live updates on new transactions having that address in input or output. address-transactions field contains new mempool transactions, and block-transactions contains new confirmed transactions." + }, + payload: '{ "track-address": "bc1qeldw4mqns26wew8swgpkt3fs364w3ehs046w2f" }', + showConditions: bitcoinNetworks.concat(liquidNetworks), + showJsExamples: false, + codeExample: { + default: { + codeTemplate: { + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "block-transactions": [ + { + "txid": "9d3ea0d131c45450c135d549b62032019bc47a80368e14edc72caf38f5a88033", + "version": 1, + "locktime": 0, + "vin": [ + { + "txid": "69da555a9c69788a3a081958457894e56b1ee6766bc72cecf881b1b4f327f78b", + "vout": 0, + "prevout": { + "scriptpubkey": "a914c9848245ae4f5d5934b5cbdfb79e04cdd337470b87", + "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 c9848245ae4f5d5934b5cbdfb79e04cdd337470b OP_EQUAL", + "scriptpubkey_type": "p2sh", + "scriptpubkey_address": "3L4YUynB4X44rJBY9CmiLMN8Wjti49JCYB", + "value": 24962957 + }, + "scriptsig": "0048304502210099219ee0cd5da341650078e3c63885b3cc2211069f2551cf436e0100f421e1760220349b4ec284255b458d6da539fa17314e8330459e0a653c254f775d4ec8f32b3f0147304402203a8353c5ee76a2e266432e5f993f882e05725297e64c0833cf44719f7dda8d3b022058a2f72e7739efd21657b4943cac60a0a3c749e712787f0e85726da4c3adcf8e014c695221027f2a0df8e86535d08ca3e766a178f90c813d2dd1d55b0166e82518efbffb18c02103556a35844b517e2fc8216701e2e0a64dbcbe62ad420ac6dd73dc79e69efeb69521031ef21bd55171032b7aec21ec82932735fb986f1d4d8611feee62ab38acf4a6bb53ae", + "scriptsig_asm": "OP_0 OP_PUSHBYTES_72 304502210099219ee0cd5da341650078e3c63885b3cc2211069f2551cf436e0100f421e1760220349b4ec284255b458d6da539fa17314e8330459e0a653c254f775d4ec8f32b3f01 OP_PUSHBYTES_71 304402203a8353c5ee76a2e266432e5f993f882e05725297e64c0833cf44719f7dda8d3b022058a2f72e7739efd21657b4943cac60a0a3c749e712787f0e85726da4c3adcf8e01 OP_PUSHDATA1 5221027f2a0df8e86535d08ca3e766a178f90c813d2dd1d55b0166e82518efbffb18c02103556a35844b517e2fc8216701e2e0a64dbcbe62ad420ac6dd73dc79e69efeb69521031ef21bd55171032b7aec21ec82932735fb986f1d4d8611feee62ab38acf4a6bb53ae", + "is_coinbase": false, + "sequence": 4294967295, + "inner_redeemscript_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 027f2a0df8e86535d08ca3e766a178f90c813d2dd1d55b0166e82518efbffb18c0 OP_PUSHBYTES_33 03556a35844b517e2fc8216701e2e0a64dbcbe62ad420ac6dd73dc79e69efeb695 OP_PUSHBYTES_33 031ef21bd55171032b7aec21ec82932735fb986f1d4d8611feee62ab38acf4a6bb OP_PUSHNUM_3 OP_CHECKMULTISIG" + }, + ... + { + "txid": "43852d32c7ae6d362d446d090daa4d389f78ec77e6693f9248cd924dc0b1ecc3", + "vout": 1, + "prevout": { + "scriptpubkey": "a914a3aff5f5765f167c1582fd85517ddde83174118187", + "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 a3aff5f5765f167c1582fd85517ddde831741181 OP_EQUAL", + "scriptpubkey_type": "p2sh", + "scriptpubkey_address": "3GcWrnGFoNzbn1KaiP5czS5xPELdWcgDX2", + "value": 1719827 + }, + "scriptsig": "0047304402205f83d22a0476158aa0986682c96ce2b2dab26c814968dba62905cdfeef1b3ac7022059438a3439bb18bd49242010c8a276ea6f1810d523042e679fa6679d60e89e0201483045022100eb085df09e0fb4894090a5f39b9f2188392f7ac2847ed8255629baffc7371f170220120463b91d6c4bb8968fb3eda9012b88d13d8ca71de28e7a64b1dd88282ff144014c69522103650083cbc9cd1da1224e0780bce1ee8abd5150c5252defd0edeccd3521610b282102510ab30a6a97464ef0d61f71ec8b1d2325f12934ff15ba73579bfd0ac5f4fc1a2102985b3be77f56a9a29c5f68d3c893d6c4d76ec8c07792f0291d375c29b71ee2f853ae", + "scriptsig_asm": "OP_0 OP_PUSHBYTES_71 304402205f83d22a0476158aa0986682c96ce2b2dab26c814968dba62905cdfeef1b3ac7022059438a3439bb18bd49242010c8a276ea6f1810d523042e679fa6679d60e89e0201 OP_PUSHBYTES_72 3045022100eb085df09e0fb4894090a5f39b9f2188392f7ac2847ed8255629baffc7371f170220120463b91d6c4bb8968fb3eda9012b88d13d8ca71de28e7a64b1dd88282ff14401 OP_PUSHDATA1 522103650083cbc9cd1da1224e0780bce1ee8abd5150c5252defd0edeccd3521610b282102510ab30a6a97464ef0d61f71ec8b1d2325f12934ff15ba73579bfd0ac5f4fc1a2102985b3be77f56a9a29c5f68d3c893d6c4d76ec8c07792f0291d375c29b71ee2f853ae", + "is_coinbase": false, + "sequence": 4294967295, + "inner_redeemscript_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 03650083cbc9cd1da1224e0780bce1ee8abd5150c5252defd0edeccd3521610b28 OP_PUSHBYTES_33 02510ab30a6a97464ef0d61f71ec8b1d2325f12934ff15ba73579bfd0ac5f4fc1a OP_PUSHBYTES_33 02985b3be77f56a9a29c5f68d3c893d6c4d76ec8c07792f0291d375c29b71ee2f8 OP_PUSHNUM_3 OP_CHECKMULTISIG" + } + ], + "vout": [ + { + "scriptpubkey": "0014292fce548b228cd3df6334dd525fac62e7eb5f7a", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 292fce548b228cd3df6334dd525fac62e7eb5f7a", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "bc1q9yhuu4yty2xd8hmrxnw4yhavvtn7khm62uw38p", + "value": 57000 + }, + ... + { + "scriptpubkey": "0020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec", + "scriptpubkey_type": "v0_p2wsh", + "scriptpubkey_address": "bc1quhruqrghgcca950rvhtrg7cpd7u8k6svpzgzmrjy8xyukacl5lkq0r8l2d", + "value": 17343523 + } + ], + "size": 5514, + "weight": 22056, + "sigops": 208, + "fee": 44000, + "status": { + "confirmed": true, + "block_height": 875602, + "block_hash": "000000000000000000016c0639b6c1a34d6659c231aa2de5849ab3377ed75020", + "block_time": 1734704791 + }, + "order": 864069877, + "vsize": 5514, + "adjustedVsize": 5514, + "feePerVsize": 7.979688066739209, + "adjustedFeePerVsize": 7.979688066739209, + "effectiveFeePerVsize": 7.979688066739209, + "firstSeen": 1734704590, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 191567 + }, + "flags": 1099511659526 + } + ] +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "block-transactions": [ + { + "txid": "9d3ea0d131c45450c135d549b62032019bc47a80368e14edc72caf38f5a88033", + "version": 1, + "locktime": 0, + "vin": [ + { + "txid": "69da555a9c69788a3a081958457894e56b1ee6766bc72cecf881b1b4f327f78b", + "vout": 0, + "prevout": { + "scriptpubkey": "a914c9848245ae4f5d5934b5cbdfb79e04cdd337470b87", + "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 c9848245ae4f5d5934b5cbdfb79e04cdd337470b OP_EQUAL", + "scriptpubkey_type": "p2sh", + "scriptpubkey_address": "3L4YUynB4X44rJBY9CmiLMN8Wjti49JCYB", + "value": 24962957 + }, + "scriptsig": "0048304502210099219ee0cd5da341650078e3c63885b3cc2211069f2551cf436e0100f421e1760220349b4ec284255b458d6da539fa17314e8330459e0a653c254f775d4ec8f32b3f0147304402203a8353c5ee76a2e266432e5f993f882e05725297e64c0833cf44719f7dda8d3b022058a2f72e7739efd21657b4943cac60a0a3c749e712787f0e85726da4c3adcf8e014c695221027f2a0df8e86535d08ca3e766a178f90c813d2dd1d55b0166e82518efbffb18c02103556a35844b517e2fc8216701e2e0a64dbcbe62ad420ac6dd73dc79e69efeb69521031ef21bd55171032b7aec21ec82932735fb986f1d4d8611feee62ab38acf4a6bb53ae", + "scriptsig_asm": "OP_0 OP_PUSHBYTES_72 304502210099219ee0cd5da341650078e3c63885b3cc2211069f2551cf436e0100f421e1760220349b4ec284255b458d6da539fa17314e8330459e0a653c254f775d4ec8f32b3f01 OP_PUSHBYTES_71 304402203a8353c5ee76a2e266432e5f993f882e05725297e64c0833cf44719f7dda8d3b022058a2f72e7739efd21657b4943cac60a0a3c749e712787f0e85726da4c3adcf8e01 OP_PUSHDATA1 5221027f2a0df8e86535d08ca3e766a178f90c813d2dd1d55b0166e82518efbffb18c02103556a35844b517e2fc8216701e2e0a64dbcbe62ad420ac6dd73dc79e69efeb69521031ef21bd55171032b7aec21ec82932735fb986f1d4d8611feee62ab38acf4a6bb53ae", + "is_coinbase": false, + "sequence": 4294967295, + "inner_redeemscript_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 027f2a0df8e86535d08ca3e766a178f90c813d2dd1d55b0166e82518efbffb18c0 OP_PUSHBYTES_33 03556a35844b517e2fc8216701e2e0a64dbcbe62ad420ac6dd73dc79e69efeb695 OP_PUSHBYTES_33 031ef21bd55171032b7aec21ec82932735fb986f1d4d8611feee62ab38acf4a6bb OP_PUSHNUM_3 OP_CHECKMULTISIG" + }, + ... + { + "txid": "43852d32c7ae6d362d446d090daa4d389f78ec77e6693f9248cd924dc0b1ecc3", + "vout": 1, + "prevout": { + "scriptpubkey": "a914a3aff5f5765f167c1582fd85517ddde83174118187", + "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 a3aff5f5765f167c1582fd85517ddde831741181 OP_EQUAL", + "scriptpubkey_type": "p2sh", + "scriptpubkey_address": "3GcWrnGFoNzbn1KaiP5czS5xPELdWcgDX2", + "value": 1719827 + }, + "scriptsig": "0047304402205f83d22a0476158aa0986682c96ce2b2dab26c814968dba62905cdfeef1b3ac7022059438a3439bb18bd49242010c8a276ea6f1810d523042e679fa6679d60e89e0201483045022100eb085df09e0fb4894090a5f39b9f2188392f7ac2847ed8255629baffc7371f170220120463b91d6c4bb8968fb3eda9012b88d13d8ca71de28e7a64b1dd88282ff144014c69522103650083cbc9cd1da1224e0780bce1ee8abd5150c5252defd0edeccd3521610b282102510ab30a6a97464ef0d61f71ec8b1d2325f12934ff15ba73579bfd0ac5f4fc1a2102985b3be77f56a9a29c5f68d3c893d6c4d76ec8c07792f0291d375c29b71ee2f853ae", + "scriptsig_asm": "OP_0 OP_PUSHBYTES_71 304402205f83d22a0476158aa0986682c96ce2b2dab26c814968dba62905cdfeef1b3ac7022059438a3439bb18bd49242010c8a276ea6f1810d523042e679fa6679d60e89e0201 OP_PUSHBYTES_72 3045022100eb085df09e0fb4894090a5f39b9f2188392f7ac2847ed8255629baffc7371f170220120463b91d6c4bb8968fb3eda9012b88d13d8ca71de28e7a64b1dd88282ff14401 OP_PUSHDATA1 522103650083cbc9cd1da1224e0780bce1ee8abd5150c5252defd0edeccd3521610b282102510ab30a6a97464ef0d61f71ec8b1d2325f12934ff15ba73579bfd0ac5f4fc1a2102985b3be77f56a9a29c5f68d3c893d6c4d76ec8c07792f0291d375c29b71ee2f853ae", + "is_coinbase": false, + "sequence": 4294967295, + "inner_redeemscript_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 03650083cbc9cd1da1224e0780bce1ee8abd5150c5252defd0edeccd3521610b28 OP_PUSHBYTES_33 02510ab30a6a97464ef0d61f71ec8b1d2325f12934ff15ba73579bfd0ac5f4fc1a OP_PUSHBYTES_33 02985b3be77f56a9a29c5f68d3c893d6c4d76ec8c07792f0291d375c29b71ee2f8 OP_PUSHNUM_3 OP_CHECKMULTISIG" + } + ], + "vout": [ + { + "scriptpubkey": "0014292fce548b228cd3df6334dd525fac62e7eb5f7a", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 292fce548b228cd3df6334dd525fac62e7eb5f7a", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "bc1q9yhuu4yty2xd8hmrxnw4yhavvtn7khm62uw38p", + "value": 57000 + }, + ... + { + "scriptpubkey": "0020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec", + "scriptpubkey_type": "v0_p2wsh", + "scriptpubkey_address": "bc1quhruqrghgcca950rvhtrg7cpd7u8k6svpzgzmrjy8xyukacl5lkq0r8l2d", + "value": 17343523 + } + ], + "size": 5514, + "weight": 22056, + "sigops": 208, + "fee": 44000, + "status": { + "confirmed": true, + "block_height": 875602, + "block_hash": "000000000000000000016c0639b6c1a34d6659c231aa2de5849ab3377ed75020", + "block_time": 1734704791 + }, + "order": 864069877, + "vsize": 5514, + "adjustedVsize": 5514, + "feePerVsize": 7.979688066739209, + "adjustedFeePerVsize": 7.979688066739209, + "effectiveFeePerVsize": 7.979688066739209, + "firstSeen": 1734704590, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 191567 + }, + "flags": 1099511659526 + } + ] +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "block-transactions": [ + { + "txid": "9d3ea0d131c45450c135d549b62032019bc47a80368e14edc72caf38f5a88033", + "version": 1, + "locktime": 0, + "vin": [ + { + "txid": "69da555a9c69788a3a081958457894e56b1ee6766bc72cecf881b1b4f327f78b", + "vout": 0, + "prevout": { + "scriptpubkey": "a914c9848245ae4f5d5934b5cbdfb79e04cdd337470b87", + "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 c9848245ae4f5d5934b5cbdfb79e04cdd337470b OP_EQUAL", + "scriptpubkey_type": "p2sh", + "scriptpubkey_address": "3L4YUynB4X44rJBY9CmiLMN8Wjti49JCYB", + "value": 24962957 + }, + "scriptsig": "0048304502210099219ee0cd5da341650078e3c63885b3cc2211069f2551cf436e0100f421e1760220349b4ec284255b458d6da539fa17314e8330459e0a653c254f775d4ec8f32b3f0147304402203a8353c5ee76a2e266432e5f993f882e05725297e64c0833cf44719f7dda8d3b022058a2f72e7739efd21657b4943cac60a0a3c749e712787f0e85726da4c3adcf8e014c695221027f2a0df8e86535d08ca3e766a178f90c813d2dd1d55b0166e82518efbffb18c02103556a35844b517e2fc8216701e2e0a64dbcbe62ad420ac6dd73dc79e69efeb69521031ef21bd55171032b7aec21ec82932735fb986f1d4d8611feee62ab38acf4a6bb53ae", + "scriptsig_asm": "OP_0 OP_PUSHBYTES_72 304502210099219ee0cd5da341650078e3c63885b3cc2211069f2551cf436e0100f421e1760220349b4ec284255b458d6da539fa17314e8330459e0a653c254f775d4ec8f32b3f01 OP_PUSHBYTES_71 304402203a8353c5ee76a2e266432e5f993f882e05725297e64c0833cf44719f7dda8d3b022058a2f72e7739efd21657b4943cac60a0a3c749e712787f0e85726da4c3adcf8e01 OP_PUSHDATA1 5221027f2a0df8e86535d08ca3e766a178f90c813d2dd1d55b0166e82518efbffb18c02103556a35844b517e2fc8216701e2e0a64dbcbe62ad420ac6dd73dc79e69efeb69521031ef21bd55171032b7aec21ec82932735fb986f1d4d8611feee62ab38acf4a6bb53ae", + "is_coinbase": false, + "sequence": 4294967295, + "inner_redeemscript_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 027f2a0df8e86535d08ca3e766a178f90c813d2dd1d55b0166e82518efbffb18c0 OP_PUSHBYTES_33 03556a35844b517e2fc8216701e2e0a64dbcbe62ad420ac6dd73dc79e69efeb695 OP_PUSHBYTES_33 031ef21bd55171032b7aec21ec82932735fb986f1d4d8611feee62ab38acf4a6bb OP_PUSHNUM_3 OP_CHECKMULTISIG" + }, + ... + { + "txid": "43852d32c7ae6d362d446d090daa4d389f78ec77e6693f9248cd924dc0b1ecc3", + "vout": 1, + "prevout": { + "scriptpubkey": "a914a3aff5f5765f167c1582fd85517ddde83174118187", + "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 a3aff5f5765f167c1582fd85517ddde831741181 OP_EQUAL", + "scriptpubkey_type": "p2sh", + "scriptpubkey_address": "3GcWrnGFoNzbn1KaiP5czS5xPELdWcgDX2", + "value": 1719827 + }, + "scriptsig": "0047304402205f83d22a0476158aa0986682c96ce2b2dab26c814968dba62905cdfeef1b3ac7022059438a3439bb18bd49242010c8a276ea6f1810d523042e679fa6679d60e89e0201483045022100eb085df09e0fb4894090a5f39b9f2188392f7ac2847ed8255629baffc7371f170220120463b91d6c4bb8968fb3eda9012b88d13d8ca71de28e7a64b1dd88282ff144014c69522103650083cbc9cd1da1224e0780bce1ee8abd5150c5252defd0edeccd3521610b282102510ab30a6a97464ef0d61f71ec8b1d2325f12934ff15ba73579bfd0ac5f4fc1a2102985b3be77f56a9a29c5f68d3c893d6c4d76ec8c07792f0291d375c29b71ee2f853ae", + "scriptsig_asm": "OP_0 OP_PUSHBYTES_71 304402205f83d22a0476158aa0986682c96ce2b2dab26c814968dba62905cdfeef1b3ac7022059438a3439bb18bd49242010c8a276ea6f1810d523042e679fa6679d60e89e0201 OP_PUSHBYTES_72 3045022100eb085df09e0fb4894090a5f39b9f2188392f7ac2847ed8255629baffc7371f170220120463b91d6c4bb8968fb3eda9012b88d13d8ca71de28e7a64b1dd88282ff14401 OP_PUSHDATA1 522103650083cbc9cd1da1224e0780bce1ee8abd5150c5252defd0edeccd3521610b282102510ab30a6a97464ef0d61f71ec8b1d2325f12934ff15ba73579bfd0ac5f4fc1a2102985b3be77f56a9a29c5f68d3c893d6c4d76ec8c07792f0291d375c29b71ee2f853ae", + "is_coinbase": false, + "sequence": 4294967295, + "inner_redeemscript_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 03650083cbc9cd1da1224e0780bce1ee8abd5150c5252defd0edeccd3521610b28 OP_PUSHBYTES_33 02510ab30a6a97464ef0d61f71ec8b1d2325f12934ff15ba73579bfd0ac5f4fc1a OP_PUSHBYTES_33 02985b3be77f56a9a29c5f68d3c893d6c4d76ec8c07792f0291d375c29b71ee2f8 OP_PUSHNUM_3 OP_CHECKMULTISIG" + } + ], + "vout": [ + { + "scriptpubkey": "0014292fce548b228cd3df6334dd525fac62e7eb5f7a", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 292fce548b228cd3df6334dd525fac62e7eb5f7a", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "bc1q9yhuu4yty2xd8hmrxnw4yhavvtn7khm62uw38p", + "value": 57000 + }, + ... + { + "scriptpubkey": "0020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec", + "scriptpubkey_type": "v0_p2wsh", + "scriptpubkey_address": "bc1quhruqrghgcca950rvhtrg7cpd7u8k6svpzgzmrjy8xyukacl5lkq0r8l2d", + "value": 17343523 + } + ], + "size": 5514, + "weight": 22056, + "sigops": 208, + "fee": 44000, + "status": { + "confirmed": true, + "block_height": 875602, + "block_hash": "000000000000000000016c0639b6c1a34d6659c231aa2de5849ab3377ed75020", + "block_time": 1734704791 + }, + "order": 864069877, + "vsize": 5514, + "adjustedVsize": 5514, + "feePerVsize": 7.979688066739209, + "adjustedFeePerVsize": 7.979688066739209, + "effectiveFeePerVsize": 7.979688066739209, + "firstSeen": 1734704590, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 191567 + }, + "flags": 1099511659526 + } + ] +}` + }, + codeSampleLiquid: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "block-transactions": [ + { + "txid": "9d3ea0d131c45450c135d549b62032019bc47a80368e14edc72caf38f5a88033", + "version": 1, + "locktime": 0, + "vin": [ + { + "txid": "69da555a9c69788a3a081958457894e56b1ee6766bc72cecf881b1b4f327f78b", + "vout": 0, + "prevout": { + "scriptpubkey": "a914c9848245ae4f5d5934b5cbdfb79e04cdd337470b87", + "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 c9848245ae4f5d5934b5cbdfb79e04cdd337470b OP_EQUAL", + "scriptpubkey_type": "p2sh", + "scriptpubkey_address": "3L4YUynB4X44rJBY9CmiLMN8Wjti49JCYB", + "value": 24962957 + }, + "scriptsig": "0048304502210099219ee0cd5da341650078e3c63885b3cc2211069f2551cf436e0100f421e1760220349b4ec284255b458d6da539fa17314e8330459e0a653c254f775d4ec8f32b3f0147304402203a8353c5ee76a2e266432e5f993f882e05725297e64c0833cf44719f7dda8d3b022058a2f72e7739efd21657b4943cac60a0a3c749e712787f0e85726da4c3adcf8e014c695221027f2a0df8e86535d08ca3e766a178f90c813d2dd1d55b0166e82518efbffb18c02103556a35844b517e2fc8216701e2e0a64dbcbe62ad420ac6dd73dc79e69efeb69521031ef21bd55171032b7aec21ec82932735fb986f1d4d8611feee62ab38acf4a6bb53ae", + "scriptsig_asm": "OP_0 OP_PUSHBYTES_72 304502210099219ee0cd5da341650078e3c63885b3cc2211069f2551cf436e0100f421e1760220349b4ec284255b458d6da539fa17314e8330459e0a653c254f775d4ec8f32b3f01 OP_PUSHBYTES_71 304402203a8353c5ee76a2e266432e5f993f882e05725297e64c0833cf44719f7dda8d3b022058a2f72e7739efd21657b4943cac60a0a3c749e712787f0e85726da4c3adcf8e01 OP_PUSHDATA1 5221027f2a0df8e86535d08ca3e766a178f90c813d2dd1d55b0166e82518efbffb18c02103556a35844b517e2fc8216701e2e0a64dbcbe62ad420ac6dd73dc79e69efeb69521031ef21bd55171032b7aec21ec82932735fb986f1d4d8611feee62ab38acf4a6bb53ae", + "is_coinbase": false, + "sequence": 4294967295, + "inner_redeemscript_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 027f2a0df8e86535d08ca3e766a178f90c813d2dd1d55b0166e82518efbffb18c0 OP_PUSHBYTES_33 03556a35844b517e2fc8216701e2e0a64dbcbe62ad420ac6dd73dc79e69efeb695 OP_PUSHBYTES_33 031ef21bd55171032b7aec21ec82932735fb986f1d4d8611feee62ab38acf4a6bb OP_PUSHNUM_3 OP_CHECKMULTISIG" + }, + ... + { + "txid": "43852d32c7ae6d362d446d090daa4d389f78ec77e6693f9248cd924dc0b1ecc3", + "vout": 1, + "prevout": { + "scriptpubkey": "a914a3aff5f5765f167c1582fd85517ddde83174118187", + "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 a3aff5f5765f167c1582fd85517ddde831741181 OP_EQUAL", + "scriptpubkey_type": "p2sh", + "scriptpubkey_address": "3GcWrnGFoNzbn1KaiP5czS5xPELdWcgDX2", + "value": 1719827 + }, + "scriptsig": "0047304402205f83d22a0476158aa0986682c96ce2b2dab26c814968dba62905cdfeef1b3ac7022059438a3439bb18bd49242010c8a276ea6f1810d523042e679fa6679d60e89e0201483045022100eb085df09e0fb4894090a5f39b9f2188392f7ac2847ed8255629baffc7371f170220120463b91d6c4bb8968fb3eda9012b88d13d8ca71de28e7a64b1dd88282ff144014c69522103650083cbc9cd1da1224e0780bce1ee8abd5150c5252defd0edeccd3521610b282102510ab30a6a97464ef0d61f71ec8b1d2325f12934ff15ba73579bfd0ac5f4fc1a2102985b3be77f56a9a29c5f68d3c893d6c4d76ec8c07792f0291d375c29b71ee2f853ae", + "scriptsig_asm": "OP_0 OP_PUSHBYTES_71 304402205f83d22a0476158aa0986682c96ce2b2dab26c814968dba62905cdfeef1b3ac7022059438a3439bb18bd49242010c8a276ea6f1810d523042e679fa6679d60e89e0201 OP_PUSHBYTES_72 3045022100eb085df09e0fb4894090a5f39b9f2188392f7ac2847ed8255629baffc7371f170220120463b91d6c4bb8968fb3eda9012b88d13d8ca71de28e7a64b1dd88282ff14401 OP_PUSHDATA1 522103650083cbc9cd1da1224e0780bce1ee8abd5150c5252defd0edeccd3521610b282102510ab30a6a97464ef0d61f71ec8b1d2325f12934ff15ba73579bfd0ac5f4fc1a2102985b3be77f56a9a29c5f68d3c893d6c4d76ec8c07792f0291d375c29b71ee2f853ae", + "is_coinbase": false, + "sequence": 4294967295, + "inner_redeemscript_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 03650083cbc9cd1da1224e0780bce1ee8abd5150c5252defd0edeccd3521610b28 OP_PUSHBYTES_33 02510ab30a6a97464ef0d61f71ec8b1d2325f12934ff15ba73579bfd0ac5f4fc1a OP_PUSHBYTES_33 02985b3be77f56a9a29c5f68d3c893d6c4d76ec8c07792f0291d375c29b71ee2f8 OP_PUSHNUM_3 OP_CHECKMULTISIG" + } + ], + "vout": [ + { + "scriptpubkey": "0014292fce548b228cd3df6334dd525fac62e7eb5f7a", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 292fce548b228cd3df6334dd525fac62e7eb5f7a", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "bc1q9yhuu4yty2xd8hmrxnw4yhavvtn7khm62uw38p", + "value": 57000 + }, + ... + { + "scriptpubkey": "0020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec", + "scriptpubkey_type": "v0_p2wsh", + "scriptpubkey_address": "bc1quhruqrghgcca950rvhtrg7cpd7u8k6svpzgzmrjy8xyukacl5lkq0r8l2d", + "value": 17343523 + } + ], + "size": 5514, + "weight": 22056, + "sigops": 208, + "fee": 44000, + "status": { + "confirmed": true, + "block_height": 875602, + "block_hash": "000000000000000000016c0639b6c1a34d6659c231aa2de5849ab3377ed75020", + "block_time": 1734704791 + }, + "order": 864069877, + "vsize": 5514, + "adjustedVsize": 5514, + "feePerVsize": 7.979688066739209, + "adjustedFeePerVsize": 7.979688066739209, + "effectiveFeePerVsize": 7.979688066739209, + "firstSeen": 1734704590, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 191567 + }, + "flags": 1099511659526 + } + ] +}` + } + } + } + }, + { + type: "endpoint", + category: "addresses", + fragment: "track-addresses", + title: "Track Addresses", + description: { + default: "Subscribe to multiple addresses to receive live updates on new transactions having these addresses in input or output. Limits on the maximum number of tracked addresses apply. For higher tracking limits, consider upgrading to an enterprise sponsorship." + }, + payload: `{ + "track-addresses": [ + "bc1qeldw4mqns26wew8swgpkt3fs364w3ehs046w2f", + "bc1qjj09853tfpztjgrk4jeyzj4ml59fv9cmslv3c4gxxf57u0k3kxmqllx29y" + ] +}`, + showConditions: bitcoinNetworks.concat(liquidNetworks), + showJsExamples: false, + codeExample: { + default: { + codeTemplate: { + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "multi-address-transactions": { + "bc1qjj09853tfpztjgrk4jeyzj4ml59fv9cmslv3c4gxxf57u0k3kxmqllx29y": { + "mempool": [], + "confirmed": [ + { + "txid": "1e4764f908f19b74284a889478b95d013c1bd36dc832dcb7eb36fe1801fed404", + "version": 2, + "locktime": 875625, + "vin": [ + { + "txid": "ce361fed5996aec6d440556383164e9e4e5b8be8c2a213c4b36ae711efda3b3f", + "vout": 1, + "prevout": { + "scriptpubkey": "0014257ba1ebc987831dbe8ee560419282483bf68588", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 257ba1ebc987831dbe8ee560419282483bf68588", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "bc1qy4a6r67fs7p3m05wu4syry5zfqaldpvg8vsqzz", + "value": 1831200 + }, + "scriptsig": "", + "scriptsig_asm": "", + "witness": [ + "3044022028363f66fe74bdddf46d204cbf9844d4ef99d6fcb801f93f3ea1666ff51514340220058eb99790dd002323bd12afa0b62903cf72465d48c40cb11366dfa4eebbd87a01", + "020e625e13a81995f29ee828e31500b8454bd0b115f84dfa07d994eecd733efffa" + ], + "is_coinbase": false, + "sequence": 4294967294 + }, + ... + ], + "vout": [ + { + "scriptpubkey": "0020949e53d22b4844b92076acb2414abbfd0a96171b87d91c55063269ee3ed1b1b6", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 949e53d22b4844b92076acb2414abbfd0a96171b87d91c55063269ee3ed1b1b6", + "scriptpubkey_type": "v0_p2wsh", + "scriptpubkey_address": "bc1qjj09853tfpztjgrk4jeyzj4ml59fv9cmslv3c4gxxf57u0k3kxmqllx29y", + "value": 2546637 + } + ], + "size": 351, + "weight": 756, + "sigops": 2, + "fee": 4206, + "status": { + "confirmed": true, + "block_height": 875626, + "block_hash": "0000000000000000000086de1f4815ff0f7f0411d846301c5efa1e437130dc22", + "block_time": 1734720142 + }, + "order": 81067521, + "vsize": 189, + "adjustedVsize": 189, + "feePerVsize": 22.253968253968253, + "adjustedFeePerVsize": 22.253968253968253, + "effectiveFeePerVsize": 22.253968253968253, + "firstSeen": 1734719830, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 134866.5 + }, + "flags": 1099511640074 + } + ], + "removed": [] + } + } +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "multi-address-transactions": { + "bc1qjj09853tfpztjgrk4jeyzj4ml59fv9cmslv3c4gxxf57u0k3kxmqllx29y": { + "mempool": [], + "confirmed": [ + { + "txid": "1e4764f908f19b74284a889478b95d013c1bd36dc832dcb7eb36fe1801fed404", + "version": 2, + "locktime": 875625, + "vin": [ + { + "txid": "ce361fed5996aec6d440556383164e9e4e5b8be8c2a213c4b36ae711efda3b3f", + "vout": 1, + "prevout": { + "scriptpubkey": "0014257ba1ebc987831dbe8ee560419282483bf68588", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 257ba1ebc987831dbe8ee560419282483bf68588", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "bc1qy4a6r67fs7p3m05wu4syry5zfqaldpvg8vsqzz", + "value": 1831200 + }, + "scriptsig": "", + "scriptsig_asm": "", + "witness": [ + "3044022028363f66fe74bdddf46d204cbf9844d4ef99d6fcb801f93f3ea1666ff51514340220058eb99790dd002323bd12afa0b62903cf72465d48c40cb11366dfa4eebbd87a01", + "020e625e13a81995f29ee828e31500b8454bd0b115f84dfa07d994eecd733efffa" + ], + "is_coinbase": false, + "sequence": 4294967294 + }, + ... + ], + "vout": [ + { + "scriptpubkey": "0020949e53d22b4844b92076acb2414abbfd0a96171b87d91c55063269ee3ed1b1b6", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 949e53d22b4844b92076acb2414abbfd0a96171b87d91c55063269ee3ed1b1b6", + "scriptpubkey_type": "v0_p2wsh", + "scriptpubkey_address": "bc1qjj09853tfpztjgrk4jeyzj4ml59fv9cmslv3c4gxxf57u0k3kxmqllx29y", + "value": 2546637 + } + ], + "size": 351, + "weight": 756, + "sigops": 2, + "fee": 4206, + "status": { + "confirmed": true, + "block_height": 875626, + "block_hash": "0000000000000000000086de1f4815ff0f7f0411d846301c5efa1e437130dc22", + "block_time": 1734720142 + }, + "order": 81067521, + "vsize": 189, + "adjustedVsize": 189, + "feePerVsize": 22.253968253968253, + "adjustedFeePerVsize": 22.253968253968253, + "effectiveFeePerVsize": 22.253968253968253, + "firstSeen": 1734719830, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 134866.5 + }, + "flags": 1099511640074 + } + ], + "removed": [] + } + } +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "multi-address-transactions": { + "bc1qjj09853tfpztjgrk4jeyzj4ml59fv9cmslv3c4gxxf57u0k3kxmqllx29y": { + "mempool": [], + "confirmed": [ + { + "txid": "1e4764f908f19b74284a889478b95d013c1bd36dc832dcb7eb36fe1801fed404", + "version": 2, + "locktime": 875625, + "vin": [ + { + "txid": "ce361fed5996aec6d440556383164e9e4e5b8be8c2a213c4b36ae711efda3b3f", + "vout": 1, + "prevout": { + "scriptpubkey": "0014257ba1ebc987831dbe8ee560419282483bf68588", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 257ba1ebc987831dbe8ee560419282483bf68588", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "bc1qy4a6r67fs7p3m05wu4syry5zfqaldpvg8vsqzz", + "value": 1831200 + }, + "scriptsig": "", + "scriptsig_asm": "", + "witness": [ + "3044022028363f66fe74bdddf46d204cbf9844d4ef99d6fcb801f93f3ea1666ff51514340220058eb99790dd002323bd12afa0b62903cf72465d48c40cb11366dfa4eebbd87a01", + "020e625e13a81995f29ee828e31500b8454bd0b115f84dfa07d994eecd733efffa" + ], + "is_coinbase": false, + "sequence": 4294967294 + }, + ... + ], + "vout": [ + { + "scriptpubkey": "0020949e53d22b4844b92076acb2414abbfd0a96171b87d91c55063269ee3ed1b1b6", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 949e53d22b4844b92076acb2414abbfd0a96171b87d91c55063269ee3ed1b1b6", + "scriptpubkey_type": "v0_p2wsh", + "scriptpubkey_address": "bc1qjj09853tfpztjgrk4jeyzj4ml59fv9cmslv3c4gxxf57u0k3kxmqllx29y", + "value": 2546637 + } + ], + "size": 351, + "weight": 756, + "sigops": 2, + "fee": 4206, + "status": { + "confirmed": true, + "block_height": 875626, + "block_hash": "0000000000000000000086de1f4815ff0f7f0411d846301c5efa1e437130dc22", + "block_time": 1734720142 + }, + "order": 81067521, + "vsize": 189, + "adjustedVsize": 189, + "feePerVsize": 22.253968253968253, + "adjustedFeePerVsize": 22.253968253968253, + "effectiveFeePerVsize": 22.253968253968253, + "firstSeen": 1734719830, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 134866.5 + }, + "flags": 1099511640074 + } + ], + "removed": [] + } + } +}` + }, + codeSampleLiquid: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "multi-address-transactions": { + "ex1qzq0h0wvnnh9xpd508fzxaft0nu9wjmdvzalu6f": { + "mempool": [], + "confirmed": [ + { + "txid": "d61ad73b64895ccabd32816643554c676891bdb52da0fba2b37079e04c4c4b2c", + "version": 2, + "locktime": 3171528, + "vin": [ + { + "txid": "4847a0627952a0bcad6c8947d46a0e5b13eefbcfbf76246ea16a1a7c82bcc49b", + "vout": 2, + "prevout": { + "scriptpubkey": "00144d72c2967e1a581c0e71e82d65e99523a9149d02", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 4d72c2967e1a581c0e71e82d65e99523a9149d02", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "ex1qf4ev99n7rfvpcrn3aqkkt6v4yw53f8gznv9paa", + "valuecommitment": "09af208bbc0b9809aff4368dee81f74f178f77f844e7dfc5d70615bc757fa8b2f9", + "assetcommitment": "0a2ca17c42fadd887373c371e44cf49c6cd64c3081e23eef3275bdace0b8c674b5" + }, + "scriptsig": "", + "scriptsig_asm": "", + "witness": [ + "30440220653c6e1bd3de5bd9a56cbb6eb246834724667a5c5d12dc07107edc7c72bd6634022008d1f770dc9ba624bb250bba3a5254aa633f01a9bcb2a85aedc5b251e338b7b301", + "03fb2f0245e19f9e886fce54894558bbbcf50bf9576245e60a4c9780f7447eaf22" + ], + "is_coinbase": false, + "sequence": 4294967294, + "is_pegin": false + }, + { + "txid": "ea5f690853ece5549807862a153357092c4f7dbe10886b86b84f87a3201dd8dc", + "vout": 0, + "prevout": { + "scriptpubkey": "0014a5996021b4001325b1aa85c3bf400516855a6e05", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 a5996021b4001325b1aa85c3bf400516855a6e05", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "ex1q5kvkqgd5qqfjtvd2shpm7sq9z6z45ms9ma7ywz", + "valuecommitment": "09f36fde0f51390cdf2ee6830b3c696569c3f9c5855ce26bd4f6d0280a83b86ecf", + "assetcommitment": "0bfcd8fcfaebfa41b596a89aa55fbf2eaa8c383ec71e8d9d0d461ea645d8d1bc45" + }, + "scriptsig": "", + "scriptsig_asm": "", + "witness": [ + "30440220033208f3e37c35009ba00472a67899222afbd5cc12b0d3906d2eec6f50a058510220607d4c5de43459e38158ee1cbe5e6a74c155041cecdbf961a077a952ee1e543601", + "0394d6ecb2f5db9fdeb0f7ac5301ea148704fc6986fdb8181bddc1d2eec9e99c32" + ], + "is_coinbase": false, + "sequence": 4294967294, + "is_pegin": false + }, + { + "txid": "23e63b888d5da3ce1193bb4a74a0762d78904cfa7a6307ff47e91054d961208b", + "vout": 2, + "prevout": { + "scriptpubkey": "00144990783e871e57fa2499f00c5f6f4ddc2602e7c8", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 4990783e871e57fa2499f00c5f6f4ddc2602e7c8", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "ex1qfxg8s058retl5fye7qx97m6dmsnq9e7gq0dcee", + "valuecommitment": "0816440695f0c47ce471c7e10a93d36aee4554b46ed269bfa8390dd9db69409537", + "assetcommitment": "0a5a0eb7cab779cb6ce5d6517c73e244075eea15fbd54a7beb34710862aef58359" + }, + "scriptsig": "", + "scriptsig_asm": "", + "witness": [ + "3044022051e5482a486f55cfd5ae25062b0252e13de9bfa11a9c7e5f608ac6e03c62dc8902201d2b47a27fc07999973ec44e4569b4b3fcc338b1c3977173cfeab9cccde0b3e301", + "025991a68daafc95494019c228855999db8f19c872fd3f58bac6ff149db7b53cff" + ], + "is_coinbase": false, + "sequence": 4294967294, + "is_pegin": false + }, + { + "txid": "2bbeb9440d3c08a1d3cd9acf5959ee740a6a64ffcaa4aa2b43e30026a2a40334", + "vout": 2, + "prevout": { + "scriptpubkey": "00144d72c2967e1a581c0e71e82d65e99523a9149d02", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 4d72c2967e1a581c0e71e82d65e99523a9149d02", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "ex1qf4ev99n7rfvpcrn3aqkkt6v4yw53f8gznv9paa", + "valuecommitment": "09d0c574d61d50065a2e398fb7252315b65176bf97de1d180d337f3aadfaa0e53e", + "assetcommitment": "0b82ede6b9a6cb9505a7f6bcc76f72caa3228de193debf4e586d60baebeaef0ab5" + }, + "scriptsig": "", + "scriptsig_asm": "", + "witness": [ + "3044022033ab9ea81a21b0f917792097ed69ab3724957a5e5d3a0430a0b2a16e0a74d8750220202859dc7e53998f5dc4b424321b7a711e3ff6517422a099504b337e30ec8acb01", + "03fb2f0245e19f9e886fce54894558bbbcf50bf9576245e60a4c9780f7447eaf22" + ], + "is_coinbase": false, + "sequence": 4294967294, + "is_pegin": false + } + ], + "vout": [ + { + "scriptpubkey": "a914e185d1192f34d55ba3fbd15408168f339683d80287", + "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 e185d1192f34d55ba3fbd15408168f339683d802 OP_EQUAL", + "scriptpubkey_type": "p2sh", + "scriptpubkey_address": "H3jyk9ipDU5efhHW9n52xCY78HNFAQTy78", + "valuecommitment": "08f676101a27d784f1f89765ff33ad5a1e95ab2081da76b29ef97bdfaf309e1318", + "assetcommitment": "0a115106f540daae5a0a7cf66dcf07a69dc2faffb917e82f340bcdfc7da143228b" + }, + { + "scriptpubkey": "0014101f77b9939dca60b68f3a446ea56f9f0ae96dac", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 101f77b9939dca60b68f3a446ea56f9f0ae96dac", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "ex1qzq0h0wvnnh9xpd508fzxaft0nu9wjmdvzalu6f", + "valuecommitment": "09bec5886710680e125b52ac99f6aee452984847cddc57abaa96eb8cc360f80104", + "assetcommitment": "0adfd85f43988146c1878e98bc5e6206f368280cb03760c37e86ec0bd39005d0cd" + }, + { + "scriptpubkey": "00140fe27684e78285d508073f2b8a3a6c884515d1a9", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 0fe27684e78285d508073f2b8a3a6c884515d1a9", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "ex1qpl38dp88s2za2zq88u4c5wnv3pz3t5dfha22k9", + "valuecommitment": "08bef8c28296cf050802c943d46aa539d2f5280e9b9471db928746480815cf5457", + "assetcommitment": "0a5a032f72df6fba7f1acd7230f44cdf41ce27926e48e262ffdfea18efd19e0439" + }, + { + "scriptpubkey": "", + "scriptpubkey_asm": "", + "scriptpubkey_type": "fee", + "value": 394, + "asset": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d" + } + ], + "size": 13955, + "weight": 15713, + "sigops": 0, + "fee": 394, + "status": { + "confirmed": true, + "block_height": 3171530, + "block_hash": "400270631b0f66d70cd6a045f36bb3f37c9076688fd496669d5da2a7245392d9", + "block_time": 1734720368 + }, + "order": 743132236, + "vsize": 3929, + "adjustedVsize": 3928.25, + "feePerVsize": 0.10029911538216763, + "adjustedFeePerVsize": 0.10029911538216763, + "effectiveFeePerVsize": 0.10027996945787732, + "firstSeen": 1734720314, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 6972.5 + }, + "flags": 1099511633962, + "cpfpChecked": true, + "cpfpUpdated": 1734720355424 + } + ], + "removed": [] + } + } +}` + } + } + } + }, + { + type: "category", + category: "transactions", + fragment: "transactions", + title: "Transactions", + showConditions: bitcoinNetworks.concat(liquidNetworks) + }, + { + type: "endpoint", + category: "transactions", + fragment: "track-tx", + title: "Track Transaction", + description: { + default: "Subscribe to a transaction to receive live updates on its confirmation status and position in the mempool." + }, + payload: '{ "track-tx": "8a4666c6d22ce74fa47e1c4fdb09af556a234cc6a606539a75caf66ba44a2d07" }', + showConditions: bitcoinNetworks.concat(liquidNetworks), + showJsExamples: false, + codeExample: { + default: { + codeTemplate: { + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "txPosition": { + "txid": [ + "8a4666c6d22ce74fa47e1c4fdb09af556a234cc6a606539a75caf66ba44a2d07" + ], + "position": { + "block": 0, + "vsize": 726868 + }, + "cpfp": { + "ancestors": [ + { + "txid": "d509a6b8f36149588f9f48dc25fa5e37fc00dee781aed6da1113066c56f04879", + "fee": 605, + "weight": 520 + } + ], + "bestDescendant": null, + "descendants": [ + { + "txid": "28d3c592a9a8103d53c784aa539908f4dc5f9c463e179f0eae5dc5f349bdb00f", + "fee": 2501, + "weight": 816 + } + ], + "effectiveFeePerVsize": 5.12063778580024, + "sigops": 0, + "adjustedVsize": 130 + } + } +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "txPosition": { + "txid": [ + "8a4666c6d22ce74fa47e1c4fdb09af556a234cc6a606539a75caf66ba44a2d07" + ], + "position": { + "block": 0, + "vsize": 726868 + }, + "cpfp": { + "ancestors": [ + { + "txid": "d509a6b8f36149588f9f48dc25fa5e37fc00dee781aed6da1113066c56f04879", + "fee": 605, + "weight": 520 + } + ], + "bestDescendant": null, + "descendants": [ + { + "txid": "28d3c592a9a8103d53c784aa539908f4dc5f9c463e179f0eae5dc5f349bdb00f", + "fee": 2501, + "weight": 816 + } + ], + "effectiveFeePerVsize": 5.12063778580024, + "sigops": 0, + "adjustedVsize": 130 + } + } +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "txPosition": { + "txid": [ + "8a4666c6d22ce74fa47e1c4fdb09af556a234cc6a606539a75caf66ba44a2d07" + ], + "position": { + "block": 0, + "vsize": 726868 + }, + "cpfp": { + "ancestors": [ + { + "txid": "d509a6b8f36149588f9f48dc25fa5e37fc00dee781aed6da1113066c56f04879", + "fee": 605, + "weight": 520 + } + ], + "bestDescendant": null, + "descendants": [ + { + "txid": "28d3c592a9a8103d53c784aa539908f4dc5f9c463e179f0eae5dc5f349bdb00f", + "fee": 2501, + "weight": 816 + } + ], + "effectiveFeePerVsize": 5.12063778580024, + "sigops": 0, + "adjustedVsize": 130 + } + } +}` + }, + codeSampleLiquid: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "txPosition": { + "txid": [ + "8a4666c6d22ce74fa47e1c4fdb09af556a234cc6a606539a75caf66ba44a2d07" + ], + "position": { + "block": 0, + "vsize": 726868 + }, + "cpfp": { + "ancestors": [ + { + "txid": "d509a6b8f36149588f9f48dc25fa5e37fc00dee781aed6da1113066c56f04879", + "fee": 605, + "weight": 520 + } + ], + "bestDescendant": null, + "descendants": [ + { + "txid": "28d3c592a9a8103d53c784aa539908f4dc5f9c463e179f0eae5dc5f349bdb00f", + "fee": 2501, + "weight": 816 + } + ], + "effectiveFeePerVsize": 5.12063778580024, + "sigops": 0, + "adjustedVsize": 130 + } + } +}` + } + } + } + }, + { + type: "endpoint", + category: "transactions", + fragment: "track-txs", + title: "Track Transactions", + description: { + default: "Subscribe to multiple transactions to receive live updates on their status and position in the mempool. Limits on the maximum number of tracked addresses apply. For higher tracking limits, consider upgrading to an enterprise sponsorship." + }, + payload: `{ + "track-txs": [ + "8a4666c6d22ce74fa47e1c4fdb09af556a234cc6a606539a75caf66ba44a2d07", + "941df06064c290b4627e92bdbf3bff7c0e97aab33e273c2a20404f9cfd21b607" + ] + }`, showConditions: bitcoinNetworks.concat(liquidNetworks), + showJsExamples: false, + codeExample: { + default: { + codeTemplate: { + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "tracked-txs": { + "8a4666c6d22ce74fa47e1c4fdb09af556a234cc6a606539a75caf66ba44a2d07": { + "position": { + "block": 0, + "vsize": 434494 + }, + "cpfp": { + "ancestors": [ + { + "txid": "d509a6b8f36149588f9f48dc25fa5e37fc00dee781aed6da1113066c56f04879", + "fee": 605, + "weight": 520 + } + ], + "bestDescendant": null, + "descendants": [ + { + "txid": "28d3c592a9a8103d53c784aa539908f4dc5f9c463e179f0eae5dc5f349bdb00f", + "fee": 2501, + "weight": 816 + } + ], + "effectiveFeePerVsize": 5.12063778580024, + "sigops": 0, + "adjustedVsize": 130 + } + }, + "941df06064c290b4627e92bdbf3bff7c0e97aab33e273c2a20404f9cfd21b607": { + "position": { + "block": 2, + "vsize": 932479.5 + } + } + } +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "tracked-txs": { + "8a4666c6d22ce74fa47e1c4fdb09af556a234cc6a606539a75caf66ba44a2d07": { + "position": { + "block": 0, + "vsize": 434494 + }, + "cpfp": { + "ancestors": [ + { + "txid": "d509a6b8f36149588f9f48dc25fa5e37fc00dee781aed6da1113066c56f04879", + "fee": 605, + "weight": 520 + } + ], + "bestDescendant": null, + "descendants": [ + { + "txid": "28d3c592a9a8103d53c784aa539908f4dc5f9c463e179f0eae5dc5f349bdb00f", + "fee": 2501, + "weight": 816 + } + ], + "effectiveFeePerVsize": 5.12063778580024, + "sigops": 0, + "adjustedVsize": 130 + } + }, + "941df06064c290b4627e92bdbf3bff7c0e97aab33e273c2a20404f9cfd21b607": { + "position": { + "block": 2, + "vsize": 932479.5 + } + } + } +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "tracked-txs": { + "8a4666c6d22ce74fa47e1c4fdb09af556a234cc6a606539a75caf66ba44a2d07": { + "position": { + "block": 0, + "vsize": 434494 + }, + "cpfp": { + "ancestors": [ + { + "txid": "d509a6b8f36149588f9f48dc25fa5e37fc00dee781aed6da1113066c56f04879", + "fee": 605, + "weight": 520 + } + ], + "bestDescendant": null, + "descendants": [ + { + "txid": "28d3c592a9a8103d53c784aa539908f4dc5f9c463e179f0eae5dc5f349bdb00f", + "fee": 2501, + "weight": 816 + } + ], + "effectiveFeePerVsize": 5.12063778580024, + "sigops": 0, + "adjustedVsize": 130 + } + }, + "941df06064c290b4627e92bdbf3bff7c0e97aab33e273c2a20404f9cfd21b607": { + "position": { + "block": 2, + "vsize": 932479.5 + } + } + } +}` + }, + codeSampleLiquid: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "tracked-txs": { + "8a4666c6d22ce74fa47e1c4fdb09af556a234cc6a606539a75caf66ba44a2d07": { + "position": { + "block": 0, + "vsize": 434494 + }, + "cpfp": { + "ancestors": [ + { + "txid": "d509a6b8f36149588f9f48dc25fa5e37fc00dee781aed6da1113066c56f04879", + "fee": 605, + "weight": 520 + } + ], + "bestDescendant": null, + "descendants": [ + { + "txid": "28d3c592a9a8103d53c784aa539908f4dc5f9c463e179f0eae5dc5f349bdb00f", + "fee": 2501, + "weight": 816 + } + ], + "effectiveFeePerVsize": 5.12063778580024, + "sigops": 0, + "adjustedVsize": 130 + } + }, + "941df06064c290b4627e92bdbf3bff7c0e97aab33e273c2a20404f9cfd21b607": { + "position": { + "block": 2, + "vsize": 932479.5 + } + } + } +}` + } + } + } + }, + { + type: "category", + category: "mempool", + fragment: "mempool", + title: "Mempool", + showConditions: bitcoinNetworks.concat(liquidNetworks) + }, + { + type: "endpoint", + category: "mempool", + fragment: "track-mempool", + title: "Track Mempool", + description: { + default: "Subscribe to new mempool events, such as new transactions entering the mempool. Available fields: added, removed, mined, replaced.
Because this is potentially a lot of data, consider using the track-mempool-txids endpoint described below instead, or upgrade to an enterprise sponsorship." + }, + payload: '{ "track-mempool": true }', + showConditions: bitcoinNetworks.concat(liquidNetworks), + showJsExamples: false, + codeExample: { + default: { + codeTemplate: { + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "mempool-transactions": { + "sequence": 81419, + "added": [ + { + "txid": "6229c0784bc776be22a5ee84e0e3d9b8f9e17843f079a8444b03bdc98b77d229", + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "b4b324e3bff7ee0a7e664e8c03df1fe3a0bd53e5685ea6b10abb5f89ba1b2ead", + "vout": 5, + "prevout": { + "scriptpubkey": "76a914b54afb58f0faa9d1bde2ed755bc56ef1e4a4e24188ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 b54afb58f0faa9d1bde2ed755bc56ef1e4a4e241 OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "1HXb8YtsgBhFWdYezjd6bt7Dw4UGKyZo54", + "value": 17000 + }, + "scriptsig": "4830450221008e9b91aae7b4705841c97dc99d6ab233f10ff9b97d7c139be08634d2f0f5f66f02205d67eae8c830ed0979e169403d13c0f43efd78edbb9a344390245f5a83649404012103cf9fad8b202384de9ef010129a62b8249920a6205fe53cc0efbea9eb0db595e7", + "scriptsig_asm": "OP_PUSHBYTES_72 30450221008e9b91aae7b4705841c97dc99d6ab233f10ff9b97d7c139be08634d2f0f5f66f02205d67eae8c830ed0979e169403d13c0f43efd78edbb9a344390245f5a8364940401 OP_PUSHBYTES_33 03cf9fad8b202384de9ef010129a62b8249920a6205fe53cc0efbea9eb0db595e7", + "is_coinbase": false, + "sequence": 4294967293 + } + ], + "vout": [ + { + "scriptpubkey": "76a91401603bd82a5d5a6e8c6df5d9ae662b9fc5db60f288ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 01603bd82a5d5a6e8c6df5d9ae662b9fc5db60f2 OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "18GxdcLgNtRUc8v5TNJtPnvoi8jMVWxvb", + "value": 10419 + }, + { + "scriptpubkey": "76a914338ad842d236486627834bf9f5e182c7a8aa937188ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 338ad842d236486627834bf9f5e182c7a8aa9371 OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "15hXntT6oUKNhtk4FWvuGPQJDX47wpbAaa", + "value": 5396 + } + ], + "size": 226, + "weight": 904, + "sigops": 8, + "fee": 1185, + "status": { + "confirmed": false + }, + "order": 701659019, + "vsize": 226, + "adjustedVsize": 226, + "feePerVsize": 5.243362831858407, + "adjustedFeePerVsize": 5.243362831858407, + "effectiveFeePerVsize": 5.243362831858407, + "firstSeen": 1734893382, + "uid": 429139, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 125270 + }, + "flags": 1099511628809 + }, + ... + ], + "removed": [], + "mined": [], + "replaced": [] + } +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "mempool-transactions": { + "sequence": 81419, + "added": [ + { + "txid": "6229c0784bc776be22a5ee84e0e3d9b8f9e17843f079a8444b03bdc98b77d229", + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "b4b324e3bff7ee0a7e664e8c03df1fe3a0bd53e5685ea6b10abb5f89ba1b2ead", + "vout": 5, + "prevout": { + "scriptpubkey": "76a914b54afb58f0faa9d1bde2ed755bc56ef1e4a4e24188ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 b54afb58f0faa9d1bde2ed755bc56ef1e4a4e241 OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "1HXb8YtsgBhFWdYezjd6bt7Dw4UGKyZo54", + "value": 17000 + }, + "scriptsig": "4830450221008e9b91aae7b4705841c97dc99d6ab233f10ff9b97d7c139be08634d2f0f5f66f02205d67eae8c830ed0979e169403d13c0f43efd78edbb9a344390245f5a83649404012103cf9fad8b202384de9ef010129a62b8249920a6205fe53cc0efbea9eb0db595e7", + "scriptsig_asm": "OP_PUSHBYTES_72 30450221008e9b91aae7b4705841c97dc99d6ab233f10ff9b97d7c139be08634d2f0f5f66f02205d67eae8c830ed0979e169403d13c0f43efd78edbb9a344390245f5a8364940401 OP_PUSHBYTES_33 03cf9fad8b202384de9ef010129a62b8249920a6205fe53cc0efbea9eb0db595e7", + "is_coinbase": false, + "sequence": 4294967293 + } + ], + "vout": [ + { + "scriptpubkey": "76a91401603bd82a5d5a6e8c6df5d9ae662b9fc5db60f288ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 01603bd82a5d5a6e8c6df5d9ae662b9fc5db60f2 OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "18GxdcLgNtRUc8v5TNJtPnvoi8jMVWxvb", + "value": 10419 + }, + { + "scriptpubkey": "76a914338ad842d236486627834bf9f5e182c7a8aa937188ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 338ad842d236486627834bf9f5e182c7a8aa9371 OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "15hXntT6oUKNhtk4FWvuGPQJDX47wpbAaa", + "value": 5396 + } + ], + "size": 226, + "weight": 904, + "sigops": 8, + "fee": 1185, + "status": { + "confirmed": false + }, + "order": 701659019, + "vsize": 226, + "adjustedVsize": 226, + "feePerVsize": 5.243362831858407, + "adjustedFeePerVsize": 5.243362831858407, + "effectiveFeePerVsize": 5.243362831858407, + "firstSeen": 1734893382, + "uid": 429139, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 125270 + }, + "flags": 1099511628809 + }, + ... + ], + "removed": [], + "mined": [], + "replaced": [] + } +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "mempool-transactions": { + "sequence": 81419, + "added": [ + { + "txid": "6229c0784bc776be22a5ee84e0e3d9b8f9e17843f079a8444b03bdc98b77d229", + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "b4b324e3bff7ee0a7e664e8c03df1fe3a0bd53e5685ea6b10abb5f89ba1b2ead", + "vout": 5, + "prevout": { + "scriptpubkey": "76a914b54afb58f0faa9d1bde2ed755bc56ef1e4a4e24188ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 b54afb58f0faa9d1bde2ed755bc56ef1e4a4e241 OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "1HXb8YtsgBhFWdYezjd6bt7Dw4UGKyZo54", + "value": 17000 + }, + "scriptsig": "4830450221008e9b91aae7b4705841c97dc99d6ab233f10ff9b97d7c139be08634d2f0f5f66f02205d67eae8c830ed0979e169403d13c0f43efd78edbb9a344390245f5a83649404012103cf9fad8b202384de9ef010129a62b8249920a6205fe53cc0efbea9eb0db595e7", + "scriptsig_asm": "OP_PUSHBYTES_72 30450221008e9b91aae7b4705841c97dc99d6ab233f10ff9b97d7c139be08634d2f0f5f66f02205d67eae8c830ed0979e169403d13c0f43efd78edbb9a344390245f5a8364940401 OP_PUSHBYTES_33 03cf9fad8b202384de9ef010129a62b8249920a6205fe53cc0efbea9eb0db595e7", + "is_coinbase": false, + "sequence": 4294967293 + } + ], + "vout": [ + { + "scriptpubkey": "76a91401603bd82a5d5a6e8c6df5d9ae662b9fc5db60f288ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 01603bd82a5d5a6e8c6df5d9ae662b9fc5db60f2 OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "18GxdcLgNtRUc8v5TNJtPnvoi8jMVWxvb", + "value": 10419 + }, + { + "scriptpubkey": "76a914338ad842d236486627834bf9f5e182c7a8aa937188ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 338ad842d236486627834bf9f5e182c7a8aa9371 OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "15hXntT6oUKNhtk4FWvuGPQJDX47wpbAaa", + "value": 5396 + } + ], + "size": 226, + "weight": 904, + "sigops": 8, + "fee": 1185, + "status": { + "confirmed": false + }, + "order": 701659019, + "vsize": 226, + "adjustedVsize": 226, + "feePerVsize": 5.243362831858407, + "adjustedFeePerVsize": 5.243362831858407, + "effectiveFeePerVsize": 5.243362831858407, + "firstSeen": 1734893382, + "uid": 429139, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 125270 + }, + "flags": 1099511628809 + }, + ... + ], + "removed": [], + "mined": [], + "replaced": [] + } +}` + }, + codeSampleLiquid: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "mempool-transactions": { + "sequence": 81419, + "added": [ + { + "txid": "6229c0784bc776be22a5ee84e0e3d9b8f9e17843f079a8444b03bdc98b77d229", + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "b4b324e3bff7ee0a7e664e8c03df1fe3a0bd53e5685ea6b10abb5f89ba1b2ead", + "vout": 5, + "prevout": { + "scriptpubkey": "76a914b54afb58f0faa9d1bde2ed755bc56ef1e4a4e24188ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 b54afb58f0faa9d1bde2ed755bc56ef1e4a4e241 OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "1HXb8YtsgBhFWdYezjd6bt7Dw4UGKyZo54", + "value": 17000 + }, + "scriptsig": "4830450221008e9b91aae7b4705841c97dc99d6ab233f10ff9b97d7c139be08634d2f0f5f66f02205d67eae8c830ed0979e169403d13c0f43efd78edbb9a344390245f5a83649404012103cf9fad8b202384de9ef010129a62b8249920a6205fe53cc0efbea9eb0db595e7", + "scriptsig_asm": "OP_PUSHBYTES_72 30450221008e9b91aae7b4705841c97dc99d6ab233f10ff9b97d7c139be08634d2f0f5f66f02205d67eae8c830ed0979e169403d13c0f43efd78edbb9a344390245f5a8364940401 OP_PUSHBYTES_33 03cf9fad8b202384de9ef010129a62b8249920a6205fe53cc0efbea9eb0db595e7", + "is_coinbase": false, + "sequence": 4294967293 + } + ], + "vout": [ + { + "scriptpubkey": "76a91401603bd82a5d5a6e8c6df5d9ae662b9fc5db60f288ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 01603bd82a5d5a6e8c6df5d9ae662b9fc5db60f2 OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "18GxdcLgNtRUc8v5TNJtPnvoi8jMVWxvb", + "value": 10419 + }, + { + "scriptpubkey": "76a914338ad842d236486627834bf9f5e182c7a8aa937188ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 338ad842d236486627834bf9f5e182c7a8aa9371 OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "15hXntT6oUKNhtk4FWvuGPQJDX47wpbAaa", + "value": 5396 + } + ], + "size": 226, + "weight": 904, + "sigops": 8, + "fee": 1185, + "status": { + "confirmed": false + }, + "order": 701659019, + "vsize": 226, + "adjustedVsize": 226, + "feePerVsize": 5.243362831858407, + "adjustedFeePerVsize": 5.243362831858407, + "effectiveFeePerVsize": 5.243362831858407, + "firstSeen": 1734893382, + "uid": 429139, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 125270 + }, + "flags": 1099511628809 + }, + ... + ], + "removed": [], + "mined": [], + "replaced": [] + } +}` + }, + } + } + }, + { + type: "endpoint", + category: "mempool", + fragment: "track-mempool-txids", + title: "Track Mempool Txids", + description: { + default: "Low-bandwith substitute to the above command track-mempool: subscribe to new mempool events, such as new transactions entering the mempool, but only transaction IDs are returned to save bandwith. Available fields: added, removed, mined, replaced." + }, + payload: '{ "track-mempool-txids": true }', + showConditions: bitcoinNetworks.concat(liquidNetworks), + showJsExamples: false, + codeExample: { + default: { + codeTemplate: { + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "mempool-txids": { + "sequence": 79919, + "added": [ + "4bbb648ab194aaaf9188bccc6efcdcbb59c8485115a7384972c8287782206a0f", + "f7883f3784829d1e741e696bdceec488eeb53fe0b69b0eca574ac9f2e7e8e117", + "784e8e3b182c29798660bf42befb5c6479148c7d90c0d6eea032b89418e7cc3b", + "d3920a7be05269d859bd89b08a6546dc6d6dd523dbc5f7b62b9c0c5eedc43292", + "de6078d584cb5f4a27c3f0bb3d8bbb16b3d5f8303237391f390d0ee9e84d0099", + "39fcbd6e0ec0ad49405f19c72bb033f578147181b77dbe47044f80b0b7604ab5", + "47ed060004fab3fb5fa4885008aa2cadbe3335655f1303231abfe89b4b0c9bd9" + ], + "removed": [], + "mined": [], + "replaced": [] + } +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "mempool-txids": { + "sequence": 79919, + "added": [ + "4bbb648ab194aaaf9188bccc6efcdcbb59c8485115a7384972c8287782206a0f", + "f7883f3784829d1e741e696bdceec488eeb53fe0b69b0eca574ac9f2e7e8e117", + "784e8e3b182c29798660bf42befb5c6479148c7d90c0d6eea032b89418e7cc3b", + "d3920a7be05269d859bd89b08a6546dc6d6dd523dbc5f7b62b9c0c5eedc43292", + "de6078d584cb5f4a27c3f0bb3d8bbb16b3d5f8303237391f390d0ee9e84d0099", + "39fcbd6e0ec0ad49405f19c72bb033f578147181b77dbe47044f80b0b7604ab5", + "47ed060004fab3fb5fa4885008aa2cadbe3335655f1303231abfe89b4b0c9bd9" + ], + "removed": [], + "mined": [], + "replaced": [] + } +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "mempool-txids": { + "sequence": 79919, + "added": [ + "4bbb648ab194aaaf9188bccc6efcdcbb59c8485115a7384972c8287782206a0f", + "f7883f3784829d1e741e696bdceec488eeb53fe0b69b0eca574ac9f2e7e8e117", + "784e8e3b182c29798660bf42befb5c6479148c7d90c0d6eea032b89418e7cc3b", + "d3920a7be05269d859bd89b08a6546dc6d6dd523dbc5f7b62b9c0c5eedc43292", + "de6078d584cb5f4a27c3f0bb3d8bbb16b3d5f8303237391f390d0ee9e84d0099", + "39fcbd6e0ec0ad49405f19c72bb033f578147181b77dbe47044f80b0b7604ab5", + "47ed060004fab3fb5fa4885008aa2cadbe3335655f1303231abfe89b4b0c9bd9" + ], + "removed": [], + "mined": [], + "replaced": [] + } +}` + }, + codeSampleLiquid: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "mempool-txids": { + "sequence": 79919, + "added": [ + "4bbb648ab194aaaf9188bccc6efcdcbb59c8485115a7384972c8287782206a0f", + "f7883f3784829d1e741e696bdceec488eeb53fe0b69b0eca574ac9f2e7e8e117", + "784e8e3b182c29798660bf42befb5c6479148c7d90c0d6eea032b89418e7cc3b", + "d3920a7be05269d859bd89b08a6546dc6d6dd523dbc5f7b62b9c0c5eedc43292", + "de6078d584cb5f4a27c3f0bb3d8bbb16b3d5f8303237391f390d0ee9e84d0099", + "39fcbd6e0ec0ad49405f19c72bb033f578147181b77dbe47044f80b0b7604ab5", + "47ed060004fab3fb5fa4885008aa2cadbe3335655f1303231abfe89b4b0c9bd9" + ], + "removed": [], + "mined": [], + "replaced": [] + } +}` + }, + } + } + }, + { + type: "endpoint", + category: "mempool", + fragment: "track-mempool-block", + title: "Track Mempool Block", + description: { + default: "Subscribe to live mempool projected block template, index 0 being the first mempool block.
A full set of stripped transactions in that block is returned when the subscription starts, and deltas (removed and added transactions) are then sent every time the mempool changes." + }, + payload: '{ "track-mempool-block": 0 }', + showConditions: bitcoinNetworks.concat(liquidNetworks), + showJsExamples: false, + codeExample: { + default: { + codeTemplate: { + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "projected-block-transactions": { + "index": 0, + "sequence": 80270, + "delta": { + "added": [ + [ + "172b34fb099d80f61b65d1c107c4f25665c8f50e30c1371b2e6fbced62991d58", + 2000, + 171.25, + 5942725, + 11.68, + 1099511631877, + 1734881537 + ], + ... + ], + "removed": [ + "956a6eee382214631c3299e0410565e05fbd6328c89fa746efab6371705aca2a", + ... + ], + "changed": [] + } + } +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "projected-block-transactions": { + "index": 0, + "sequence": 80270, + "delta": { + "added": [ + [ + "172b34fb099d80f61b65d1c107c4f25665c8f50e30c1371b2e6fbced62991d58", + 2000, + 171.25, + 5942725, + 11.68, + 1099511631877, + 1734881537 + ], + ... + ], + "removed": [ + "956a6eee382214631c3299e0410565e05fbd6328c89fa746efab6371705aca2a", + ... + ], + "changed": [] + } + } +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "projected-block-transactions": { + "index": 0, + "sequence": 80270, + "delta": { + "added": [ + [ + "172b34fb099d80f61b65d1c107c4f25665c8f50e30c1371b2e6fbced62991d58", + 2000, + 171.25, + 5942725, + 11.68, + 1099511631877, + 1734881537 + ], + ... + ], + "removed": [ + "956a6eee382214631c3299e0410565e05fbd6328c89fa746efab6371705aca2a", + ... + ], + "changed": [] + } + } +}` + }, + codeSampleLiquid: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "projected-block-transactions": { + "index": 0, + "sequence": 80270, + "delta": { + "added": [ + [ + "172b34fb099d80f61b65d1c107c4f25665c8f50e30c1371b2e6fbced62991d58", + 2000, + 171.25, + 5942725, + 11.68, + 1099511631877, + 1734881537 + ], + ... + ], + "removed": [ + "956a6eee382214631c3299e0410565e05fbd6328c89fa746efab6371705aca2a", + ... + ], + "changed": [] + } + } +}` + }, + } + } + }, + { + type: "endpoint", + category: "mempool", + fragment: "track-rbf", + title: "Track Mempool RBF Transactions", + description: { + default: "Subscribe to new RBF events." + }, + payload: '{ "track-rbf": "all" }', + showConditions: bitcoinNetworks, + showJsExamples: false, + codeExample: { + default: { + codeTemplate: { + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "rbfLatest": [ + { + "tx": { + "txid": "cc6cb210f7ec32660fe4d46984ef64b64143fb02dc7ed70578c32b5f338ef6d6", + "fee": 8280, + "vsize": 204, + "value": 156397, + "rate": 10, + "time": 1734876576, + "rbf": true, + "fullRbf": false + }, + "time": 1734876576, + "fullRbf": false, + "replaces": [ + { + "tx": { + "txid": "4e94c23e075cf9c2b4ccaf32e3652b8b1bfecca6726390ccab821417f23b0876", + "fee": 4956, + "vsize": 204, + "value": 159721, + "rate": 9, + "time": 1734876204, + "rbf": true, + "fullRbf": false + }, + "time": 1734876204, + "fullRbf": false, + "replaces": [ + { + "tx": { + "txid": "9624fe4f9a183dcea2e8c6b640394eecaec37363aec883a64358f6953fba3145", + "fee": 1632, + "vsize": 204, + "value": 163045, + "rate": 8, + "time": 1734876081, + "rbf": true + }, + "time": 1734876081, + "interval": 123, + "fullRbf": false, + "replaces": [] + } + ], + "interval": 372 + } + ] + }, + ... + ] +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "rbfLatest": [ + { + "tx": { + "txid": "cc6cb210f7ec32660fe4d46984ef64b64143fb02dc7ed70578c32b5f338ef6d6", + "fee": 8280, + "vsize": 204, + "value": 156397, + "rate": 10, + "time": 1734876576, + "rbf": true, + "fullRbf": false + }, + "time": 1734876576, + "fullRbf": false, + "replaces": [ + { + "tx": { + "txid": "4e94c23e075cf9c2b4ccaf32e3652b8b1bfecca6726390ccab821417f23b0876", + "fee": 4956, + "vsize": 204, + "value": 159721, + "rate": 9, + "time": 1734876204, + "rbf": true, + "fullRbf": false + }, + "time": 1734876204, + "fullRbf": false, + "replaces": [ + { + "tx": { + "txid": "9624fe4f9a183dcea2e8c6b640394eecaec37363aec883a64358f6953fba3145", + "fee": 1632, + "vsize": 204, + "value": 163045, + "rate": 8, + "time": 1734876081, + "rbf": true + }, + "time": 1734876081, + "interval": 123, + "fullRbf": false, + "replaces": [] + } + ], + "interval": 372 + } + ] + }, + ... + ] +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "rbfLatest": [ + { + "tx": { + "txid": "cc6cb210f7ec32660fe4d46984ef64b64143fb02dc7ed70578c32b5f338ef6d6", + "fee": 8280, + "vsize": 204, + "value": 156397, + "rate": 10, + "time": 1734876576, + "rbf": true, + "fullRbf": false + }, + "time": 1734876576, + "fullRbf": false, + "replaces": [ + { + "tx": { + "txid": "4e94c23e075cf9c2b4ccaf32e3652b8b1bfecca6726390ccab821417f23b0876", + "fee": 4956, + "vsize": 204, + "value": 159721, + "rate": 9, + "time": 1734876204, + "rbf": true, + "fullRbf": false + }, + "time": 1734876204, + "fullRbf": false, + "replaces": [ + { + "tx": { + "txid": "9624fe4f9a183dcea2e8c6b640394eecaec37363aec883a64358f6953fba3145", + "fee": 1632, + "vsize": 204, + "value": 163045, + "rate": 8, + "time": 1734876081, + "rbf": true + }, + "time": 1734876081, + "interval": 123, + "fullRbf": false, + "replaces": [] + } + ], + "interval": 372 + } + ] + }, + ... + ] +}` + }, + codeSampleLiquid: emptyCodeSample + } + } + }, + { + type: "endpoint", + category: "mempool", + fragment: "track-full-rbf", + title: "Track Mempool Full RBF Transactions", + description: { + default: "Subscribe to new Full RBF events." + }, + payload: '{ "track-rbf": "fullRbf" }', + showConditions: bitcoinNetworks, + showJsExamples: false, + codeExample: { + default: { + codeTemplate: { + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "rbfLatest": [ + { + "tx": { + "txid": "ed9e1ec0e1635d465ee95c8872efff367d420fc2c4e624bada2c6e6e6c8e0629", + "fee": 4123, + "vsize": 587.75, + "value": 25545, + "rate": 7.014887282007656, + "time": 1734876941, + "rbf": false, + "fullRbf": true + }, + "time": 1734876941, + "fullRbf": true, + "replaces": [ + { + "tx": { + "txid": "495ad5d39d44286e99bc45d104605407325cd4790f842dc3287fbfdda8ee5795", + "fee": 1178, + "vsize": 587.25, + "value": 28490, + "rate": 2.0059599829714774, + "time": 1734853572, + "rbf": false, + "fullRbf": true + }, + "time": 1734853572, + "fullRbf": true, + "replaces": [ + { + "tx": { + "txid": "189751a7560a6c39deb9a93db2a27374842c646268d0007ba52aefa189833afa", + "fee": 589, + "vsize": 587.25, + "value": 29079, + "rate": 1.0029799914857387, + "time": 1734781955, + "rbf": false + }, + "time": 1734781955, + "interval": 71617, + "fullRbf": true, + "replaces": [] + } + ], + "interval": 23369 + } + ] + }, + ... + ] +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "rbfLatest": [ + { + "tx": { + "txid": "ed9e1ec0e1635d465ee95c8872efff367d420fc2c4e624bada2c6e6e6c8e0629", + "fee": 4123, + "vsize": 587.75, + "value": 25545, + "rate": 7.014887282007656, + "time": 1734876941, + "rbf": false, + "fullRbf": true + }, + "time": 1734876941, + "fullRbf": true, + "replaces": [ + { + "tx": { + "txid": "495ad5d39d44286e99bc45d104605407325cd4790f842dc3287fbfdda8ee5795", + "fee": 1178, + "vsize": 587.25, + "value": 28490, + "rate": 2.0059599829714774, + "time": 1734853572, + "rbf": false, + "fullRbf": true + }, + "time": 1734853572, + "fullRbf": true, + "replaces": [ + { + "tx": { + "txid": "189751a7560a6c39deb9a93db2a27374842c646268d0007ba52aefa189833afa", + "fee": 589, + "vsize": 587.25, + "value": 29079, + "rate": 1.0029799914857387, + "time": 1734781955, + "rbf": false + }, + "time": 1734781955, + "interval": 71617, + "fullRbf": true, + "replaces": [] + } + ], + "interval": 23369 + } + ] + }, + ... + ] +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "rbfLatest": [ + { + "tx": { + "txid": "ed9e1ec0e1635d465ee95c8872efff367d420fc2c4e624bada2c6e6e6c8e0629", + "fee": 4123, + "vsize": 587.75, + "value": 25545, + "rate": 7.014887282007656, + "time": 1734876941, + "rbf": false, + "fullRbf": true + }, + "time": 1734876941, + "fullRbf": true, + "replaces": [ + { + "tx": { + "txid": "495ad5d39d44286e99bc45d104605407325cd4790f842dc3287fbfdda8ee5795", + "fee": 1178, + "vsize": 587.25, + "value": 28490, + "rate": 2.0059599829714774, + "time": 1734853572, + "rbf": false, + "fullRbf": true + }, + "time": 1734853572, + "fullRbf": true, + "replaces": [ + { + "tx": { + "txid": "189751a7560a6c39deb9a93db2a27374842c646268d0007ba52aefa189833afa", + "fee": 589, + "vsize": 587.25, + "value": 29079, + "rate": 1.0029799914857387, + "time": 1734781955, + "rbf": false + }, + "time": 1734781955, + "interval": 71617, + "fullRbf": true, + "replaces": [] + } + ], + "interval": 23369 + } + ] + }, + ... + ] +}` + }, + codeSampleLiquid: emptyCodeSample + } + } + }, + +]; export const restApiDocsData = [ { diff --git a/frontend/src/app/docs/api-docs/api-docs-nav.component.ts b/frontend/src/app/docs/api-docs/api-docs-nav.component.ts index 11e39b518..dd19d0b4f 100644 --- a/frontend/src/app/docs/api-docs/api-docs-nav.component.ts +++ b/frontend/src/app/docs/api-docs/api-docs-nav.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Env, StateService } from '@app/services/state.service'; -import { restApiDocsData } from '@app/docs/api-docs/api-docs-data'; +import { restApiDocsData, wsApiDocsData } from '@app/docs/api-docs/api-docs-data'; import { faqData } from '@app/docs/api-docs/api-docs-data'; @Component({ @@ -28,6 +28,8 @@ export class ApiDocsNavComponent implements OnInit { this.auditEnabled = this.env.AUDIT; if (this.whichTab === 'rest') { this.tabData = restApiDocsData; + } else if (this.whichTab === 'websocket') { + this.tabData = wsApiDocsData; } else if (this.whichTab === 'faq') { this.tabData = faqData; } diff --git a/frontend/src/app/docs/api-docs/api-docs.component.html b/frontend/src/app/docs/api-docs/api-docs.component.html index 38b351e37..75e37a3bd 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.html +++ b/frontend/src/app/docs/api-docs/api-docs.component.html @@ -108,18 +108,43 @@
-
-
-
-
-
Endpoint
- {{ wrapUrl(network.val, wsDocs, true) }} +
+ +
+ +
+ +
+ +
+

Get higher API limits with Mempool Enterprise®

+ -
-
Description
-
Default push: {{ '{' }} action: 'want', data: ['blocks', ...] {{ '}' }} to express what you want pushed. Available: blocks, mempool-blocks, live-2h-chart, and stats.

Push transactions related to address: {{ '{' }} 'track-address': '3PbJ...bF9B' {{ '}' }} to receive all new transactions containing that address as input or output. Returns an array of transactions. address-transactions for new mempool transactions, and block-transactions for new block confirmed transactions.
+
+ +

Below is a reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} Websocket service running at {{ websocketUrl(network.val) }}.

+

Note that usage limits apply to our WebSocket API. Consider an enterprise sponsorship if you need higher API limits, such as higher tracking limits.

+ +
+
+

{{ item.title }}

+
+ {{ item.title }} {{ item.category }} +
+
+
Description
+
+
+
+
Payload
+
+
+ +
+
-
diff --git a/frontend/src/app/docs/api-docs/api-docs.component.scss b/frontend/src/app/docs/api-docs/api-docs.component.scss index ce8c37121..0d5ff93f1 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.scss +++ b/frontend/src/app/docs/api-docs/api-docs.component.scss @@ -470,3 +470,21 @@ dd { margin-left: 1em; } } + +code { + background-color: var(--bg); + font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New; +} + +pre { + display: block; + font-size: 87.5%; + color: #f18920; + background-color: var(--bg); + padding: 30px; + code{ + background-color: transparent; + white-space: break-spaces; + word-break: break-all; + } +} \ No newline at end of file diff --git a/frontend/src/app/docs/api-docs/api-docs.component.ts b/frontend/src/app/docs/api-docs/api-docs.component.ts index 0298fc9f3..75f71bbf5 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.ts +++ b/frontend/src/app/docs/api-docs/api-docs.component.ts @@ -145,7 +145,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit { if (document.getElementById( targetId + "-tab-header" )) { tabHeaderHeight = document.getElementById( targetId + "-tab-header" ).scrollHeight; } - if( ( window.innerWidth <= 992 ) && ( ( this.whichTab === 'rest' ) || ( this.whichTab === 'faq' ) ) && targetId ) { + if( ( window.innerWidth <= 992 ) && ( ( this.whichTab === 'rest' ) || ( this.whichTab === 'faq' ) || ( this.whichTab === 'websocket' ) ) && targetId ) { const endpointContainerEl = document.querySelector( "#" + targetId ); const endpointContentEl = document.querySelector( "#" + targetId + " .endpoint-content" ); const endPointContentElHeight = endpointContentEl.clientHeight; @@ -207,13 +207,29 @@ export class ApiDocsComponent implements OnInit, AfterViewInit { text = text.replace('%{' + indexNumber + '}', curlText); } - if (websocket) { - const wsHostname = this.hostname.replace('https://', 'wss://'); - wsHostname.replace('http://', 'ws://'); - return `${wsHostname}${curlNetwork}${text}`; - } return `${this.hostname}${curlNetwork}${text}`; } + websocketUrl(network: string) { + let curlNetwork = ''; + if (this.env.BASE_MODULE === 'mempool') { + if (!['', 'mainnet'].includes(network)) { + curlNetwork = `/${network}`; + } + } else if (this.env.BASE_MODULE === 'liquid') { + if (!['', 'liquid'].includes(network)) { + curlNetwork = `/${network}`; + } + } + + if (network === this.env.ROOT_NETWORK) { + curlNetwork = ''; + } + + let wsHostname = this.hostname.replace('https://', 'wss://'); + wsHostname = wsHostname.replace('http://', 'ws://'); + return `${wsHostname}${curlNetwork}/api/v1/ws`; + } + } From 37ddc29c2c6ae8709c50e696baa3b4c9acc5c366 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 25 Dec 2024 16:38:53 +0800 Subject: [PATCH 27/91] [accelerator] print sca status for google payment --- .../accelerate-checkout.component.ts | 13 +++++++------ frontend/src/app/services/services-api.service.ts | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index d6ac7f54f..4c935c57f 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -613,7 +613,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return; } const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2)); - if (!verificationToken) { + if (!verificationToken || !verificationToken.token) { console.error(`SCA verification failed`); this.accelerateError = 'SCA Verification Failed. Payment Declined.'; this.processing = false; @@ -623,10 +623,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.servicesApiService.accelerateWithGooglePay$( this.tx.txid, tokenResult.token, - verificationToken, + verificationToken.token, cardTag, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, - costUSD + costUSD, + verificationToken.userChallenged ).subscribe({ next: () => { this.processing = false; @@ -752,9 +753,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } /** - * Required in SCA Mandated Regions: Learn more at https://developer.squareup.com/docs/sca-overview + * https://developer.squareup.com/docs/sca-overview */ - async $verifyBuyer(payments, token, details, amount) { + async $verifyBuyer(payments, token, details, amount): Promise<{token: string, userChallenged: boolean}> { const verificationDetails = { amount: amount, currencyCode: 'USD', @@ -774,7 +775,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { token, verificationDetails, ); - return verificationResults.token; + return verificationResults; } /** diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index bec9d88a1..be6851dfc 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -143,8 +143,8 @@ export class ServicesApiServices { return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD }); } - accelerateWithGooglePay$(txInput: string, token: string, verificationToken: string, cardTag: string, referenceId: string, userApprovedUSD: number) { - return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD }); + accelerateWithGooglePay$(txInput: string, token: string, verificationToken: string, cardTag: string, referenceId: string, userApprovedUSD: number, userChallenged: boolean) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged }); } getAccelerations$(): Observable { From ff235760b2c378e9e16e5a32fc8ae7a998f514f9 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Fri, 27 Dec 2024 15:37:36 +0800 Subject: [PATCH 28/91] [fido] logout localstorage update --- frontend/src/app/services/auth.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/services/auth.service.ts b/frontend/src/app/services/auth.service.ts index db910779e..319bf8ef9 100644 --- a/frontend/src/app/services/auth.service.ts +++ b/frontend/src/app/services/auth.service.ts @@ -58,6 +58,7 @@ export class AuthServiceMempool { setAuth(auth: any) { if (!auth) { localStorage.removeItem('auth'); + localStorage.removeItem('authenticatorStatus'); } else { localStorage.setItem('auth', JSON.stringify(auth)); } From 6fa747b3034a94457a35e5de93ef53a46bfc1a4c Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 27 Dec 2024 15:58:53 +0700 Subject: [PATCH 29/91] remove unused lightweight-charts --- frontend/package-lock.json | 27 --------------------------- frontend/package.json | 1 - 2 files changed, 28 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a27bffcb4..82c9d78d5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -35,7 +35,6 @@ "domino": "^2.1.6", "echarts": "~5.5.0", "esbuild": "^0.24.0", - "lightweight-charts": "~3.8.0", "ngx-echarts": "~17.2.0", "ngx-infinite-scroll": "^17.0.0", "qrcode": "1.5.1", @@ -9921,11 +9920,6 @@ "node": ">=0.4.0" } }, - "node_modules/fancy-canvas": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-0.2.2.tgz", - "integrity": "sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA==" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -12106,14 +12100,6 @@ } } }, - "node_modules/lightweight-charts": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-3.8.0.tgz", - "integrity": "sha512-7yFGnYuE1RjRJG9RwUTBz5wvF1QtjBOSW4FFlikr8Dh+/TDNt4ci+HsWSYmStgQUpawpvkCJ3j5/W25GppGj9Q==", - "dependencies": { - "fancy-canvas": "0.2.2" - } - }, "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -25433,11 +25419,6 @@ "object-keys": "^1.0.6" } }, - "fancy-canvas": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-0.2.2.tgz", - "integrity": "sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA==" - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -27015,14 +26996,6 @@ "webpack-sources": "^3.0.0" } }, - "lightweight-charts": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-3.8.0.tgz", - "integrity": "sha512-7yFGnYuE1RjRJG9RwUTBz5wvF1QtjBOSW4FFlikr8Dh+/TDNt4ci+HsWSYmStgQUpawpvkCJ3j5/W25GppGj9Q==", - "requires": { - "fancy-canvas": "0.2.2" - } - }, "limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6a0d7dc12..49b759177 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -87,7 +87,6 @@ "clipboard": "^2.0.11", "domino": "^2.1.6", "echarts": "~5.5.0", - "lightweight-charts": "~3.8.0", "ngx-echarts": "~17.2.0", "ngx-infinite-scroll": "^17.0.0", "qrcode": "1.5.1", From 3c84505579b4b7d889f513575596d8888fcac6ce Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 27 Dec 2024 18:44:37 +0700 Subject: [PATCH 30/91] upgrading fortawesome dep --- frontend/package-lock.json | 304 +++++++++++++++++++------------------ frontend/package.json | 8 +- 2 files changed, 159 insertions(+), 153 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 82c9d78d5..c59a85671 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,9 +23,9 @@ "@angular/router": "^17.3.1", "@angular/ssr": "^17.3.1", "@fortawesome/angular-fontawesome": "~0.14.1", - "@fortawesome/fontawesome-common-types": "~6.6.0", - "@fortawesome/fontawesome-svg-core": "~6.6.0", - "@fortawesome/free-solid-svg-icons": "~6.6.0", + "@fortawesome/fontawesome-common-types": "~6.7.2", + "@fortawesome/fontawesome-svg-core": "~6.7.2", + "@fortawesome/free-solid-svg-icons": "~6.7.2", "@mempool/mempool.js": "2.3.0", "@ng-bootstrap/ng-bootstrap": "^16.0.0", "@types/qrcode": "~1.5.0", @@ -61,7 +61,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.15.0", + "cypress": "^13.17.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", @@ -3112,9 +3112,10 @@ } }, "node_modules/@cypress/request": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz", - "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz", + "integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==", + "license": "Apache-2.0", "optional": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -3130,9 +3131,9 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "6.13.0", + "qs": "6.13.1", "safe-buffer": "^5.1.2", - "tough-cookie": "^4.1.3", + "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -3140,6 +3141,22 @@ "node": ">= 6" } }, + "node_modules/@cypress/request/node_modules/qs": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", + "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/@cypress/schematic": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-2.5.0.tgz", @@ -3673,30 +3690,33 @@ } }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", - "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", - "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", + "license": "MIT", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" + "@fortawesome/fontawesome-common-types": "6.7.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", - "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", + "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", + "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" + "@fortawesome/fontawesome-common-types": "6.7.2" }, "engines": { "node": ">=6" @@ -5672,6 +5692,7 @@ "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", "optional": true, "dependencies": { "safer-buffer": "~2.1.0" @@ -5706,6 +5727,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", "optional": true, "engines": { "node": ">=0.8" @@ -5826,6 +5848,7 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", "optional": true, "engines": { "node": "*" @@ -5835,6 +5858,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT", "optional": true }, "node_modules/axios": { @@ -5992,6 +6016,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", "optional": true, "dependencies": { "tweetnacl": "^0.14.3" @@ -7067,6 +7092,7 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0", "optional": true }, "node_modules/chai": { @@ -7169,15 +7195,16 @@ } }, "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", + "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "optional": true, "engines": { "node": ">=8" @@ -7952,13 +7979,14 @@ "peer": true }, "node_modules/cypress": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz", - "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", + "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { - "@cypress/request": "^3.0.4", + "@cypress/request": "^3.0.6", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -7969,6 +7997,7 @@ "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", + "ci-info": "^4.0.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.1", "commander": "^6.2.1", @@ -7983,7 +8012,6 @@ "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^3.0.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", @@ -7998,6 +8026,7 @@ "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.3", + "tree-kill": "1.2.2", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, @@ -8200,6 +8229,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0" @@ -8686,6 +8716,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", "optional": true, "dependencies": { "jsbn": "~0.1.0", @@ -9904,6 +9935,7 @@ "engines": [ "node >=0.6.0" ], + "license": "MIT", "optional": true }, "node_modules/falafel": { @@ -10187,6 +10219,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", "optional": true, "engines": { "node": "*" @@ -10394,6 +10427,7 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0" @@ -10848,6 +10882,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0", @@ -11214,18 +11249,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "optional": true, - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -11475,6 +11498,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT", "optional": true }, "node_modules/is-unicode-supported": { @@ -11539,6 +11563,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT", "optional": true }, "node_modules/istanbul-lib-coverage": { @@ -11672,6 +11697,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT", "optional": true }, "node_modules/jsesc": { @@ -11700,6 +11726,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)", "optional": true }, "node_modules/json-schema-traverse": { @@ -11717,6 +11744,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", "optional": true }, "node_modules/json5": { @@ -11777,6 +11805,7 @@ "engines": [ "node >=0.6.0" ], + "license": "MIT", "optional": true, "dependencies": { "assert-plus": "1.0.0", @@ -14096,6 +14125,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", "optional": true }, "node_modules/picocolors": { @@ -14526,12 +14556,6 @@ "node": ">= 0.10" } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "optional": true - }, "node_modules/public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -14647,12 +14671,6 @@ "node": ">=0.4.x" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "optional": true - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -16014,6 +16032,7 @@ "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", "optional": true, "dependencies": { "asn1": "~0.2.3", @@ -16563,6 +16582,26 @@ "readable-stream": "3" } }, + "node_modules/tldts": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz", + "integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tldts-core": "^6.1.70" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz", + "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==", + "license": "MIT", + "optional": true + }, "node_modules/tlite": { "version": "0.1.9", "resolved": "https://registry.npmjs.org/tlite/-/tlite-0.1.9.tgz", @@ -16607,27 +16646,16 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "license": "BSD-3-Clause", "optional": true, "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "optional": true, - "engines": { - "node": ">= 4.0.0" + "node": ">=16" } }, "node_modules/transform-ast": { @@ -16796,6 +16824,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", "optional": true, "dependencies": { "safe-buffer": "^5.0.1" @@ -16808,6 +16837,7 @@ "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense", "optional": true }, "node_modules/type": { @@ -17116,16 +17146,6 @@ "querystring": "0.2.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "optional": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/url/node_modules/punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", @@ -17193,6 +17213,7 @@ "engines": [ "node >=0.6.0" ], + "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0", @@ -20334,9 +20355,9 @@ } }, "@cypress/request": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz", - "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz", + "integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==", "optional": true, "requires": { "aws-sign2": "~0.7.0", @@ -20352,11 +20373,22 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "6.13.0", + "qs": "6.13.1", "safe-buffer": "^5.1.2", - "tough-cookie": "^4.1.3", + "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" + }, + "dependencies": { + "qs": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", + "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "optional": true, + "requires": { + "side-channel": "^1.0.6" + } + } } }, "@cypress/schematic": { @@ -20635,24 +20667,24 @@ } }, "@fortawesome/fontawesome-common-types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", - "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==" + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==" }, "@fortawesome/fontawesome-svg-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", - "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", "requires": { - "@fortawesome/fontawesome-common-types": "6.6.0" + "@fortawesome/fontawesome-common-types": "6.7.2" } }, "@fortawesome/free-solid-svg-icons": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", - "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", + "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", "requires": { - "@fortawesome/fontawesome-common-types": "6.6.0" + "@fortawesome/fontawesome-common-types": "6.7.2" } }, "@goto-bus-stop/common-shake": { @@ -23284,9 +23316,9 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" }, "ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", + "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", "optional": true }, "cipher-base": { @@ -23882,12 +23914,12 @@ "peer": true }, "cypress": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz", - "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", + "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", "optional": true, "requires": { - "@cypress/request": "^3.0.4", + "@cypress/request": "^3.0.6", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -23898,6 +23930,7 @@ "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", + "ci-info": "^4.0.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.1", "commander": "^6.2.1", @@ -23912,7 +23945,6 @@ "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^3.0.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", @@ -23927,6 +23959,7 @@ "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.3", + "tree-kill": "1.2.2", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, @@ -26354,15 +26387,6 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" }, - "is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "optional": true, - "requires": { - "ci-info": "^3.2.0" - } - }, "is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -28779,12 +28803,6 @@ "event-stream": "=3.3.4" } }, - "psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "optional": true - }, "public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -28876,12 +28894,6 @@ "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "optional": true - }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -30346,6 +30358,21 @@ } } }, + "tldts": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz", + "integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==", + "optional": true, + "requires": { + "tldts-core": "^6.1.70" + } + }, + "tldts-core": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz", + "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==", + "optional": true + }, "tlite": { "version": "0.1.9", "resolved": "https://registry.npmjs.org/tlite/-/tlite-0.1.9.tgz", @@ -30378,23 +30405,12 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, "tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", "optional": true, "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "dependencies": { - "universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "optional": true - } + "tldts": "^6.1.32" } }, "transform-ast": { @@ -30730,16 +30746,6 @@ } } }, - "url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "optional": true, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 49b759177..2910b8869 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -76,9 +76,9 @@ "@angular/router": "^17.3.1", "@angular/ssr": "^17.3.1", "@fortawesome/angular-fontawesome": "~0.14.1", - "@fortawesome/fontawesome-common-types": "~6.6.0", - "@fortawesome/fontawesome-svg-core": "~6.6.0", - "@fortawesome/free-solid-svg-icons": "~6.6.0", + "@fortawesome/fontawesome-common-types": "~6.7.2", + "@fortawesome/fontawesome-svg-core": "~6.7.2", + "@fortawesome/free-solid-svg-icons": "~6.7.2", "@mempool/mempool.js": "2.3.0", "@ng-bootstrap/ng-bootstrap": "^16.0.0", "@types/qrcode": "~1.5.0", @@ -114,7 +114,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.15.0", + "cypress": "^13.17.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", From e77dd114f457ad9ab4c78882ab712ab585c1d69f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 02:15:20 +0000 Subject: [PATCH 31/91] Bump echarts from 5.5.0 to 5.6.0 in /frontend Bumps [echarts](https://github.com/apache/echarts) from 5.5.0 to 5.6.0. - [Release notes](https://github.com/apache/echarts/releases) - [Commits](https://github.com/apache/echarts/compare/5.5.0...5.6.0) --- updated-dependencies: - dependency-name: echarts dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 30 +++++++++++++++--------------- frontend/package.json | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c59a85671..932979e2b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -33,7 +33,7 @@ "browserify": "^17.0.0", "clipboard": "^2.0.11", "domino": "^2.1.6", - "echarts": "~5.5.0", + "echarts": "~5.6.0", "esbuild": "^0.24.0", "ngx-echarts": "~17.2.0", "ngx-infinite-scroll": "^17.0.0", @@ -8724,12 +8724,12 @@ } }, "node_modules/echarts": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz", - "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", "dependencies": { "tslib": "2.3.0", - "zrender": "5.5.0" + "zrender": "5.6.1" } }, "node_modules/echarts/node_modules/tslib": { @@ -18366,9 +18366,9 @@ } }, "node_modules/zrender": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz", - "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", "dependencies": { "tslib": "2.3.0" } @@ -24485,12 +24485,12 @@ } }, "echarts": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz", - "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", "requires": { "tslib": "2.3.0", - "zrender": "5.5.0" + "zrender": "5.6.1" }, "dependencies": { "tslib": { @@ -31485,9 +31485,9 @@ } }, "zrender": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz", - "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", "requires": { "tslib": "2.3.0" }, diff --git a/frontend/package.json b/frontend/package.json index 2910b8869..b50085a54 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -86,7 +86,7 @@ "browserify": "^17.0.0", "clipboard": "^2.0.11", "domino": "^2.1.6", - "echarts": "~5.5.0", + "echarts": "~5.6.0", "ngx-echarts": "~17.2.0", "ngx-infinite-scroll": "^17.0.0", "qrcode": "1.5.1", From 65533444890d94adc17cb28d881c42b9a46f6a19 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 6 Jan 2025 18:41:46 +0000 Subject: [PATCH 32/91] add missing rxjs unsubscriptions --- .../acceleration-fees-graph.component.ts | 14 ++++--- .../block-overview-graph.component.ts | 3 +- .../block-view/block-view.component.ts | 2 +- .../block/block-preview.component.ts | 2 +- .../app/components/block/block.component.ts | 40 ++++++++++--------- 5 files changed, 34 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts index 6a99edbf1..05602d577 100644 --- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts +++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts @@ -46,6 +46,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest aggregatedHistory$: Observable; statsSubscription: Subscription; + aggregatedHistorySubscription: Subscription; + fragmentSubscription: Subscription; isLoading = true; formatNumber = formatNumber; timespan = ''; @@ -79,8 +81,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest } this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); - - this.route.fragment.subscribe((fragment) => { + + this.fragmentSubscription = this.route.fragment.subscribe((fragment) => { if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) { this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); } @@ -113,7 +115,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest share(), ); - this.aggregatedHistory$.subscribe(); + this.aggregatedHistorySubscription = this.aggregatedHistory$.subscribe(); } ngOnChanges(changes: SimpleChanges): void { @@ -335,8 +337,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest } ngOnDestroy(): void { - if (this.statsSubscription) { - this.statsSubscription.unsubscribe(); - } + this.aggregatedHistorySubscription?.unsubscribe(); + this.fragmentSubscription?.unsubscribe(); + this.statsSubscription?.unsubscribe(); } } diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index d59e38c13..605a9c222 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -177,8 +177,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On if (this.canvas) { this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost); this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored); - this.themeChangedSubscription?.unsubscribe(); } + this.themeChangedSubscription?.unsubscribe(); + this.searchSubscription?.unsubscribe(); } clear(direction): void { diff --git a/frontend/src/app/components/block-view/block-view.component.ts b/frontend/src/app/components/block-view/block-view.component.ts index b5d5256ee..9c316fe1a 100644 --- a/frontend/src/app/components/block-view/block-view.component.ts +++ b/frontend/src/app/components/block-view/block-view.component.ts @@ -116,7 +116,7 @@ export class BlockViewComponent implements OnInit, OnDestroy { this.isLoadingBlock = false; this.isLoadingOverview = true; }), - shareReplay(1) + shareReplay({ bufferSize: 1, refCount: true }) ); this.overviewSubscription = block$.pipe( diff --git a/frontend/src/app/components/block/block-preview.component.ts b/frontend/src/app/components/block/block-preview.component.ts index b2fc3fb6f..42a47f3c4 100644 --- a/frontend/src/app/components/block/block-preview.component.ts +++ b/frontend/src/app/components/block/block-preview.component.ts @@ -117,7 +117,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { this.openGraphService.waitOver('block-data-' + this.rawId); }), throttleTime(50, asyncScheduler, { leading: true, trailing: true }), - shareReplay(1) + shareReplay({ bufferSize: 1, refCount: true }) ); this.overviewSubscription = block$.pipe( diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index dab3c00fa..072e76755 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core'; import { Location } from '@angular/common'; -import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { ActivatedRoute, ParamMap, Params, Router } from '@angular/router'; import { ElectrsApiService } from '@app/services/electrs-api.service'; -import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter } from 'rxjs/operators'; +import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter, take } from 'rxjs/operators'; import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs'; import { StateService } from '@app/services/state.service'; import { SeoService } from '@app/services/seo.service'; @@ -68,6 +68,7 @@ export class BlockComponent implements OnInit, OnDestroy { paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; numUnexpected: number = 0; mode: 'projected' | 'actual' = 'projected'; + currentQueryParams: Params; overviewSubscription: Subscription; accelerationsSubscription: Subscription; @@ -80,8 +81,8 @@ export class BlockComponent implements OnInit, OnDestroy { timeLtr: boolean; childChangeSubscription: Subscription; auditPrefSubscription: Subscription; + isAuditEnabledSubscription: Subscription; oobSubscription: Subscription; - priceSubscription: Subscription; blockConversion: Price; @@ -118,7 +119,7 @@ export class BlockComponent implements OnInit, OnDestroy { this.setAuditAvailable(this.auditSupported); if (this.auditSupported) { - this.isAuditEnabledFromParam().subscribe(auditParam => { + this.isAuditEnabledSubscription = this.isAuditEnabledFromParam().subscribe(auditParam => { if (this.auditParamEnabled) { this.auditModeEnabled = auditParam; } else { @@ -281,7 +282,7 @@ export class BlockComponent implements OnInit, OnDestroy { } }), throttleTime(300, asyncScheduler, { leading: true, trailing: true }), - shareReplay(1) + shareReplay({ bufferSize: 1, refCount: true }) ); this.overviewSubscription = this.block$.pipe( @@ -363,6 +364,7 @@ export class BlockComponent implements OnInit, OnDestroy { .subscribe((network) => this.network = network); this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { + this.currentQueryParams = params; if (params.showDetails === 'true') { this.showDetails = true; } else { @@ -414,6 +416,7 @@ export class BlockComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.stateService.markBlock$.next({}); this.overviewSubscription?.unsubscribe(); + this.accelerationsSubscription?.unsubscribe(); this.keyNavigationSubscription?.unsubscribe(); this.blocksSubscription?.unsubscribe(); this.cacheBlocksSubscription?.unsubscribe(); @@ -421,8 +424,10 @@ export class BlockComponent implements OnInit, OnDestroy { this.queryParamsSubscription?.unsubscribe(); this.timeLtrSubscription?.unsubscribe(); this.childChangeSubscription?.unsubscribe(); - this.priceSubscription?.unsubscribe(); + this.auditPrefSubscription?.unsubscribe(); + this.isAuditEnabledSubscription?.unsubscribe(); this.oobSubscription?.unsubscribe(); + this.priceSubscription?.unsubscribe(); } // TODO - Refactor this.fees/this.reward for liquid because it is not @@ -733,19 +738,18 @@ export class BlockComponent implements OnInit, OnDestroy { toggleAuditMode(): void { this.stateService.hideAudit.next(this.auditModeEnabled); - this.route.queryParams.subscribe(params => { - const queryParams = { ...params }; - delete queryParams['audit']; + const queryParams = { ...this.currentQueryParams }; + delete queryParams['audit']; - let newUrl = this.router.url.split('?')[0]; - const queryString = new URLSearchParams(queryParams).toString(); - if (queryString) { - newUrl += '?' + queryString; - } - - this.location.replaceState(newUrl); - }); + let newUrl = this.router.url.split('?')[0]; + const queryString = new URLSearchParams(queryParams).toString(); + if (queryString) { + newUrl += '?' + queryString; + } + this.location.replaceState(newUrl); + // avoid duplicate subscriptions + this.auditPrefSubscription?.unsubscribe(); this.auditPrefSubscription = this.stateService.hideAudit.subscribe((hide) => { this.auditModeEnabled = !hide; this.showAudit = this.auditAvailable && this.auditModeEnabled; @@ -762,7 +766,7 @@ export class BlockComponent implements OnInit, OnDestroy { return this.route.queryParams.pipe( map(params => { this.auditParamEnabled = 'audit' in params; - + return this.auditParamEnabled ? !(params['audit'] === 'false') : true; }) ); From 9f5666f41043d0eb5c34269d39d867fb0d87530d Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 6 Jan 2025 19:02:24 +0000 Subject: [PATCH 33/91] explicitly destroy block scenes --- .../block-overview-graph.component.ts | 11 +++++++---- .../block-overview-graph/fast-vertex-array.ts | 8 ++++++++ .../app/components/block-view/block-view.component.ts | 3 +++ frontend/src/app/components/block/block.component.ts | 6 ++++++ .../components/eight-blocks/eight-blocks.component.ts | 3 +++ .../mempool-block-overview.component.ts | 1 + 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index 605a9c222..3d6a76531 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -172,12 +172,17 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On ngOnDestroy(): void { if (this.animationFrameRequest) { cancelAnimationFrame(this.animationFrameRequest); - clearTimeout(this.animationHeartBeat); } + clearTimeout(this.animationHeartBeat); if (this.canvas) { this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost); this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored); } + if (this.scene) { + this.scene.destroy(); + } + this.vertexArray.destroy(); + this.vertexArray = null; this.themeChangedSubscription?.unsubscribe(); this.searchSubscription?.unsubscribe(); } @@ -490,9 +495,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On if (this.running && this.scene && now <= (this.scene.animateUntil + 500)) { this.doRun(); } else { - if (this.animationHeartBeat) { - clearTimeout(this.animationHeartBeat); - } + clearTimeout(this.animationHeartBeat); this.animationHeartBeat = window.setTimeout(() => { this.start(); }, 1000); diff --git a/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts b/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts index 42439ef8d..98cf7ce4d 100644 --- a/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts +++ b/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts @@ -110,4 +110,12 @@ export class FastVertexArray { getVertexData(): Float32Array { return this.data; } + + destroy(): void { + this.data = null; + this.sprites = null; + this.freeSlots = null; + this.lastSlot = 0; + this.dirty = false; + } } diff --git a/frontend/src/app/components/block-view/block-view.component.ts b/frontend/src/app/components/block-view/block-view.component.ts index 9c316fe1a..19a18383e 100644 --- a/frontend/src/app/components/block-view/block-view.component.ts +++ b/frontend/src/app/components/block-view/block-view.component.ts @@ -176,5 +176,8 @@ export class BlockViewComponent implements OnInit, OnDestroy { if (this.queryParamsSubscription) { this.queryParamsSubscription.unsubscribe(); } + if (this.blockGraph) { + this.blockGraph.destroy(); + } } } diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 072e76755..ddcf023ed 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -428,6 +428,12 @@ export class BlockComponent implements OnInit, OnDestroy { this.isAuditEnabledSubscription?.unsubscribe(); this.oobSubscription?.unsubscribe(); this.priceSubscription?.unsubscribe(); + this.blockGraphProjected.forEach(graph => { + graph.destroy(); + }); + this.blockGraphActual.forEach(graph => { + graph.destroy(); + }); } // TODO - Refactor this.fees/this.reward for liquid because it is not diff --git a/frontend/src/app/components/eight-blocks/eight-blocks.component.ts b/frontend/src/app/components/eight-blocks/eight-blocks.component.ts index 8ca8437ac..0e0861382 100644 --- a/frontend/src/app/components/eight-blocks/eight-blocks.component.ts +++ b/frontend/src/app/components/eight-blocks/eight-blocks.component.ts @@ -162,6 +162,9 @@ export class EightBlocksComponent implements OnInit, OnDestroy { this.cacheBlocksSubscription?.unsubscribe(); this.networkChangedSubscription?.unsubscribe(); this.queryParamsSubscription?.unsubscribe(); + this.blockGraphs.forEach(graph => { + graph.destroy(); + }); } shiftTestBlocks(): void { diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index fca8b279c..a46be2733 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -120,6 +120,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang } ngOnDestroy(): void { + this.blockGraph?.destroy(); this.blockSub.unsubscribe(); this.timeLtrSubscription.unsubscribe(); this.websocketService.stopTrackMempoolBlock(); From e18c57254989bea29c94d7d43922ea5fe873de6e Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Tue, 7 Jan 2025 16:31:03 +0900 Subject: [PATCH 34/91] add missing icon --- frontend/src/app/shared/shared.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index ce5ac0f65..bfd4b84de 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, - faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck } from '@fortawesome/free-solid-svg-icons'; + faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck, faMoneyBillTrendUp } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MenuComponent } from '@components/menu/menu.component'; import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component'; @@ -451,5 +451,6 @@ export class SharedModule { library.addIcons(faTimeline); library.addIcons(faCircleXmark); library.addIcons(faCalendarCheck); + library.addIcons(faMoneyBillTrendUp); } } From 68ea7c59f35bc0556b36e374bc870421b490bd92 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 7 Jan 2025 11:21:04 +0100 Subject: [PATCH 35/91] Fix transaction filter logic for first seen fetching --- .../src/app/components/transaction/transaction.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 71ffaa2cd..ab71529c0 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -240,7 +240,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { retry({ count: 2, delay: 2000 }), // Try again until we either get a valid response, or the transaction is confirmed repeat({ delay: 2000 }), - filter((transactionTimes) => transactionTimes?.length && transactionTimes[0] > 0 && !this.tx.status?.confirmed), + filter((transactionTimes) => transactionTimes?.[0] > 0 || this.tx.status?.confirmed), take(1), )), ) From 7740908a4cdd852cb5a95ae6d0b1f74b8774baa4 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 8 Jan 2025 13:08:33 +0000 Subject: [PATCH 36/91] fix access block scene after destroyed --- .../block-overview-graph.component.ts | 2 +- .../block-overview-graph/fast-vertex-array.ts | 22 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index 3d6a76531..419a51995 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -453,7 +453,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On } this.applyQueuedUpdates(); // skip re-render if there's no change to the scene - if (this.scene && this.gl) { + if (this.scene && this.gl && this.vertexArray) { /* SET UP SHADER UNIFORMS */ // screen dimensions this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight); diff --git a/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts b/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts index 98cf7ce4d..8f9978d13 100644 --- a/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts +++ b/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts @@ -19,6 +19,7 @@ export class FastVertexArray { freeSlots: number[]; lastSlot: number; dirty = false; + destroyed = false; constructor(length, stride) { this.length = length; @@ -32,6 +33,9 @@ export class FastVertexArray { } insert(sprite: TxSprite): number { + if (this.destroyed) { + return; + } this.count++; let position; @@ -45,11 +49,14 @@ export class FastVertexArray { } } this.sprites[position] = sprite; - return position; this.dirty = true; + return position; } remove(index: number): void { + if (this.destroyed) { + return; + } this.count--; this.clearData(index); this.freeSlots.push(index); @@ -61,20 +68,26 @@ export class FastVertexArray { } setData(index: number, dataChunk: number[]): void { + if (this.destroyed) { + return; + } this.data.set(dataChunk, (index * this.stride)); this.dirty = true; } - clearData(index: number): void { + private clearData(index: number): void { this.data.fill(0, (index * this.stride), ((index + 1) * this.stride)); this.dirty = true; } getData(index: number): Float32Array { + if (this.destroyed) { + return; + } return this.data.subarray(index, this.stride); } - expand(): void { + private expand(): void { this.length *= 2; const newData = new Float32Array(this.length * this.stride); newData.set(this.data); @@ -82,7 +95,7 @@ export class FastVertexArray { this.dirty = true; } - compact(): void { + private compact(): void { // New array length is the smallest power of 2 larger than the sprite count (but no smaller than 512) const newLength = Math.max(512, Math.pow(2, Math.ceil(Math.log2(this.count)))); if (newLength !== this.length) { @@ -117,5 +130,6 @@ export class FastVertexArray { this.freeSlots = null; this.lastSlot = 0; this.dirty = false; + this.destroyed = true; } } From 9fbbe4980da852bcd3f1aa49997f592877d1d199 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 8 Jan 2025 16:01:59 +0100 Subject: [PATCH 37/91] Fix transaction amount change for P2PK addresses --- .../transactions-list/transactions-list.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index dfe19ca74..8e67ccdfc 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -202,12 +202,12 @@ export class TransactionsListComponent implements OnInit, OnChanges { for (const address of this.addresses) { switch (address.length) { case 130: { - if (v.scriptpubkey === '21' + address + 'ac') { + if (v.scriptpubkey === '41' + address + 'ac') { return v.value; } } break; case 66: { - if (v.scriptpubkey === '41' + address + 'ac') { + if (v.scriptpubkey === '21' + address + 'ac') { return v.value; } } break; @@ -224,12 +224,12 @@ export class TransactionsListComponent implements OnInit, OnChanges { for (const address of this.addresses) { switch (address.length) { case 130: { - if (v.prevout?.scriptpubkey === '21' + address + 'ac') { + if (v.prevout?.scriptpubkey === '41' + address + 'ac') { return v.prevout?.value; } } break; case 66: { - if (v.prevout?.scriptpubkey === '41' + address + 'ac') { + if (v.prevout?.scriptpubkey === '21' + address + 'ac') { return v.prevout?.value; } } break; From 21cdb7e3a17ea287d4e7541192e633926f0f351f Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sat, 11 Jan 2025 18:31:45 +0900 Subject: [PATCH 38/91] [accelerator] remove tx restriction for cashapp payments --- .../accelerate-checkout/accelerate-checkout.component.ts | 8 ++------ .../src/app/components/tracker/tracker.component.html | 1 - frontend/src/app/components/tracker/tracker.component.ts | 4 ---- .../app/components/transaction/transaction.component.html | 1 - .../app/components/transaction/transaction.component.ts | 5 ----- 5 files changed, 2 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 4c935c57f..36c62fcf9 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -62,7 +62,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() miningStats: MiningStats; @Input() eta: ETA; @Input() scrollEvent: boolean; - @Input() cashappEnabled: boolean = true; @Input() applePayEnabled: boolean = false; @Input() googlePayEnabled: boolean = true; @Input() advancedEnabled: boolean = false; @@ -217,7 +216,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.loadingBtcpayInvoice = true; this.invoice = null; this.requestBTCPayInvoice(); - } else if (this._step === 'cashapp' && this.cashappEnabled) { + } else if (this._step === 'cashapp') { this.loadingCashapp = true; this.setupSquare(); this.scrollToElementWithTimeout('confirm-title', 'center', 100); @@ -828,9 +827,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } get couldPayWithCashapp(): boolean { - if (!this.cashappEnabled) { - return false; - } return !!this.estimate?.availablePaymentMethods?.cashapp; } @@ -865,7 +861,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } get canPayWithCashapp(): boolean { - if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) { + if (!this.conversions || (!this.isProdDomain && !isDevMode())) { return false; } diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index 797694919..e07898da3 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -124,7 +124,6 @@ 0 && this.tx.weight < 4000; - } - get showAccelerationSummary(): boolean { return ( this.tx diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 8c2d9de01..5438bfe4a 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -138,7 +138,6 @@ 0 && this.tx.weight < 4000) { - this.cashappEligible = true; - } if (!this.gotInitialPosition && txPosition.position?.block === 0 && txPosition.position?.vsize < 750_000) { this.accelerationFlowCompleted = true; } @@ -1036,7 +1032,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.showAccelerationDetails = false; this.accelerationFlowCompleted = false; this.accelerationInfo = null; - this.cashappEligible = false; this.txInBlockIndex = null; this.mempoolPosition = null; this.pool = null; From faa83866fd1d8d09a651d600973612b5952b7e4e Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 12 Jan 2025 12:04:19 +0900 Subject: [PATCH 39/91] [blocks] fix block pools-v2 hash migration --- backend/src/api/database-migration.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 0ca58a785..4f43bd9d2 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -1105,13 +1105,6 @@ class DatabaseMigration { `); } - // blocks pools-v2.json hash - if (databaseSchemaVersion < 95) { - await this.$executeQuery('ALTER TABLE `blocks` ADD definition_hash varchar(255) NOT NULL DEFAULT "5f32a67401929169f225f5db43c9efa795d1b159"'); - await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `definition_hash` (`definition_hash`)'); - await this.updateToSchemaVersion(95); - } - if (config.MEMPOOL.NETWORK !== 'mainnet') { // Apply all the mainnet specific migrations to all other networks // Version 69 @@ -1125,6 +1118,18 @@ class DatabaseMigration { } await this.updateToSchemaVersion(94); } + + // blocks pools-v2.json hash + if (databaseSchemaVersion < 95) { + let poolJsonSha = 'f737d86571d190cf1a1a3cf5fd86b33ba9624254'; + const [poolJsonShaDb]: any[] = await DB.query(`SELECT string FROM state WHERE name = 'pools_json_sha'`); + if (poolJsonShaDb?.length > 0) { + poolJsonSha = poolJsonShaDb[0].string; + } + await this.$executeQuery(`ALTER TABLE blocks ADD definition_hash varchar(255) NOT NULL DEFAULT "${poolJsonSha}"`); + await this.$executeQuery('ALTER TABLE blocks ADD INDEX `definition_hash` (`definition_hash`)'); + await this.updateToSchemaVersion(95); + } } /** From 4a4259fa7d9e54f83fabe36e085e58a3d5d8921e Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 12 Jan 2025 12:16:51 +0900 Subject: [PATCH 40/91] add new robot icon, wrap import into new lines --- frontend/src/app/shared/shared.module.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index bfd4b84de..2bcfc1255 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -4,7 +4,10 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, - faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck, faMoneyBillTrendUp } from '@fortawesome/free-solid-svg-icons'; + faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, + faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, + faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, + faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MenuComponent } from '@components/menu/menu.component'; import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component'; @@ -452,5 +455,6 @@ export class SharedModule { library.addIcons(faCircleXmark); library.addIcons(faCalendarCheck); library.addIcons(faMoneyBillTrendUp); + library.addIcons(faRobot); } } From 3325db48830291c482f329cadee3a8bbfeefbb76 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Mon, 13 Jan 2025 11:06:21 +0900 Subject: [PATCH 41/91] [accelerator] add btcpay invoice retry button and polish checkout UI --- .../accelerate-checkout.component.html | 15 +++++++++------ .../accelerate-checkout.component.scss | 18 ++++++++++++++++++ .../accelerate-checkout.component.ts | 5 +++-- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index df67de65c..3fd92939a 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -357,9 +357,9 @@
-
+
-

Payment to mempool.space for acceleration of txid {{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}

+ Payment to mempool.space for acceleration of txid {{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
@@ -378,9 +378,12 @@

Pay {{ ((invoice.btcDue * 100_000_000) || cost) | number }} sats

} @else if (btcpayInvoiceFailed) { -

Failed to load invoice

-
- +
+ + Failed to load invoice + @if (!loadingBtcpayInvoice) { + + }
} @else {

Loading invoice...

@@ -391,7 +394,7 @@
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
-

OR

+

—— OR ——

} } diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss index ad085ed20..fd9ad06ec 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss @@ -146,6 +146,11 @@ .payment-area { background: var(--bg); + margin-top: 0.5rem; + padding: 0.5rem; + @media (max-width: 575px) { + padding-bottom: 1.25rem; + } } .col.pie { @@ -212,4 +217,17 @@ } .apple-pay-button-white-with-line { -apple-pay-button-style: white-outline; +} + +.btcpay-invoice { + display: flex; + height: 292px; + flex-direction: column; + justify-content: center; + align-items: center; + @media (max-width: 575px) { + height: 75px; + flex-direction: row; + gap: 5px; + } } \ No newline at end of file diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 4c935c57f..e675b6440 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -214,7 +214,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } if (this._step === 'checkout' && this.canPayWithBitcoin) { this.btcpayInvoiceFailed = false; - this.loadingBtcpayInvoice = true; this.invoice = null; this.requestBTCPayInvoice(); } else if (this._step === 'cashapp' && this.cashappEnabled) { @@ -323,7 +322,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) { - this.loadingBtcpayInvoice = true; this.requestBTCPayInvoice(); } @@ -782,16 +780,19 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * BTCPay */ async requestBTCPayInvoice(): Promise { + this.loadingBtcpayInvoice = true; this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe( switchMap(response => { return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId); }), catchError(error => { console.log(error); + this.loadingBtcpayInvoice = false; this.btcpayInvoiceFailed = true; return of(null); }) ).subscribe((invoice) => { + this.loadingBtcpayInvoice = false; this.invoice = invoice; this.cd.markForCheck(); }); From f6ab2caaf9e26b1cb7c6295f03ee17edbb2aee18 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 14 Jan 2025 10:42:40 +0900 Subject: [PATCH 42/91] Fix textarea keyboard navigation --- frontend/src/app/components/app/app.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/app/app.component.ts b/frontend/src/app/components/app/app.component.ts index 365c23972..01f887c58 100644 --- a/frontend/src/app/components/app/app.component.ts +++ b/frontend/src/app/components/app/app.component.ts @@ -41,7 +41,7 @@ export class AppComponent implements OnInit { @HostListener('document:keydown', ['$event']) handleKeyboardEvents(event: KeyboardEvent) { - if (event.target instanceof HTMLInputElement) { + if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) { return; } // prevent arrow key horizontal scrolling From 6a4aeaf7ed624c971df150d1386caf1ca2f504c3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 13 Jan 2025 07:11:17 +0000 Subject: [PATCH 43/91] Fix unconfirmed badge on broken RBF txs --- .../src/app/components/transaction/transaction.component.html | 1 + .../components/confirmations/confirmations.component.html | 4 ++-- .../components/confirmations/confirmations.component.ts | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 8c2d9de01..cce0e23eb 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -24,6 +24,7 @@ [height]="tx?.status?.block_height" [replaced]="replaced" [removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed" + [cached]="isCached" >
diff --git a/frontend/src/app/shared/components/confirmations/confirmations.component.html b/frontend/src/app/shared/components/confirmations/confirmations.component.html index 4ad3cb33a..282979824 100644 --- a/frontend/src/app/shared/components/confirmations/confirmations.component.html +++ b/frontend/src/app/shared/components/confirmations/confirmations.component.html @@ -11,9 +11,9 @@ - + - + \ No newline at end of file diff --git a/frontend/src/app/shared/components/confirmations/confirmations.component.ts b/frontend/src/app/shared/components/confirmations/confirmations.component.ts index 624c58278..d54f80b10 100644 --- a/frontend/src/app/shared/components/confirmations/confirmations.component.ts +++ b/frontend/src/app/shared/components/confirmations/confirmations.component.ts @@ -12,6 +12,7 @@ export class ConfirmationsComponent implements OnChanges { @Input() height: number; @Input() replaced: boolean = false; @Input() removed: boolean = false; + @Input() cached: boolean = false; @Input() hideUnconfirmed: boolean = false; @Input() buttonClass: string = ''; From 4dcbccd9b23acf012466be5ab24e80adae9db0cb Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 14 Jan 2025 06:41:34 +0000 Subject: [PATCH 44/91] avoid creating incomplete RBF trees --- backend/src/api/rbf-cache.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index 944ad790e..df6e10c77 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -119,7 +119,11 @@ class RbfCache { public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void { - if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) { + if ( !newTxExtended + || !replaced?.length + || this.txs.has(newTxExtended.txid) + || !(replaced.some(tx => !this.replacedBy.has(tx.txid))) + ) { return; } From cd02d8923524852b39f0c86df39550b7cb3dbd3f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 14 Jan 2025 09:04:01 +0000 Subject: [PATCH 45/91] Fix time traveling balance charts --- .../address-graph/address-graph.component.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/components/address-graph/address-graph.component.ts b/frontend/src/app/components/address-graph/address-graph.component.ts index db9345b18..2bbfd5e34 100644 --- a/frontend/src/app/components/address-graph/address-graph.component.ts +++ b/frontend/src/app/components/address-graph/address-graph.component.ts @@ -478,25 +478,30 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { } extendSummary(summary) { - let extendedSummary = summary.slice(); + const extendedSummary = summary.slice(); // Add a point at today's date to make the graph end at the current time extendedSummary.unshift({ time: Date.now() / 1000, value: 0 }); - extendedSummary.reverse(); - let oneHour = 60 * 60; + let maxTime = Date.now() / 1000; + + const oneHour = 60 * 60; // Fill gaps longer than interval for (let i = 0; i < extendedSummary.length - 1; i++) { - let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour); + if (extendedSummary[i].time > maxTime) { + extendedSummary[i].time = maxTime - 30; + } + maxTime = extendedSummary[i].time; + const hours = Math.floor((extendedSummary[i].time - extendedSummary[i + 1].time) / oneHour); if (hours > 1) { for (let j = 1; j < hours; j++) { - let newTime = extendedSummary[i].time + oneHour * j; + const newTime = extendedSummary[i].time - oneHour * j; extendedSummary.splice(i + j, 0, { time: newTime, value: 0 }); } i += hours - 1; } } - return extendedSummary.reverse(); + return extendedSummary; } } From 8529b996750d27b8a5e34c6053711266945f2f29 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 25 Jul 2024 22:34:52 +0000 Subject: [PATCH 46/91] custom dashboard wallet widgets --- frontend/src/app/interfaces/node-api.interface.ts | 2 +- frontend/src/app/services/state.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index b39f8e0d3..4d85a938d 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -1,4 +1,4 @@ -import { AddressTxSummary, Block, ChainStats, Transaction } from "./electrs.interface"; +import { AddressTxSummary, Block, ChainStats } from "./electrs.interface"; export interface OptimizedMempoolStats { added: number; diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 0d006b552..55f76d6b1 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -171,7 +171,7 @@ export class StateService { mempoolRemovedTransactions$ = new Subject(); multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>(); blockTransactions$ = new Subject(); - walletTransactions$ = new Subject(); + walletTransactions$ = new Subject>(); isLoadingWebSocket$ = new ReplaySubject(1); isLoadingMempool$ = new BehaviorSubject(true); vbytesPerSecond$ = new ReplaySubject(1); From c4ec50b7719f6e709582aae89aad25c6e901712b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 4 Oct 2024 22:31:49 +0000 Subject: [PATCH 47/91] Restore transactions list to wallet page --- .../components/wallet/wallet.component.html | 30 ++++++++ .../app/components/wallet/wallet.component.ts | 77 ++++++++++++++++++- 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/wallet/wallet.component.html b/frontend/src/app/components/wallet/wallet.component.html index 52b7b02a5..ed6b82456 100644 --- a/frontend/src/app/components/wallet/wallet.component.html +++ b/frontend/src/app/components/wallet/wallet.component.html @@ -74,6 +74,36 @@ +
+ +
+

Transactions

+
+ + + +
+ +
+
+
+ +
+
+ +
+
+
+ +
+ + +
+ +
+
+ +
diff --git a/frontend/src/app/components/wallet/wallet.component.ts b/frontend/src/app/components/wallet/wallet.component.ts index ce44250e9..bf5b67ae7 100644 --- a/frontend/src/app/components/wallet/wallet.component.ts +++ b/frontend/src/app/components/wallet/wallet.component.ts @@ -9,6 +9,8 @@ import { of, Observable, Subscription } from 'rxjs'; import { SeoService } from '@app/services/seo.service'; import { seoDescriptionNetwork } from '@app/shared/common.utils'; import { WalletAddress } from '@interfaces/node-api.interface'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; +import { AudioService } from '@app/services/audio.service'; class WalletStats implements ChainStats { addresses: string[]; @@ -24,6 +26,7 @@ class WalletStats implements ChainStats { acc.funded_txo_sum += stat.funded_txo_sum; acc.spent_txo_count += stat.spent_txo_count; acc.spent_txo_sum += stat.spent_txo_sum; + acc.tx_count += stat.tx_count; return acc; }, { funded_txo_count: 0, @@ -109,12 +112,17 @@ export class WalletComponent implements OnInit, OnDestroy { addressStrings: string[] = []; walletName: string; isLoadingWallet = true; + isLoadingTransactions = true; + transactions: Transaction[]; + totalTransactionCount: number; + retryLoadMore = false; wallet$: Observable>; walletAddresses$: Observable>; walletSummary$: Observable; walletStats$: Observable; error: any; walletSubscription: Subscription; + transactionSubscription: Subscription; collapseAddresses: boolean = true; @@ -129,6 +137,8 @@ export class WalletComponent implements OnInit, OnDestroy { private websocketService: WebsocketService, private stateService: StateService, private apiService: ApiService, + private electrsApiService: ElectrsApiService, + private audioService: AudioService, private seoService: SeoService, ) { } @@ -172,6 +182,21 @@ export class WalletComponent implements OnInit, OnDestroy { }), switchMap(initial => this.stateService.walletTransactions$.pipe( startWith(null), + tap((transactions) => { + if (!transactions?.length) { + return; + } + for (const transaction of transactions) { + const tx = this.transactions.find((t) => t.txid === transaction.txid); + if (tx) { + tx.status = transaction.status; + } else { + this.transactions.unshift(transaction); + } + } + this.transactions = this.transactions.slice(); + this.audioService.playSound('magic'); + }), scan((wallet, walletTransactions) => { for (const tx of (walletTransactions || [])) { const funded: Record = {}; @@ -267,8 +292,57 @@ export class WalletComponent implements OnInit, OnDestroy { return stats; }, walletStats), ); - }), + }) ); + + this.transactionSubscription = this.wallet$.pipe( + switchMap(wallet => { + const addresses = Object.keys(wallet).map(addr => this.normalizeAddress(addr)); + return this.electrsApiService.getAddressesTransactions$(addresses); + }), + map(transactions => { + // only confirmed transactions supported for now + return transactions.filter(tx => tx.status.confirmed); + }), + catchError((error) => { + console.log(error); + this.error = error; + this.seoService.logSoft404(); + this.isLoadingWallet = false; + return of([]); + }) + ).subscribe((transactions: Transaction[] | null) => { + if (!transactions) { + return; + } + this.transactions = transactions; + this.isLoadingTransactions = false; + }); + } + + loadMore(): void { + if (this.isLoadingTransactions || this.fullyLoaded) { + return; + } + this.isLoadingTransactions = true; + this.retryLoadMore = false; + this.electrsApiService.getAddressesTransactions$(this.addressStrings, this.transactions[this.transactions.length - 1].txid) + .subscribe((transactions: Transaction[]) => { + if (transactions && transactions.length) { + this.transactions = this.transactions.concat(transactions); + } else { + this.fullyLoaded = true; + } + this.isLoadingTransactions = false; + }, + (error) => { + this.isLoadingTransactions = false; + this.retryLoadMore = true; + // In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page. + if (error.status === 422) { + window.location.reload(); + } + }); } deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] { @@ -299,5 +373,6 @@ export class WalletComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.websocketService.stopTrackingWallet(); this.walletSubscription.unsubscribe(); + this.transactionSubscription.unsubscribe(); } } From f77dc68ec71c026c9b7390e6eda79bd9510d2f56 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 4 Oct 2024 22:43:52 +0000 Subject: [PATCH 48/91] Add link to wallet page from custom dashboard txs widget --- .../custom-dashboard/custom-dashboard.component.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html index 13cdd97ce..8ca1a5ac4 100644 --- a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html +++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html @@ -281,9 +281,11 @@
From b65d00f2897969cdc6d2aaf1601dbc62d00f23d7 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 6 Oct 2024 22:29:32 +0000 Subject: [PATCH 49/91] switch multi-address APIs to use POST --- frontend/src/app/services/electrs-api.service.ts | 14 +++++++++----- frontend/src/app/services/state.service.ts | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/services/electrs-api.service.ts b/frontend/src/app/services/electrs-api.service.ts index 3cd5b5abd..6e9697f49 100644 --- a/frontend/src/app/services/electrs-api.service.ts +++ b/frontend/src/app/services/electrs-api.service.ts @@ -142,12 +142,16 @@ export class ElectrsApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params }); } - getAddressesTransactions$(addresses: string[], txid?: string): Observable { + getAddressesTransactions$(addresses: string[], txid?: string): Observable { let params = new HttpParams(); if (txid) { params = params.append('after_txid', txid); } - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs?addresses=${addresses.join(',')}`, { params }); + return this.httpClient.post( + this.apiBaseUrl + this.apiBasePath + '/api/addresses/txs', + addresses, + { params } + ); } getAddressSummary$(address: string, txid?: string): Observable { @@ -163,7 +167,7 @@ export class ElectrsApiService { if (txid) { params = params.append('after_txid', txid); } - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs/summary?addresses=${addresses.join(',')}`, { params }); + return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/addresses/txs/summary', addresses, { params }); } getScriptHashTransactions$(script: string, txid?: string): Observable { @@ -182,7 +186,7 @@ export class ElectrsApiService { params = params.append('after_txid', txid); } return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe( - switchMap(scriptHashes => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs?scripthashes=${scriptHashes.join(',')}`, { params })), + switchMap(scriptHashes => this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/scripthashes/txs', scriptHashes, { params })), ); } @@ -212,7 +216,7 @@ export class ElectrsApiService { params = params.append('after_txid', txid); } return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe( - switchMap(scriptHashes => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs/summary?scripthashes=${scriptHashes.join(',')}`, { params })), + switchMap(scriptHashes => this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/scripthashes/txs/summary', scriptHashes, { params })), ); } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 55f76d6b1..0d006b552 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -171,7 +171,7 @@ export class StateService { mempoolRemovedTransactions$ = new Subject(); multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>(); blockTransactions$ = new Subject(); - walletTransactions$ = new Subject>(); + walletTransactions$ = new Subject(); isLoadingWebSocket$ = new ReplaySubject(1); isLoadingMempool$ = new BehaviorSubject(true); vbytesPerSecond$ = new ReplaySubject(1); From c248544fe806f362c16ab6af69d809588c668523 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 18 Oct 2024 04:39:31 +0000 Subject: [PATCH 50/91] Update wallet page title --- frontend/src/app/components/wallet/wallet.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/wallet/wallet.component.html b/frontend/src/app/components/wallet/wallet.component.html index ed6b82456..9aa82b818 100644 --- a/frontend/src/app/components/wallet/wallet.component.html +++ b/frontend/src/app/components/wallet/wallet.component.html @@ -1,6 +1,6 @@
-

Wallet

+

{{ walletName }}

From 204d54b1896e0a3eeaaa927d565b3bc3fcc1770c Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 22 Dec 2024 10:47:36 +0000 Subject: [PATCH 51/91] fix wallet transactions ordering --- frontend/src/app/components/wallet/wallet.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/wallet/wallet.component.ts b/frontend/src/app/components/wallet/wallet.component.ts index bf5b67ae7..43cc7ee80 100644 --- a/frontend/src/app/components/wallet/wallet.component.ts +++ b/frontend/src/app/components/wallet/wallet.component.ts @@ -302,7 +302,7 @@ export class WalletComponent implements OnInit, OnDestroy { }), map(transactions => { // only confirmed transactions supported for now - return transactions.filter(tx => tx.status.confirmed); + return transactions.filter(tx => tx.status.confirmed).sort((a, b) => b.status.block_height - a.status.block_height); }), catchError((error) => { console.log(error); @@ -329,7 +329,7 @@ export class WalletComponent implements OnInit, OnDestroy { this.electrsApiService.getAddressesTransactions$(this.addressStrings, this.transactions[this.transactions.length - 1].txid) .subscribe((transactions: Transaction[]) => { if (transactions && transactions.length) { - this.transactions = this.transactions.concat(transactions); + this.transactions = this.transactions.concat(transactions.sort((a, b) => b.status.block_height - a.status.block_height)); } else { this.fullyLoaded = true; } From aca2f2ec7d7ab6af419683e23c4d3605bd0a7830 Mon Sep 17 00:00:00 2001 From: wiz Date: Wed, 15 Jan 2025 16:34:56 +0900 Subject: [PATCH 52/91] ops: Set expires -1 header on 2s server cached responses --- production/nginx/location-api.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/production/nginx/location-api.conf b/production/nginx/location-api.conf index 80f513147..b337c0f5b 100644 --- a/production/nginx/location-api.conf +++ b/production/nginx/location-api.conf @@ -140,7 +140,8 @@ location @mempool-api-v1-cache-normal { proxy_cache_valid 200 2s; proxy_redirect off; - expires 2s; + # cache for 2 seconds on server, but send expires -1 so browser doesn't cache + expires -1; } location @mempool-api-v1-cache-disabled { From 4ecf2eb679094291a4d5749778c5cb7f11e44ffc Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 2 Sep 2024 02:28:54 +0000 Subject: [PATCH 53/91] experimental stratum job visualization --- backend/src/api/services/stratum.ts | 101 ++++++++++ backend/src/api/websocket-handler.ts | 28 +++ backend/src/index.ts | 2 + .../stratum-list/stratum-list.component.html | 45 +++++ .../stratum-list/stratum-list.component.scss | 101 ++++++++++ .../stratum-list/stratum-list.component.ts | 185 ++++++++++++++++++ .../src/app/interfaces/websocket.interface.ts | 24 +++ frontend/src/app/master-page.module.ts | 7 +- frontend/src/app/services/mining.service.ts | 5 +- frontend/src/app/services/state.service.ts | 24 ++- .../src/app/services/websocket.service.ts | 24 +++ frontend/src/app/shared/shared.module.ts | 3 + 12 files changed, 543 insertions(+), 6 deletions(-) create mode 100644 backend/src/api/services/stratum.ts create mode 100644 frontend/src/app/components/stratum/stratum-list/stratum-list.component.html create mode 100644 frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss create mode 100644 frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts diff --git a/backend/src/api/services/stratum.ts b/backend/src/api/services/stratum.ts new file mode 100644 index 000000000..5fa3dd7a3 --- /dev/null +++ b/backend/src/api/services/stratum.ts @@ -0,0 +1,101 @@ +import { WebSocket } from 'ws'; +import logger from '../../logger'; +import websocketHandler from '../websocket-handler'; + +export interface StratumJob { + pool: number; + height: number; + coinbase: string; + scriptsig: string; + reward: number; + jobId: string; + extraNonce: string; + extraNonce2Size: number; + prevHash: string; + coinbase1: string; + coinbase2: string; + merkleBranches: string[]; + version: string; + bits: string; + time: string; + timestamp: number; + cleanJobs: boolean; + received: number; +} + +function isStratumJob(obj: any): obj is StratumJob { + return obj + && typeof obj === 'object' + && 'pool' in obj + && 'prevHash' in obj + && 'height' in obj + && 'received' in obj + && 'version' in obj + && 'timestamp' in obj + && 'bits' in obj + && 'merkleBranches' in obj + && 'cleanJobs' in obj; +} + +class StratumApi { + private ws: WebSocket | null = null; + private runWebsocketLoop: boolean = false; + private startedWebsocketLoop: boolean = false; + private websocketConnected: boolean = false; + private jobs: Record = {}; + + public constructor() {} + + public getJobs(): Record { + return this.jobs; + } + + private handleWebsocketMessage(msg: any): void { + if (isStratumJob(msg)) { + this.jobs[msg.pool] = msg; + websocketHandler.handleNewStratumJob(this.jobs[msg.pool]); + } + } + + public async connectWebsocket(): Promise { + this.runWebsocketLoop = true; + if (this.startedWebsocketLoop) { + return; + } + while (this.runWebsocketLoop) { + this.startedWebsocketLoop = true; + if (!this.ws) { + this.ws = new WebSocket(`http://localhost:3333`); + this.websocketConnected = true; + + this.ws.on('open', () => { + logger.info('Stratum websocket opened'); + }); + + this.ws.on('error', (error) => { + logger.err('Stratum websocket error: ' + error); + this.ws = null; + this.websocketConnected = false; + }); + + this.ws.on('close', () => { + logger.info('Stratum websocket closed'); + this.ws = null; + this.websocketConnected = false; + }); + + this.ws.on('message', (data, isBinary) => { + try { + const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string); + this.handleWebsocketMessage(parsedMsg); + } catch (e) { + logger.warn('Failed to parse stratum websocket message: ' + (e instanceof Error ? e.message : e)); + } + }); + } + await new Promise(resolve => setTimeout(resolve, 5000)); + } + } +} + +export default new StratumApi(); \ No newline at end of file diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 13e27c360..390896caa 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -38,6 +38,7 @@ interface AddressTransactions { import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import { calculateMempoolTxCpfp } from './cpfp'; import { getRecentFirstSeen } from '../utils/file-read'; +import stratumApi, { StratumJob } from './services/stratum'; // valid 'want' subscriptions const wantable = [ @@ -403,6 +404,16 @@ class WebsocketHandler { delete client['track-mempool']; } + if (parsedMessage && parsedMessage['track-stratum'] != null) { + if (parsedMessage['track-stratum']) { + const sub = parsedMessage['track-stratum']; + client['track-stratum'] = sub; + response['stratumJobs'] = this.socketData['stratumJobs']; + } else { + client['track-stratum'] = false; + } + } + if (Object.keys(response).length) { client.send(this.serializeResponse(response)); } @@ -1384,6 +1395,23 @@ class WebsocketHandler { await statistics.runStatistics(); } + public handleNewStratumJob(job: StratumJob): void { + this.updateSocketDataFields({ 'stratumJobs': stratumApi.getJobs() }); + + for (const server of this.webSocketServers) { + server.clients.forEach((client) => { + if (client.readyState !== WebSocket.OPEN) { + return; + } + if (client['track-stratum'] && (client['track-stratum'] === 'all' || client['track-stratum'] === job.pool)) { + client.send(JSON.stringify({ + 'stratumJob': job + })); + } + }); + } + } + // takes a dictionary of JSON serialized values // and zips it together into a valid JSON object private serializeResponse(response): string { diff --git a/backend/src/index.ts b/backend/src/index.ts index c179b66bc..53c4c2f22 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -48,6 +48,7 @@ import accelerationRoutes from './api/acceleration/acceleration.routes'; import aboutRoutes from './api/about.routes'; import mempoolBlocks from './api/mempool-blocks'; import walletApi from './api/services/wallets'; +import stratumApi from './api/services/stratum'; class Server { private wss: WebSocket.Server | undefined; @@ -320,6 +321,7 @@ class Server { loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler)); accelerationApi.connectWebsocket(); + stratumApi.connectWebsocket(); } setUpHttpApiRoutes(): void { diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html new file mode 100644 index 000000000..6132035be --- /dev/null +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html @@ -0,0 +1,45 @@ +
+

Stratum Jobs

+ +
+ +
+ + + + + + + + + + + @for (row of rows; track row.job.pool) { + + + + @for (cell of row.merkleCells; track $index) { + + } + + + } + +
HeightReward + Merkle Branches + Pool
+ {{ row.job.height }} + + + +
+
+ @if (pools[row.job.pool]) { + + + {{ pools[row.job.pool].name}} + + } +
+
+
diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss new file mode 100644 index 000000000..da0e63967 --- /dev/null +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss @@ -0,0 +1,101 @@ +.stratum-table { + width: 100%; +} + +td { + position: relative; + height: 2em; + + &.height, &.reward { + padding: 0 5px; + } + + &.pool { + padding-left: 5px; + padding-right: 20px; + } + + &.merkle { + width: 100px; + .pipe-segment { + position: absolute; + border-color: white; + box-sizing: content-box; + + &.vertical { + top: 0; + right: 0; + width: 50%; + height: 100%; + border-left: solid 4px; + } + &.horizontal { + bottom: 0; + left: 0; + width: 100%; + height: 50%; + border-top: solid 4px; + } + &.branch-top { + bottom: 0; + right: 0; + width: 100%; + height: 50%; + border-top: solid 4px; + &::after { + content: ""; + position: absolute; + box-sizing: content-box; + top: -4px; + right: 0px; + bottom: 0; + width: 50%; + border-top: solid 4px; + border-left: solid 4px; + border-top-left-radius: 5px; + } + } + &.branch-mid { + bottom: 0; + right: 0px; + width: 50%; + height: 100%; + border-left: solid 4px; + &::after { + content: ""; + position: absolute; + box-sizing: content-box; + top: -4px; + left: -4px; + width: 100%; + height: 50%; + border-bottom: solid 4px; + border-left: solid 4px; + border-bottom-left-radius: 5px; + } + } + &.branch-end { + top: -4px; + right: 0; + width: 50%; + height: 50%; + border-bottom-left-radius: 5px; + border-bottom: solid 4px; + border-left: solid 4px; + } + } + } +} + +.badge { + position: relative; + color: #FFF; +} + +.pool-logo { + width: 15px; + height: 15px; + position: relative; + top: -1px; + margin-right: 2px; +} \ No newline at end of file diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts new file mode 100644 index 000000000..25bb42d1c --- /dev/null +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts @@ -0,0 +1,185 @@ +import { Component, OnInit, ChangeDetectionStrategy, OnDestroy, ChangeDetectorRef } from '@angular/core'; +import { StateService } from '../../../services/state.service'; +import { WebsocketService } from '../../../services/websocket.service'; +import { map, Observable } from 'rxjs'; +import { StratumJob } from '../../../interfaces/websocket.interface'; +import { MiningService } from '../../../services/mining.service'; +import { SinglePoolStats } from '../../../interfaces/node-api.interface'; + +type MerkleCellType = ' ' | '┬' | '├' | '└' | '│' | '─' | 'leaf'; + +interface MerkleCell { + hash: string; + type: MerkleCellType; + job?: StratumJob; +} + +interface MerkleTree { + hash?: string; + job: string; + size: number; + children?: MerkleTree[]; +} + +interface PoolRow { + job: StratumJob; + merkleCells: MerkleCell[]; +} + +@Component({ + selector: 'app-stratum-list', + templateUrl: './stratum-list.component.html', + styleUrls: ['./stratum-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class StratumList implements OnInit, OnDestroy { + rows$: Observable; + pools: { [id: number]: SinglePoolStats } = {}; + poolsReady: boolean = false; + + constructor( + private stateService: StateService, + private websocketService: WebsocketService, + private miningService: MiningService, + private cd: ChangeDetectorRef, + ) {} + + ngOnInit(): void { + this.miningService.getPools().subscribe(pools => { + this.pools = {}; + for (const pool of pools) { + this.pools[pool.unique_id] = pool; + } + this.poolsReady = true; + this.cd.markForCheck(); + }); + this.rows$ = this.stateService.stratumJobs$.pipe( + map((jobs) => this.processJobs(jobs)), + ); + this.websocketService.startTrackStratum('all'); + } + + processJobs(jobs: Record): PoolRow[] { + if (Object.keys(jobs).length === 0) { + return []; + } + + const numBranches = Math.max(...Object.values(jobs).map(job => job.merkleBranches.length)); + + let trees: MerkleTree[] = Object.keys(jobs).map(job => ({ + job, + size: 1, + })); + + // build tree from bottom up + for (let col = numBranches - 1; col >= 0; col--) { + const groups: Record = {}; + for (const tree of trees) { + const hash = jobs[tree.job].merkleBranches[col]; + if (!groups[hash]) { + groups[hash] = []; + } + groups[hash].push(tree); + } + trees = Object.values(groups).map(group => ({ + hash: jobs[group[0].job].merkleBranches[col], + job: group[0].job, + children: group, + size: group.reduce((acc, tree) => acc + tree.size, 0), + })); + } + + // initialize grid of cells + const rows: (MerkleCell | null)[][] = []; + for (let i = 0; i < Object.keys(jobs).length; i++) { + const row: (MerkleCell | null)[] = []; + for (let j = 0; j <= numBranches; j++) { + row.push(null); + } + rows.push(row); + } + + // fill in the cells + let colTrees = [trees.sort((a, b) => { + if (a.size !== b.size) { + return b.size - a.size; + } + return a.job.localeCompare(b.job); + })]; + for (let col = 0; col <= numBranches; col++) { + let row = 0; + const nextTrees: MerkleTree[][] = []; + for (let g = 0; g < colTrees.length; g++) { + for (let t = 0; t < colTrees[g].length; t++) { + const tree = colTrees[g][t]; + const isFirstTree = (t === 0); + const isLastTree = (t === colTrees[g].length - 1); + for (let i = 0; i < tree.size; i++) { + const isFirstCell = (i === 0); + const isLeaf = (col === numBranches); + rows[row][col] = { + hash: tree.hash, + job: isLeaf ? jobs[tree.job] : undefined, + type: 'leaf', + }; + if (col > 0) { + rows[row][col - 1].type = getCellType(isFirstCell, isFirstTree, isLastTree); + } + row++; + } + if (tree.children) { + nextTrees.push(tree.children.sort((a, b) => { + if (a.size !== b.size) { + return b.size - a.size; + } + return a.job.localeCompare(b.job); + })); + } + } + } + colTrees = nextTrees; + } + return rows.map(row => ({ + job: row[row.length - 1].job, + merkleCells: row.slice(0, -1), + })); + } + + pipeToClass(type: MerkleCellType): string { + return { + ' ': 'empty', + '┬': 'branch-top', + '├': 'branch-mid', + '└': 'branch-end', + '│': 'vertical', + '─': 'horizontal', + 'leaf': 'leaf' + }[type]; + } + + ngOnDestroy(): void { + this.websocketService.stopTrackStratum(); + } +} + +function getCellType(isFirstCell, isFirstTree, isLastTree): MerkleCellType { + if (isFirstCell) { + if (isFirstTree) { + if (isLastTree) { + return '─'; + } else { + return '┬'; + } + } else if (isLastTree) { + return '└'; + } else { + return '├'; + } + } else { + if (isLastTree) { + return ' '; + } else { + return '│'; + } + } +} diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index d61610a2e..9281f0fc7 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -21,6 +21,8 @@ export interface WebsocketResponse { rbfInfo?: RbfTree; rbfLatest?: RbfTree[]; rbfLatestSummary?: ReplacementInfo[]; + stratumJob?: StratumJob; + stratumJobs?: Record; utxoSpent?: object; transactions?: TransactionStripped[]; loadingIndicators?: ILoadingIndicators; @@ -37,6 +39,7 @@ export interface WebsocketResponse { 'track-rbf-summary'?: boolean; 'track-accelerations'?: boolean; 'track-wallet'?: string; + 'track-stratum'?: string | number; 'watch-mempool'?: boolean; 'refresh-blocks'?: boolean; } @@ -150,3 +153,24 @@ export interface HealthCheckHost { electrs?: string; } } + +export interface StratumJob { + pool: number; + height: number; + coinbase: string; + scriptsig: string; + reward: number; + jobId: string; + extraNonce: string; + extraNonce2Size: number; + prevHash: string; + coinbase1: string; + coinbase2: string; + merkleBranches: string[]; + version: string; + bits: string; + time: string; + timestamp: number; + cleanJobs: boolean; + received: number; +} diff --git a/frontend/src/app/master-page.module.ts b/frontend/src/app/master-page.module.ts index 2ee2e0bd8..35b632ab5 100644 --- a/frontend/src/app/master-page.module.ts +++ b/frontend/src/app/master-page.module.ts @@ -10,9 +10,10 @@ import { TestTransactionsComponent } from '@components/test-transactions/test-tr import { CalculatorComponent } from '@components/calculator/calculator.component'; import { BlocksList } from '@components/blocks-list/blocks-list.component'; import { RbfList } from '@components/rbf-list/rbf-list.component'; +import { StratumList } from '@components/stratum/stratum-list/stratum-list.component'; import { ServerHealthComponent } from '@components/server-health/server-health.component'; import { ServerStatusComponent } from '@components/server-health/server-status.component'; -import { FaucetComponent } from '@components/faucet/faucet.component' +import { FaucetComponent } from '@components/faucet/faucet.component'; const browserWindow = window || {}; // @ts-ignore @@ -56,6 +57,10 @@ const routes: Routes = [ path: 'rbf', component: RbfList, }, + { + path: 'stratum', + component: StratumList, + }, { path: 'terms-of-service', loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule), diff --git a/frontend/src/app/services/mining.service.ts b/frontend/src/app/services/mining.service.ts index 760ce93cb..a181ef771 100644 --- a/frontend/src/app/services/mining.service.ts +++ b/frontend/src/app/services/mining.service.ts @@ -64,8 +64,8 @@ export class MiningService { ); } } - - /** + + /** * Get names and slugs of all pools */ public getPools(): Observable { @@ -75,7 +75,6 @@ export class MiningService { return this.poolsData; }) ); - } /** * Set the hashrate power of ten we want to display diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 0d006b552..9d2c2ed7b 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs'; -import { AddressTxSummary, Transaction } from '@interfaces/electrs.interface'; -import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '@interfaces/websocket.interface'; +import { Transaction } from '@interfaces/electrs.interface'; +import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, StratumJob, isMempoolState } from '@interfaces/websocket.interface'; import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '@interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; @@ -159,6 +159,8 @@ export class StateService { liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>; accelerations$ = new Subject(); liveAccelerations$: Observable; + stratumJobUpdate$ = new Subject<{ state: Record } | { job: StratumJob }>(); + stratumJobs$ = new BehaviorSubject>({}); txConfirmed$ = new Subject<[string, BlockExtended]>(); txReplaced$ = new Subject(); txRbfInfo$ = new Subject(); @@ -303,6 +305,24 @@ export class StateService { map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added)) ); + this.stratumJobUpdate$.pipe( + scan((acc: Record, update: { state: Record } | { job: StratumJob }) => { + if ('state' in update) { + // Replace the entire state + return update.state; + } else { + // Update or create a single job entry + return { + ...acc, + [update.job.pool]: update.job + }; + } + }, {}), + shareReplay(1) + ).subscribe(val => { + this.stratumJobs$.next(val); + }); + this.networkChanged$.subscribe((network) => { this.transactions$ = new BehaviorSubject(null); this.blocksSubject$.next([]); diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 0f5368244..b82b32dd5 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -36,6 +36,7 @@ export class WebsocketService { private isTrackingAccelerations: boolean = false; private isTrackingWallet: boolean = false; private trackingWalletName: string; + private isTrackingStratum: string | number | false = false; private trackingMempoolBlock: number; private trackingMempoolBlockNetwork: string; private stoppingTrackMempoolBlock: any | null = null; @@ -143,6 +144,9 @@ export class WebsocketService { if (this.isTrackingWallet) { this.startTrackingWallet(this.trackingWalletName); } + if (this.isTrackingStratum !== false) { + this.startTrackStratum(this.isTrackingStratum); + } this.stateService.connectionState$.next(2); } @@ -289,6 +293,18 @@ export class WebsocketService { } } + startTrackStratum(pool: number | string) { + this.websocketSubject.next({ 'track-stratum': pool }); + this.isTrackingStratum = pool; + } + + stopTrackStratum() { + if (this.isTrackingStratum) { + this.websocketSubject.next({ 'track-stratum': null }); + this.isTrackingStratum = false; + } + } + fetchStatistics(historicalDate: string) { this.websocketSubject.next({ historicalDate }); } @@ -512,6 +528,14 @@ export class WebsocketService { this.stateService.previousRetarget$.next(response.previousRetarget); } + if (response.stratumJobs) { + this.stateService.stratumJobUpdate$.next({ state: response.stratumJobs }); + } + + if (response.stratumJob) { + this.stateService.stratumJobUpdate$.next({ job: response.stratumJob }); + } + if (response['tomahawk']) { this.stateService.serverHealth$.next(response['tomahawk']); } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 2bcfc1255..1d4f5fd99 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -83,6 +83,7 @@ import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe'; import { DifficultyAdjustmentsTable } from '@components/difficulty-adjustments-table/difficulty-adjustments-table.components'; import { BlocksList } from '@components/blocks-list/blocks-list.component'; import { RbfList } from '@components/rbf-list/rbf-list.component'; +import { StratumList } from '@components/stratum/stratum-list/stratum-list.component'; import { RewardStatsComponent } from '@components/reward-stats/reward-stats.component'; import { DataCyDirective } from '@app/data-cy.directive'; import { LoadingIndicatorComponent } from '@components/loading-indicator/loading-indicator.component'; @@ -201,6 +202,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/ DifficultyAdjustmentsTable, BlocksList, RbfList, + StratumList, DataCyDirective, RewardStatsComponent, LoadingIndicatorComponent, @@ -345,6 +347,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/ AmountShortenerPipe, DifficultyAdjustmentsTable, BlocksList, + StratumList, DataCyDirective, RewardStatsComponent, LoadingIndicatorComponent, From eddd7344ad95dee32c8816a87dd0be08a2d887a7 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 6 Sep 2024 17:44:56 +0000 Subject: [PATCH 54/91] stratum backend config --- backend/src/__fixtures__/mempool-config.template.json | 4 ++++ backend/src/__tests__/config.test.ts | 5 +++++ backend/src/api/services/stratum.ts | 6 +++++- backend/src/config.ts | 10 ++++++++++ backend/src/index.ts | 4 +++- 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/backend/src/__fixtures__/mempool-config.template.json b/backend/src/__fixtures__/mempool-config.template.json index a9f246767..d3380a2ee 100644 --- a/backend/src/__fixtures__/mempool-config.template.json +++ b/backend/src/__fixtures__/mempool-config.template.json @@ -151,5 +151,9 @@ "ENABLED": true, "PAID": false, "API_KEY": "__MEMPOOL_CURRENCY_API_KEY__" + }, + "STRATUM": { + "ENABLED": false, + "API": "http://127.0.0.1:1234" } } diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts index b3cf7e2a7..e76d22545 100644 --- a/backend/src/__tests__/config.test.ts +++ b/backend/src/__tests__/config.test.ts @@ -159,6 +159,11 @@ describe('Mempool Backend Config', () => { PAID: false, API_KEY: '', }); + + expect(config.STRATUM).toStrictEqual({ + ENABLED: false, + API: 'http://127.0.0.1:1234', + }); }); }); diff --git a/backend/src/api/services/stratum.ts b/backend/src/api/services/stratum.ts index 5fa3dd7a3..a8ee64106 100644 --- a/backend/src/api/services/stratum.ts +++ b/backend/src/api/services/stratum.ts @@ -1,5 +1,6 @@ import { WebSocket } from 'ws'; import logger from '../../logger'; +import config from '../../config'; import websocketHandler from '../websocket-handler'; export interface StratumJob { @@ -58,6 +59,9 @@ class StratumApi { } public async connectWebsocket(): Promise { + if (!config.STRATUM.ENABLED) { + return; + } this.runWebsocketLoop = true; if (this.startedWebsocketLoop) { return; @@ -65,7 +69,7 @@ class StratumApi { while (this.runWebsocketLoop) { this.startedWebsocketLoop = true; if (!this.ws) { - this.ws = new WebSocket(`http://localhost:3333`); + this.ws = new WebSocket(`${config.STRATUM.API}`); this.websocketConnected = true; this.ws.on('open', () => { diff --git a/backend/src/config.ts b/backend/src/config.ts index 794421551..0f1f44369 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -165,6 +165,10 @@ interface IConfig { WALLETS: { ENABLED: boolean; WALLETS: string[]; + }, + STRATUM: { + ENABLED: boolean; + API: string; } } @@ -332,6 +336,10 @@ const defaults: IConfig = { 'ENABLED': false, 'WALLETS': [], }, + 'STRATUM': { + 'ENABLED': false, + 'API': 'http://127.0.0.1:1234', + } }; class Config implements IConfig { @@ -354,6 +362,7 @@ class Config implements IConfig { REDIS: IConfig['REDIS']; FIAT_PRICE: IConfig['FIAT_PRICE']; WALLETS: IConfig['WALLETS']; + STRATUM: IConfig['STRATUM']; constructor() { const configs = this.merge(configFromFile, defaults); @@ -376,6 +385,7 @@ class Config implements IConfig { this.REDIS = configs.REDIS; this.FIAT_PRICE = configs.FIAT_PRICE; this.WALLETS = configs.WALLETS; + this.STRATUM = configs.STRATUM; } merge = (...objects: object[]): IConfig => { diff --git a/backend/src/index.ts b/backend/src/index.ts index 53c4c2f22..dc6a8ae1a 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -321,7 +321,9 @@ class Server { loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler)); accelerationApi.connectWebsocket(); - stratumApi.connectWebsocket(); + if (config.STRATUM.ENABLED) { + stratumApi.connectWebsocket(); + } } setUpHttpApiRoutes(): void { From 3ea491ad13b8a61927d7218192a0fdde1ed24ff9 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 9 Sep 2024 23:26:59 +0000 Subject: [PATCH 55/91] stratum frontend config --- frontend/mempool-frontend-config.sample.json | 1 + frontend/src/app/services/state.service.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/frontend/mempool-frontend-config.sample.json b/frontend/mempool-frontend-config.sample.json index f9f2576d6..70dc2edba 100644 --- a/frontend/mempool-frontend-config.sample.json +++ b/frontend/mempool-frontend-config.sample.json @@ -27,5 +27,6 @@ "ACCELERATOR": false, "ACCELERATOR_BUTTON": true, "PUBLIC_ACCELERATIONS": false, + "STRATUM_ENABLED": false, "SERVICES_API": "https://mempool.space/api/v1/services" } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 9d2c2ed7b..7f8f81744 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -81,6 +81,7 @@ export interface Env { ADDITIONAL_CURRENCIES: boolean; GIT_COMMIT_HASH_MEMPOOL_SPACE?: string; PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string; + STRATUM_ENABLED: boolean; SERVICES_API?: string; customize?: Customization; PROD_DOMAINS: string[]; @@ -123,6 +124,7 @@ const defaultEnv: Env = { 'ACCELERATOR_BUTTON': true, 'PUBLIC_ACCELERATIONS': false, 'ADDITIONAL_CURRENCIES': false, + 'STRATUM_ENABLED': false, 'SERVICES_API': 'https://mempool.space/api/v1/services', 'PROD_DOMAINS': [], }; From cb4bf0611e030143da0f7cbdbdcc46ef329c858a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 9 Sep 2024 23:27:29 +0000 Subject: [PATCH 56/91] add blockchain bar to stratum page --- .../stratum/stratum-list/stratum-list.component.ts | 1 + frontend/src/app/master-page.module.ts | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts index 25bb42d1c..af34fd091 100644 --- a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts @@ -45,6 +45,7 @@ export class StratumList implements OnInit, OnDestroy { ) {} ngOnInit(): void { + this.websocketService.want(['stats', 'blocks', 'mempool-blocks']); this.miningService.getPools().subscribe(pools => { this.pools = {}; for (const pool of pools) { diff --git a/frontend/src/app/master-page.module.ts b/frontend/src/app/master-page.module.ts index 35b632ab5..f0af944cc 100644 --- a/frontend/src/app/master-page.module.ts +++ b/frontend/src/app/master-page.module.ts @@ -57,10 +57,16 @@ const routes: Routes = [ path: 'rbf', component: RbfList, }, - { + ...(browserWindowEnv.STRATUM_ENABLED ? [{ path: 'stratum', - component: StratumList, - }, + component: StartComponent, + children: [ + { + path: '', + component: StratumList, + } + ] + }] : []), { path: 'terms-of-service', loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule), From 9ba7172b5bb829061aa6f88268a55f9dccc3f1e3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 28 Sep 2024 14:51:05 +0000 Subject: [PATCH 57/91] add coinbase tag column to stratum table --- .../stratum-list/stratum-list.component.html | 4 +++ .../stratum-list/stratum-list.component.scss | 9 +++++- .../stratum-list/stratum-list.component.ts | 29 +++++++++++++++++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html index 6132035be..08d7fb0ef 100644 --- a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html @@ -9,6 +9,7 @@ Height Reward + Coinbase Tag Merkle Branches @@ -24,6 +25,9 @@ + + {{ row.job.tag }} + @for (cell of row.merkleCells; track $index) {
diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss index da0e63967..6679f2257 100644 --- a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss @@ -6,9 +6,16 @@ td { position: relative; height: 2em; - &.height, &.reward { + &.height, &.reward, &.tag { padding: 0 5px; } + + &.tag { + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } &.pool { padding-left: 5px; diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts index af34fd091..1ab1a1c94 100644 --- a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts @@ -8,10 +8,14 @@ import { SinglePoolStats } from '../../../interfaces/node-api.interface'; type MerkleCellType = ' ' | '┬' | '├' | '└' | '│' | '─' | 'leaf'; +interface TaggedStratumJob extends StratumJob { + tag: string; +} + interface MerkleCell { hash: string; type: MerkleCellType; - job?: StratumJob; + job?: TaggedStratumJob; } interface MerkleTree { @@ -22,10 +26,25 @@ interface MerkleTree { } interface PoolRow { - job: StratumJob; + job: TaggedStratumJob; merkleCells: MerkleCell[]; } +function parseTag(scriptSig: string): string { + const hex = scriptSig.slice(8).replace(/6d6d.{64}/, ''); + const bytes: number[] = []; + for (let i = 0; i < hex.length; i += 2) { + bytes.push(parseInt(hex.substr(i, 2), 16)); + } + const ascii = new TextDecoder('utf8').decode(Uint8Array.from(bytes)).replace(/\uFFFD/g, '').replace(/\\0/g, ''); + if (ascii.includes('/ViaBTC/')) { + return '/ViaBTC/'; + } else if (ascii.includes('SpiderPool/')) { + return 'SpiderPool/'; + } + return ascii.match(/\/.*\//)?.[0] || ascii; +} + @Component({ selector: 'app-stratum-list', templateUrl: './stratum-list.component.html', @@ -60,7 +79,11 @@ export class StratumList implements OnInit, OnDestroy { this.websocketService.startTrackStratum('all'); } - processJobs(jobs: Record): PoolRow[] { + processJobs(rawJobs: Record): PoolRow[] { + const jobs: Record = {}; + for (const [id, job] of Object.entries(rawJobs)) { + jobs[id] = { ...job, tag: parseTag(job.scriptsig) }; + } if (Object.keys(jobs).length === 0) { return []; } From d6283c54eef688fdfdf610327b7630d4d9b941fe Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 25 Oct 2024 07:40:54 +0000 Subject: [PATCH 58/91] fix stratum config --- backend/mempool-config.sample.json | 4 ++++ backend/src/__fixtures__/mempool-config.template.json | 2 +- backend/src/__tests__/config.test.ts | 2 +- backend/src/config.ts | 2 +- docker/backend/mempool-config.json | 4 ++++ docker/backend/start.sh | 8 ++++++++ 6 files changed, 19 insertions(+), 3 deletions(-) diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 7ad25dff0..c2715153b 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -155,6 +155,10 @@ "API": "https://mempool.space/api/v1/services", "ACCELERATIONS": false }, + "STRATUM": { + "ENABLED": false, + "API": "http://localhost:1234" + }, "FIAT_PRICE": { "ENABLED": true, "PAID": false, diff --git a/backend/src/__fixtures__/mempool-config.template.json b/backend/src/__fixtures__/mempool-config.template.json index d3380a2ee..0ca5654a5 100644 --- a/backend/src/__fixtures__/mempool-config.template.json +++ b/backend/src/__fixtures__/mempool-config.template.json @@ -154,6 +154,6 @@ }, "STRATUM": { "ENABLED": false, - "API": "http://127.0.0.1:1234" + "API": "http://localhost:1234" } } diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts index e76d22545..e0437941f 100644 --- a/backend/src/__tests__/config.test.ts +++ b/backend/src/__tests__/config.test.ts @@ -162,7 +162,7 @@ describe('Mempool Backend Config', () => { expect(config.STRATUM).toStrictEqual({ ENABLED: false, - API: 'http://127.0.0.1:1234', + API: 'http://localhost:1234', }); }); }); diff --git a/backend/src/config.ts b/backend/src/config.ts index 0f1f44369..a1050a7d5 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -338,7 +338,7 @@ const defaults: IConfig = { }, 'STRATUM': { 'ENABLED': false, - 'API': 'http://127.0.0.1:1234', + 'API': 'http://localhost:1234', } }; diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json index c7ade9b7b..ee8e329a6 100644 --- a/docker/backend/mempool-config.json +++ b/docker/backend/mempool-config.json @@ -148,6 +148,10 @@ "API": "__MEMPOOL_SERVICES_API__", "ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__ }, + "STRATUM": { + "ENABLED": __STRATUM_ENABLED__, + "API": "__STRATUM_API__" + }, "REDIS": { "ENABLED": __REDIS_ENABLED__, "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__", diff --git a/docker/backend/start.sh b/docker/backend/start.sh index d4765972e..8adb631da 100755 --- a/docker/backend/start.sh +++ b/docker/backend/start.sh @@ -149,6 +149,10 @@ __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]} __MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"} __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false} +# STRATUM +__STRATUM_ENABLED__=${STRATUM_ENABLED:=false} +__STRATUM_API__=${STRATUM_API:="http://localhost:1234"} + # REDIS __REDIS_ENABLED__=${REDIS_ENABLED:=false} __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""} @@ -300,6 +304,10 @@ sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.j sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json +# STRATUM +sed -i "s!__STRATUM_ENABLED__!${__STRATUM_ENABLED__}!g" mempool-config.json +sed -i "s!__STRATUM_API__!${__STRATUM_API__}!g" mempool-config.json + # REDIS sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json From b254be2f49b7491086743cb5ddeb6b880ea2a813 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 6 Sep 2024 17:46:12 +0000 Subject: [PATCH 59/91] add stratum job to pool page --- .../app/components/pool/pool.component.html | 114 ++++++++- .../app/components/pool/pool.component.scss | 232 +++++++++++------- .../src/app/components/pool/pool.component.ts | 36 ++- .../stratum-list/stratum-list.component.ts | 5 +- 4 files changed, 292 insertions(+), 95 deletions(-) diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index b3c6430a8..faa0003c4 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -10,7 +10,7 @@

{{ poolStats.pool.name }}

-
+
@@ -173,7 +173,119 @@
+ + +

Next block

+
+
+
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
HeightExpectedRewardTimestamp
+ {{ job.height }} + + + + + ~ + + + + +
+
+ + + + + + + + + + + + + + + + + +
Coinbase tagCleanPrevhashJob Received
+ {{ job.scriptsig | hex2ascii }} + + @if (job.cleanJobs) { + + } @else { + + } + + + + + + +
+
+ + + + + + + + + @for (branch of job.merkleBranches; track $index) { + + } + @for (_ of [].constructor(Math.max(0, 12 - job.merkleBranches.length)); track $index) { + + } + + +
+ + Merkle Branches +   + + +
+
+
+
+
+
+ +

Blocks

diff --git a/frontend/src/app/components/pool/pool.component.scss b/frontend/src/app/components/pool/pool.component.scss index 5c2fedd26..31d12474f 100644 --- a/frontend/src/app/components/pool/pool.component.scss +++ b/frontend/src/app/components/pool/pool.component.scss @@ -49,111 +49,110 @@ div.scrollable { max-height: 75px; } -.box { - padding-bottom: 5px; +.pool-details { @media (min-width: 767.98px) { min-height: 187px; } -} -.label { - width: 25%; - @media (min-width: 767.98px) { - vertical-align: middle; + .label { + width: 25%; + @media (min-width: 767.98px) { + vertical-align: middle; + } + @media (max-width: 767.98px) { + font-weight: bold; + } } - @media (max-width: 767.98px) { - font-weight: bold; + .label.addresses { + vertical-align: top; + padding-top: 25px; + } + .addresses-data { + vertical-align: top; + font-family: monospace; + font-size: 14px; } -} -.label.addresses { - vertical-align: top; - padding-top: 25px; -} -.addresses-data { - vertical-align: top; - font-family: monospace; - font-size: 14px; -} -.data { - text-align: right; - padding-left: 5%; - @media (max-width: 992px) { - text-align: left; - padding-left: 12px; - } - @media (max-width: 450px) { + .data { text-align: right; + padding-left: 5%; + @media (max-width: 992px) { + text-align: left; + padding-left: 12px; + } + @media (max-width: 450px) { + text-align: right; + } } -} -.progress { - background-color: var(--secondary); -} + .progress { + background-color: var(--secondary); + } -.coinbase { - width: 20%; - @media (max-width: 875px) { - display: none; - } -} - -.height { - width: 10%; -} - -.timestamp { - @media (max-width: 875px) { - padding-left: 50px; - } - @media (max-width: 685px) { - display: none; - } -} - -.mined { - width: 13%; - @media (max-width: 1100px) { - display: none; - } -} - -.txs { - padding-right: 40px; - @media (max-width: 1100px) { - padding-right: 10px; - } - @media (max-width: 875px) { - padding-right: 20px; - } - @media (max-width: 567px) { - padding-right: 10px; - } -} - -.size { - width: 12%; - @media (max-width: 1000px) { - width: 15%; - } - @media (max-width: 875px) { + .coinbase { width: 20%; + @media (max-width: 875px) { + display: none; + } } - @media (max-width: 650px) { - width: 20%; - } - @media (max-width: 450px) { - display: none; - } -} -.scriptmessage { - overflow: hidden; - display: inline-block; - text-overflow: ellipsis; - vertical-align: middle; - width: auto; - text-align: left; + .height { + width: 10%; + } + + .timestamp { + @media (max-width: 875px) { + padding-left: 50px; + } + @media (max-width: 685px) { + display: none; + } + } + + .mined { + width: 13%; + @media (max-width: 1100px) { + display: none; + } + } + + .txs { + padding-right: 40px; + @media (max-width: 1100px) { + padding-right: 10px; + } + @media (max-width: 875px) { + padding-right: 20px; + } + @media (max-width: 567px) { + padding-right: 10px; + } + } + + .size { + width: 12%; + @media (max-width: 1000px) { + width: 15%; + } + @media (max-width: 875px) { + width: 20%; + } + @media (max-width: 650px) { + width: 20%; + } + @media (max-width: 450px) { + display: none; + } + } + + .scriptmessage { + overflow: hidden; + display: inline-block; + text-overflow: ellipsis; + vertical-align: middle; + width: auto; + text-align: left; + } } .skeleton-loader { @@ -214,4 +213,55 @@ div.scrollable { .taller-row { height: 75px; +} + +.stratum-table { + width: 100%; + + .merkle { + width: 100px; + } + + .empty-branch { + outline: solid 1px white; + outline-offset: -1px; + + &::after { + content: ""; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 100%; + background: linear-gradient(to top left, transparent, transparent 48%, white 49%, white 51%, transparent 52%, transparent); + } + } + + td { + position: relative; + height: 2em; + } +} + +.job-table { + td, th { + width: 25%; + max-width: 25%; + min-width: 25%; + overflow: hidden; + text-overflow: ellipsis; + padding: 0.1rem 0.2rem; + } + + @media (max-width: 767.98px) { + .expected, .timestamp, .clean, .job-received { + display: none; + } + } +} + +.title-link, .title-link:hover, .title-link:focus, .title-link:active { + display: block; + text-decoration: none; + color: inherit; } \ No newline at end of file diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 1893f0a48..23b795613 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -10,6 +10,9 @@ import { selectPowerOfTen } from '@app/bitcoin.utils'; import { formatNumber } from '@angular/common'; import { SeoService } from '@app/services/seo.service'; import { HttpErrorResponse } from '@angular/common/http'; +import { StratumJob } from '../../interfaces/websocket.interface'; +import { WebsocketService } from '../../services/websocket.service'; +import { MiningService } from '../../services/mining.service'; interface AccelerationTotal { cost: number, @@ -27,12 +30,16 @@ export class PoolComponent implements OnInit { @Input() left: number | string = 75; gfg = true; + stratumEnabled = this.stateService.env.STRATUM_ENABLED; formatNumber = formatNumber; + Math = Math; slugSubscription: Subscription; poolStats$: Observable; blocks$: Observable; oobFees$: Observable; + job$: Observable; + expectedBlockTime$: Observable; isLoading = true; error: HttpErrorResponse | null = null; @@ -53,6 +60,8 @@ export class PoolComponent implements OnInit { private apiService: ApiService, private route: ActivatedRoute, public stateService: StateService, + private websocketService: WebsocketService, + private miningService: MiningService, private seoService: SeoService, ) { this.auditAvailable = this.stateService.env.AUDIT; @@ -62,7 +71,7 @@ export class PoolComponent implements OnInit { this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => { this.isLoading = true; this.blocks = []; - this.chartOptions = {}; + this.chartOptions = {}; this.slug = slug; this.initializeObservables(); }); @@ -129,6 +138,31 @@ export class PoolComponent implements OnInit { }), filter(oob => oob.length === 3 && oob[2].count > 0) ); + + if (this.stratumEnabled) { + this.job$ = combineLatest([ + this.poolStats$.pipe( + tap((poolStats) => { + this.websocketService.startTrackStratum(poolStats.pool.unique_id); + }) + ), + this.stateService.stratumJobs$ + ]).pipe( + map(([poolStats, jobs]) => { + return jobs[poolStats.pool.unique_id]; + }) + ); + + this.expectedBlockTime$ = combineLatest([ + this.miningService.getMiningStats('1w'), + this.poolStats$, + this.stateService.difficultyAdjustment$ + ]).pipe( + map(([miningStats, poolStat, da]) => { + return (da.timeAvg / ((poolStat.estimatedHashrate || 0) / (miningStats.lastEstimatedHashrate * 1_000_000_000_000_000_000))) + Date.now() + da.timeOffset; + }) + ); + } } prepareChartOptions(hashrate, share) { diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts index 1ab1a1c94..0af9f0976 100644 --- a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts @@ -36,13 +36,14 @@ function parseTag(scriptSig: string): string { for (let i = 0; i < hex.length; i += 2) { bytes.push(parseInt(hex.substr(i, 2), 16)); } - const ascii = new TextDecoder('utf8').decode(Uint8Array.from(bytes)).replace(/\uFFFD/g, '').replace(/\\0/g, ''); + // eslint-disable-next-line no-control-regex + const ascii = new TextDecoder('utf8').decode(Uint8Array.from(bytes)).replace(/\uFFFD/g, '').replace(/\\0/g, '').replace(/[\x00-\x1F\x7F-\x9F]/g, ''); if (ascii.includes('/ViaBTC/')) { return '/ViaBTC/'; } else if (ascii.includes('SpiderPool/')) { return 'SpiderPool/'; } - return ascii.match(/\/.*\//)?.[0] || ascii; + return (ascii.match(/\/.*\//)?.[0] || ascii).trim(); } @Component({ From 58e6a785795bae3196fd99ab7371199ff88dc358 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sun, 19 Jan 2025 17:56:19 +0900 Subject: [PATCH 60/91] [accelerator] add support for card on file acceleration --- .../accelerate-checkout.component.html | 22 ++- .../accelerate-checkout.component.ts | 133 +++++++++++++++++- .../src/app/services/services-api.service.ts | 4 + frontend/src/app/shared/shared.module.ts | 3 +- 4 files changed, 153 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 150da04da..e90a5cc21 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -389,13 +389,13 @@ } - @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { + @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {

OR

} } - @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { + @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {

Pay  with

@if (canPayWithCashapp) { @@ -413,6 +413,13 @@
} + @if (canPayWithCardOnFile) { + @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { } +
+ + {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }} +
+ } } @@ -435,7 +442,7 @@ - } @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') { + } @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') {
@@ -451,7 +458,7 @@
- @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) { + @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay || step === 'cardonfile' && !loadingCardOnFile) {
@@ -476,8 +483,13 @@
} @else if (step === 'googlepay') {
+ } @else if (step === 'cardonfile') { +
+ + {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }} +
} - @if (loadingCashapp || loadingApplePay || loadingGooglePay) { + @if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) {
Loading payment method...
diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 34a98b8dc..e516fe321 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -13,7 +13,7 @@ import { EnterpriseService } from '@app/services/enterprise.service'; import { ApiService } from '@app/services/api.service'; import { isDevMode } from '@angular/core'; -export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay'; +export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay' | 'cardOnFile'; export type AccelerationEstimate = { hasAccess: boolean; @@ -26,7 +26,7 @@ export type AccelerationEstimate = { mempoolBaseFee: number; vsizeFee: number; pools: number[]; - availablePaymentMethods: Record; + availablePaymentMethods: Record; unavailable?: boolean; options: { // recommended bid options fee: number; // recommended userBid in sats @@ -49,7 +49,7 @@ export const MIN_BID_RATIO = 1; export const DEFAULT_BID_RATIO = 2; export const MAX_BID_RATIO = 4; -type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success'; +type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'cardonfile' | 'processing' | 'paid' | 'success'; @Component({ selector: 'app-accelerate-checkout', @@ -65,6 +65,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() cashappEnabled: boolean = true; @Input() applePayEnabled: boolean = false; @Input() googlePayEnabled: boolean = true; + @Input() cardOnFileEnabled: boolean = true; @Input() advancedEnabled: boolean = false; @Input() forceMobile: boolean = false; @Input() showDetails: boolean = false; @@ -117,6 +118,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { loadingCashapp = false; loadingApplePay = false; loadingGooglePay = false; + loadingCardOnFile = false; payments: any; cashAppPay: any; applePay: any; @@ -234,6 +236,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.loadingGooglePay = true; this.setupSquare(); this.scrollToElementWithTimeout('confirm-title', 'center', 100); + } else if (this._step === 'cardonfile' && this.cardOnFileEnabled) { + this.loadingCardOnFile = true; + this.setupSquare(); + this.scrollToElementWithTimeout('confirm-title', 'center', 100); } else if (this._step === 'paid') { this.timePaid = Date.now(); this.timeoutTimer = setTimeout(() => { @@ -454,6 +460,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { await this.requestApplePayPayment(); } else if (this._step === 'googlepay') { await this.requestGooglePayPayment(); + } else if (this._step === 'cardonfile') { + this.loadingCardOnFile = false; } }, error: () => { @@ -710,6 +718,109 @@ export class AccelerateCheckout implements OnInit, OnDestroy { ); } + /** + * Card On File + */ + async requestCardOnFilePayment(): Promise { + if (this.processing) { + return; + } + if (this.conversionsSubscription) { + this.conversionsSubscription.unsubscribe(); + } + + this.processing = true; + this.conversionsSubscription = this.stateService.conversions$.subscribe( + async (conversions) => { + this.conversions = conversions; + + const costUSD = this.cost / 100_000_000 * conversions.USD; + if (this.isCheckoutLocked > 0) { + return; + } + const cardOnFile = this.estimate?.availablePaymentMethods?.cardOnFile; + if (!cardOnFile?.card) { + this.accelerateError = 'card_on_file_not_found'; + return; + } + this.loadingCardOnFile = false; + + try { + this.isCheckoutLocked += 2; + this.isTokenizing += 2; + + const nameParts = cardOnFile.card.name.split(' '); + const assumedGivenName = nameParts[0]; + const assumedFamilyName = nameParts.length > 1 ? nameParts[1] : undefined; + const verificationDetails = { + card: { + billing: { + givenName: assumedGivenName, + familyName: assumedFamilyName, + addressLines: [cardOnFile.card.billing.addressLine1], + city: cardOnFile.card.billing.locality, + state: cardOnFile.card.billing.administrativeDistrictLevel1, + countyCode: cardOnFile.card.billing.country, + } + } + }; + const verificationToken = await this.$verifyBuyer(this.payments, cardOnFile.card.card_id, verificationDetails, costUSD.toFixed(2)); + if (!verificationToken || !verificationToken.token) { + console.error(`SCA verification failed`); + this.accelerateError = 'SCA Verification Failed. Payment Declined.'; + this.processing = false; + return; + } + + this.servicesApiService.accelerateWithCardOnFile$( + this.tx.txid, + cardOnFile.card.card_id, + verificationToken.token, + `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, + costUSD, + verificationToken.userChallenged + ).subscribe({ + next: () => { + this.processing = false; + this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); + this.audioService.playSound('ascend-chime-cartoon'); + setTimeout(() => { + this.isCheckoutLocked--; + this.isTokenizing--; + this.moveToStep('paid', true); + }, 1000); + }, + error: (response) => { + this.processing = false; + this.accelerateError = response.error; + this.isCheckoutLocked--; + this.isTokenizing--; + if (!(response.status === 403 && response.error === 'not_available')) { + setTimeout(() => { + // Reset everything by reloading the page :D, can be improved + const urlParams = new URLSearchParams(window.location.search); + window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); + }, 3000); + } + } + }); + + } catch (e) { + console.log(e); + this.isCheckoutLocked--; + this.isTokenizing--; + this.processing = false; + this.accelerateError = e.message; + + } finally { + // always unlock the checkout once we're finished + this.isCheckoutLocked--; + this.isTokenizing--; + } + } + ); + } + /** * CASHAPP */ @@ -955,6 +1066,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy { return false; } + get canPayWithCardOnFile(): boolean { + if (!this.cardOnFileEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) { + return false; + } + + const paymentMethod = this.estimate?.availablePaymentMethods?.cardOnFile; + if (paymentMethod) { + const costUSD = (this.cost / 100_000_000 * this.conversions.USD); + if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) { + return true; + } + } + + return false; + } + get canPayWithBalance(): boolean { if (!this.hasAccessToBalanceMode) { return false; diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 5e882cd02..59dc92358 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -146,6 +146,10 @@ export class ServicesApiServices { return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged }); } + accelerateWithCardOnFile$(txInput: string, token: string, verificationToken: string, referenceId: string, userApprovedUSD: number, userChallenged: boolean) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cardOnFile`, { txInput: txInput, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged }); + } + getAccelerations$(): Observable { return this.httpClient.get(`${this.stateService.env.SERVICES_API}/accelerator/accelerations`); } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 1d4f5fd99..e78e74a9c 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -7,7 +7,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, - faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot } from '@fortawesome/free-solid-svg-icons'; + faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faCreditCard } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MenuComponent } from '@components/menu/menu.component'; import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component'; @@ -459,5 +459,6 @@ export class SharedModule { library.addIcons(faCalendarCheck); library.addIcons(faMoneyBillTrendUp); library.addIcons(faRobot); + library.addIcons(faCreditCard); } } From caa2d83247fcd245cbe0aabb1a106ca17da5d2d6 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn <100320+knorrium@users.noreply.github.com> Date: Sun, 19 Jan 2025 02:34:05 -0800 Subject: [PATCH 61/91] Update staging hosts --- frontend/proxy.conf.staging.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/proxy.conf.staging.js b/frontend/proxy.conf.staging.js index e24662038..e6f33698b 100644 --- a/frontend/proxy.conf.staging.js +++ b/frontend/proxy.conf.staging.js @@ -3,8 +3,8 @@ const fs = require('fs'); let PROXY_CONFIG = require('./proxy.conf'); PROXY_CONFIG.forEach(entry => { - entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space"); - entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space"); + entry.target = entry.target.replace("mempool.space", "node201.fmt.mempool.space"); + entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space"); }); module.exports = PROXY_CONFIG; From 671b5ea2f2eb3ce9515a233702c5b784093f77c1 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 19 Jan 2025 12:24:26 -0800 Subject: [PATCH 62/91] Reroute testnet4 tests to a different server --- .github/workflows/ci.yml | 1 + frontend/proxy.conf.staging.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a29e9184..767cf2694 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -327,6 +327,7 @@ jobs: browser: "chrome" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" env: + CYPRESS_REROUTE_TESTNET: ${{ matrix.spec == 'cypress/e2e/testnet4/*.spec.ts' }} COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/frontend/proxy.conf.staging.js b/frontend/proxy.conf.staging.js index e6f33698b..eda7d2b9d 100644 --- a/frontend/proxy.conf.staging.js +++ b/frontend/proxy.conf.staging.js @@ -3,7 +3,8 @@ const fs = require('fs'); let PROXY_CONFIG = require('./proxy.conf'); PROXY_CONFIG.forEach(entry => { - entry.target = entry.target.replace("mempool.space", "node201.fmt.mempool.space"); + const hostname = process.env.CYPRESS_REROUTE_TESTNET ? 'node201.fmt.mempool.space' : 'mempool-staging.fra.mempool.space'; + entry.target = entry.target.replace("mempool.space", hostname); entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space"); }); From 34099e386112b781d7b70ca886f35cab8d23d969 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 19 Jan 2025 12:32:37 -0800 Subject: [PATCH 63/91] Stop switching to testnet4 until we can check for the proxied server --- frontend/cypress/e2e/mainnet/mainnet.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index a1082b769..7e17c09cd 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -344,7 +344,9 @@ describe('Mainnet', () => { cy.visit('/'); cy.waitForSkeletonGone(); - cy.changeNetwork('testnet4'); + //TODO(knorrium): add a check for the proxied server + // cy.changeNetwork('testnet4'); + cy.changeNetwork('signet'); cy.changeNetwork('mainnet'); }); From 7e766cc28db6e67386aab486a5d2e4e35eb3fda1 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 19 Jan 2025 12:36:20 -0800 Subject: [PATCH 64/91] Change spec test --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 767cf2694..af05a2448 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -327,7 +327,7 @@ jobs: browser: "chrome" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" env: - CYPRESS_REROUTE_TESTNET: ${{ matrix.spec == 'cypress/e2e/testnet4/*.spec.ts' }} + CYPRESS_REROUTE_TESTNET: ${{ contains(matrix.spec, 'testnet') }} COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 517a30d2b0e76c6a88e25b0e0d4c4dfc61597038 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 19 Jan 2025 13:25:29 -0800 Subject: [PATCH 65/91] Split module and spec matrix --- .github/workflows/ci.yml | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af05a2448..594df2a9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -252,16 +252,23 @@ jobs: fail-fast: false matrix: module: ["mempool", "liquid"] + spec: + - "cypress/e2e/mainnet/*.spec.ts" + - "cypress/e2e/signet/*.spec.ts" + - "cypress/e2e/testnet4/*.spec.ts" + - "cypress/e2e/liquid/liquid.spec.ts" + - "cypress/e2e/liquidtestnet/liquidtestnet.spec.ts" include: - module: "mempool" - spec: | - cypress/e2e/mainnet/*.spec.ts - cypress/e2e/signet/*.spec.ts - cypress/e2e/testnet4/*.spec.ts + spec: "cypress/e2e/mainnet/*.spec.ts" + - module: "mempool" + spec: "cypress/e2e/signet/*.spec.ts" + - module: "mempool" + spec: "cypress/e2e/testnet4/*.spec.ts" - module: "liquid" - spec: | - cypress/e2e/liquid/liquid.spec.ts - cypress/e2e/liquidtestnet/liquidtestnet.spec.ts + spec: "cypress/e2e/liquid/liquid.spec.ts" + - module: "liquid" + spec: "cypress/e2e/liquidtestnet/liquidtestnet.spec.ts" name: E2E tests for ${{ matrix.module }} steps: From e9e8b0c758c476e705a5696268808838a7c3ff58 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 19 Jan 2025 13:38:06 -0800 Subject: [PATCH 66/91] Use testnet as a matrix config instead --- .github/workflows/ci.yml | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 594df2a9f..7cff27841 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -251,24 +251,19 @@ jobs: strategy: fail-fast: false matrix: - module: ["mempool", "liquid"] - spec: - - "cypress/e2e/mainnet/*.spec.ts" - - "cypress/e2e/signet/*.spec.ts" - - "cypress/e2e/testnet4/*.spec.ts" - - "cypress/e2e/liquid/liquid.spec.ts" - - "cypress/e2e/liquidtestnet/liquidtestnet.spec.ts" + module: ["mempool", "liquid", "testnet4"] include: - module: "mempool" - spec: "cypress/e2e/mainnet/*.spec.ts" - - module: "mempool" - spec: "cypress/e2e/signet/*.spec.ts" - - module: "mempool" - spec: "cypress/e2e/testnet4/*.spec.ts" + spec: | + cypress/e2e/mainnet/*.spec.ts + cypress/e2e/signet/*.spec.ts + - module: "testnet4" + spec: | + cypress/e2e/testnet4/*.spec.ts - module: "liquid" - spec: "cypress/e2e/liquid/liquid.spec.ts" - - module: "liquid" - spec: "cypress/e2e/liquidtestnet/liquidtestnet.spec.ts" + spec: | + cypress/e2e/liquid/liquid.spec.ts + cypress/e2e/liquidtestnet/liquidtestnet.spec.ts name: E2E tests for ${{ matrix.module }} steps: @@ -334,7 +329,7 @@ jobs: browser: "chrome" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" env: - CYPRESS_REROUTE_TESTNET: ${{ contains(matrix.spec, 'testnet') }} + CYPRESS_REROUTE_TESTNET: ${{ contains(matrix.spec, 'testnet4') }} COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 7f6399093eafa5aeb74aa81ab2b136502c76bd6e Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 19 Jan 2025 13:56:16 -0800 Subject: [PATCH 67/91] Fix YAML --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cff27841..784a02afc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -258,8 +258,8 @@ jobs: cypress/e2e/mainnet/*.spec.ts cypress/e2e/signet/*.spec.ts - module: "testnet4" - spec: | - cypress/e2e/testnet4/*.spec.ts + spec: | + cypress/e2e/testnet4/*.spec.ts - module: "liquid" spec: | cypress/e2e/liquid/liquid.spec.ts From f59e95fcc8419426550be6bad69c6507e87179c4 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 19 Jan 2025 14:11:41 -0800 Subject: [PATCH 68/91] Run the default mempool config if we are running testnet4 tests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 784a02afc..146372f40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -318,7 +318,7 @@ jobs: with: tag: ${{ github.event_name }} working-directory: ${{ matrix.module }}/frontend - build: npm run config:defaults:${{ matrix.module }} + build: ${{ matrix.module == 'testnet4' && 'npm run config:defaults:mempool' || 'npm run config:defaults:${{ matrix.module }}' }} start: npm run start:local-staging wait-on: "http://localhost:4200" wait-on-timeout: 120 From 1098d2fe3c44b83624d3e17cda2ea73643d3dcf8 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 19 Jan 2025 14:21:23 -0800 Subject: [PATCH 69/91] Change build step to shell script --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 146372f40..ac5326323 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -318,7 +318,12 @@ jobs: with: tag: ${{ github.event_name }} working-directory: ${{ matrix.module }}/frontend - build: ${{ matrix.module == 'testnet4' && 'npm run config:defaults:mempool' || 'npm run config:defaults:${{ matrix.module }}' }} + build: | + if [[ "${{ matrix.module }}" == "testnet4" ]]; then + npm run config:defaults:mempool + else + npm run config:defaults:${{ matrix.module }} + fi start: npm run start:local-staging wait-on: "http://localhost:4200" wait-on-timeout: 120 From 3d1aacbd66e69f830a6b0034577fa284696dd2ff Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 19 Jan 2025 19:21:02 -0800 Subject: [PATCH 70/91] Copypasta matrix for tests --- .github/workflows/ci.yml | 80 +++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac5326323..6d2fc387f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -252,18 +252,6 @@ jobs: fail-fast: false matrix: module: ["mempool", "liquid", "testnet4"] - include: - - module: "mempool" - spec: | - cypress/e2e/mainnet/*.spec.ts - cypress/e2e/signet/*.spec.ts - - module: "testnet4" - spec: | - cypress/e2e/testnet4/*.spec.ts - - module: "liquid" - spec: | - cypress/e2e/liquid/liquid.spec.ts - cypress/e2e/liquidtestnet/liquidtestnet.spec.ts name: E2E tests for ${{ matrix.module }} steps: @@ -312,29 +300,77 @@ jobs: - name: Unzip assets before building (src/resources) run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video - + + # mempool - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'mempool' }} uses: cypress-io/github-action@v5 with: tag: ${{ github.event_name }} working-directory: ${{ matrix.module }}/frontend - build: | - if [[ "${{ matrix.module }}" == "testnet4" ]]; then - npm run config:defaults:mempool - else - npm run config:defaults:${{ matrix.module }} - fi + build: npm run config:defaults:${{ matrix.module }} start: npm run start:local-staging wait-on: "http://localhost:4200" wait-on-timeout: 120 record: true parallel: true - spec: ${{ matrix.spec }} + spec: | + cypress/e2e/mainnet/*.spec.ts + cypress/e2e/signet/*.spec.ts group: Tests on Chrome (${{ matrix.module }}) browser: "chrome" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" env: - CYPRESS_REROUTE_TESTNET: ${{ contains(matrix.spec, 'testnet4') }} + COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + + # liquid + - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'liquid' }} + uses: cypress-io/github-action@v5 + with: + tag: ${{ github.event_name }} + working-directory: ${{ matrix.module }}/frontend + build: npm run config:defaults:${{ matrix.module }} + start: npm run start:local-staging + wait-on: "http://localhost:4200" + wait-on-timeout: 120 + record: true + parallel: true + spec: | + cypress/e2e/liquid/liquid.spec.ts + cypress/e2e/liquidtestnet/liquidtestnet.spec.ts + group: Tests on Chrome (${{ matrix.module }}) + browser: "chrome" + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" + env: + COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + + # testnet + - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'testnet4' }} + uses: cypress-io/github-action@v5 + with: + tag: ${{ github.event_name }} + working-directory: ${{ matrix.module }}/frontend + build: npm run config:defaults:mempool + start: npm run start:local-staging + wait-on: "http://localhost:4200" + wait-on-timeout: 120 + record: true + parallel: true + spec: | + cypress/e2e/testnet4/*.spec.ts + group: Tests on Chrome (${{ matrix.module }}) + browser: "chrome" + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" + env: + CYPRESS_REROUTE_TESTNET: true COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -367,4 +403,4 @@ jobs: - name: Validate JSON syntax run: | cat mempool-config.json | jq - working-directory: docker/docker/backend + working-directory: docker/docker/backend \ No newline at end of file From 227d99e9909d1401867f3cb6258e2c4c4b05ead9 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 19 Jan 2025 19:35:08 -0800 Subject: [PATCH 71/91] Fix reroute logic --- frontend/proxy.conf.staging.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/proxy.conf.staging.js b/frontend/proxy.conf.staging.js index eda7d2b9d..260b222c0 100644 --- a/frontend/proxy.conf.staging.js +++ b/frontend/proxy.conf.staging.js @@ -3,7 +3,8 @@ const fs = require('fs'); let PROXY_CONFIG = require('./proxy.conf'); PROXY_CONFIG.forEach(entry => { - const hostname = process.env.CYPRESS_REROUTE_TESTNET ? 'node201.fmt.mempool.space' : 'mempool-staging.fra.mempool.space'; + const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space'; + console.log(`e2e tests running against ${hostname}`); entry.target = entry.target.replace("mempool.space", hostname); entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space"); }); From 003956fd16caed93a243c37095cae48c667b544a Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 19 Jan 2025 19:48:12 -0800 Subject: [PATCH 72/91] Update staging hosts Add rerouting logic for testnet4 tests Split CI e2e workflow matrix into mempool, liquid and testnet4 --- .github/workflows/ci.yml | 72 ++++++++++++++++---- frontend/cypress/e2e/mainnet/mainnet.spec.ts | 4 +- frontend/proxy.conf.staging.js | 6 +- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a29e9184..6d2fc387f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -251,17 +251,7 @@ jobs: strategy: fail-fast: false matrix: - module: ["mempool", "liquid"] - include: - - module: "mempool" - spec: | - cypress/e2e/mainnet/*.spec.ts - cypress/e2e/signet/*.spec.ts - cypress/e2e/testnet4/*.spec.ts - - module: "liquid" - spec: | - cypress/e2e/liquid/liquid.spec.ts - cypress/e2e/liquidtestnet/liquidtestnet.spec.ts + module: ["mempool", "liquid", "testnet4"] name: E2E tests for ${{ matrix.module }} steps: @@ -310,8 +300,10 @@ jobs: - name: Unzip assets before building (src/resources) run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video - + + # mempool - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'mempool' }} uses: cypress-io/github-action@v5 with: tag: ${{ github.event_name }} @@ -322,7 +314,9 @@ jobs: wait-on-timeout: 120 record: true parallel: true - spec: ${{ matrix.spec }} + spec: | + cypress/e2e/mainnet/*.spec.ts + cypress/e2e/signet/*.spec.ts group: Tests on Chrome (${{ matrix.module }}) browser: "chrome" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" @@ -332,6 +326,56 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + # liquid + - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'liquid' }} + uses: cypress-io/github-action@v5 + with: + tag: ${{ github.event_name }} + working-directory: ${{ matrix.module }}/frontend + build: npm run config:defaults:${{ matrix.module }} + start: npm run start:local-staging + wait-on: "http://localhost:4200" + wait-on-timeout: 120 + record: true + parallel: true + spec: | + cypress/e2e/liquid/liquid.spec.ts + cypress/e2e/liquidtestnet/liquidtestnet.spec.ts + group: Tests on Chrome (${{ matrix.module }}) + browser: "chrome" + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" + env: + COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + + # testnet + - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'testnet4' }} + uses: cypress-io/github-action@v5 + with: + tag: ${{ github.event_name }} + working-directory: ${{ matrix.module }}/frontend + build: npm run config:defaults:mempool + start: npm run start:local-staging + wait-on: "http://localhost:4200" + wait-on-timeout: 120 + record: true + parallel: true + spec: | + cypress/e2e/testnet4/*.spec.ts + group: Tests on Chrome (${{ matrix.module }}) + browser: "chrome" + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" + env: + CYPRESS_REROUTE_TESTNET: true + COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + validate_docker_json: if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" runs-on: "ubuntu-latest" @@ -359,4 +403,4 @@ jobs: - name: Validate JSON syntax run: | cat mempool-config.json | jq - working-directory: docker/docker/backend + working-directory: docker/docker/backend \ No newline at end of file diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index a1082b769..7e17c09cd 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -344,7 +344,9 @@ describe('Mainnet', () => { cy.visit('/'); cy.waitForSkeletonGone(); - cy.changeNetwork('testnet4'); + //TODO(knorrium): add a check for the proxied server + // cy.changeNetwork('testnet4'); + cy.changeNetwork('signet'); cy.changeNetwork('mainnet'); }); diff --git a/frontend/proxy.conf.staging.js b/frontend/proxy.conf.staging.js index e24662038..260b222c0 100644 --- a/frontend/proxy.conf.staging.js +++ b/frontend/proxy.conf.staging.js @@ -3,8 +3,10 @@ const fs = require('fs'); let PROXY_CONFIG = require('./proxy.conf'); PROXY_CONFIG.forEach(entry => { - entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space"); - entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space"); + const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space'; + console.log(`e2e tests running against ${hostname}`); + entry.target = entry.target.replace("mempool.space", hostname); + entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space"); }); module.exports = PROXY_CONFIG; From 390bbf1097ca2cc65bc2b5d6cb45c54fa7a5ef44 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Mon, 20 Jan 2025 15:20:29 +0900 Subject: [PATCH 73/91] add new fa icon --- frontend/src/app/shared/shared.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 1d4f5fd99..283f9eb54 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -7,7 +7,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, - faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot } from '@fortawesome/free-solid-svg-icons'; + faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MenuComponent } from '@components/menu/menu.component'; import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component'; @@ -459,5 +459,6 @@ export class SharedModule { library.addIcons(faCalendarCheck); library.addIcons(faMoneyBillTrendUp); library.addIcons(faRobot); + library.addIcons(faShareNodes); } } From 4e735cc8b03d87f850286461474d8f110580e444 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 20 Jan 2025 07:30:27 +0000 Subject: [PATCH 74/91] fix stratum tree rendering with different branch lengths --- .../stratum-list/stratum-list.component.ts | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts index 0af9f0976..6f252babe 100644 --- a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts @@ -8,8 +8,10 @@ import { SinglePoolStats } from '../../../interfaces/node-api.interface'; type MerkleCellType = ' ' | '┬' | '├' | '└' | '│' | '─' | 'leaf'; + interface TaggedStratumJob extends StratumJob { tag: string; + merkleBranchIds: string[]; } interface MerkleCell { @@ -46,6 +48,18 @@ function parseTag(scriptSig: string): string { return (ascii.match(/\/.*\//)?.[0] || ascii).trim(); } +function getMerkleBranchIds(merkleBranches: string[], numBranches: number): string[] { + let lastHash = ''; + const ids: string[] = []; + for (let i = 0; i < numBranches; i++) { + if (merkleBranches[i]) { + lastHash = merkleBranches[i]; + } + ids.push(`${i}-${lastHash}`); + } + return ids; +} + @Component({ selector: 'app-stratum-list', templateUrl: './stratum-list.component.html', @@ -81,16 +95,15 @@ export class StratumList implements OnInit, OnDestroy { } processJobs(rawJobs: Record): PoolRow[] { + const numBranches = Math.max(...Object.values(rawJobs).map(job => job.merkleBranches.length)); const jobs: Record = {}; for (const [id, job] of Object.entries(rawJobs)) { - jobs[id] = { ...job, tag: parseTag(job.scriptsig) }; + jobs[id] = { ...job, tag: parseTag(job.scriptsig), merkleBranchIds: getMerkleBranchIds(job.merkleBranches, numBranches) }; } if (Object.keys(jobs).length === 0) { return []; } - const numBranches = Math.max(...Object.values(jobs).map(job => job.merkleBranches.length)); - let trees: MerkleTree[] = Object.keys(jobs).map(job => ({ job, size: 1, @@ -100,12 +113,13 @@ export class StratumList implements OnInit, OnDestroy { for (let col = numBranches - 1; col >= 0; col--) { const groups: Record = {}; for (const tree of trees) { - const hash = jobs[tree.job].merkleBranches[col]; - if (!groups[hash]) { - groups[hash] = []; + const branchId = jobs[tree.job].merkleBranchIds[col]; + if (!groups[branchId]) { + groups[branchId] = []; } - groups[hash].push(tree); + groups[branchId].push(tree); } + trees = Object.values(groups).map(group => ({ hash: jobs[group[0].job].merkleBranches[col], job: group[0].job, From 36b691e25b4bfebac53e1b389228f6fb56f696b2 Mon Sep 17 00:00:00 2001 From: wiz Date: Mon, 20 Jan 2025 17:20:23 +0900 Subject: [PATCH 75/91] ops: Enable stratum in prod config via localhost nginx proxy --- production/mempool-config.mainnet.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json index 39d82d8d1..5c164340e 100644 --- a/production/mempool-config.mainnet.json +++ b/production/mempool-config.mainnet.json @@ -154,5 +154,9 @@ "WALLETS": { "ENABLED": true, "WALLETS": ["BITB", "3350"] + }, + "STRATUM": { + "ENABLED": true, + "API": "http://127.0.0.1:81" } } From e53e810a5563c3a70be8ec33c880a8e68220a14d Mon Sep 17 00:00:00 2001 From: wiz Date: Mon, 20 Jan 2025 17:34:19 +0900 Subject: [PATCH 76/91] ops: Fix stratum server URL path in prod config --- production/mempool-config.mainnet.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json index 5c164340e..758887407 100644 --- a/production/mempool-config.mainnet.json +++ b/production/mempool-config.mainnet.json @@ -157,6 +157,6 @@ }, "STRATUM": { "ENABLED": true, - "API": "http://127.0.0.1:81" + "API": "http://127.0.0.1:81/api/v1/stratum/ws" } } From 5aeaa6825989139f18703ed027673f967591e8f5 Mon Sep 17 00:00:00 2001 From: wiz Date: Mon, 20 Jan 2025 17:56:38 +0900 Subject: [PATCH 77/91] ops: Enable stratum in FOSS prod frontend config --- production/mempool-frontend-config.mainnet.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/production/mempool-frontend-config.mainnet.json b/production/mempool-frontend-config.mainnet.json index 79acaecc5..e0cdbf030 100644 --- a/production/mempool-frontend-config.mainnet.json +++ b/production/mempool-frontend-config.mainnet.json @@ -4,8 +4,7 @@ "TESTNET4_ENABLED": true, "LIQUID_ENABLED": false, "LIQUID_TESTNET_ENABLED": false, - "BISQ_ENABLED": true, - "BISQ_SEPARATE_BACKEND": true, + "STRATUM_ENABLED": true, "SIGNET_ENABLED": true, "MEMPOOL_WEBSITE_URL": "https://mempool.space", "LIQUID_WEBSITE_URL": "https://liquid.network", From b454fa09d2d8288e2d96ea4f88aae683b71197ac Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 21 Jan 2025 13:49:12 +0700 Subject: [PATCH 78/91] Remove babel backend dep --- backend/jest.config.ts | 2 +- backend/package-lock.json | 3 --- backend/package.json | 3 --- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/backend/jest.config.ts b/backend/jest.config.ts index 14f932f98..43246c518 100644 --- a/backend/jest.config.ts +++ b/backend/jest.config.ts @@ -7,7 +7,7 @@ const config: Config.InitialOptions = { automock: false, collectCoverage: true, collectCoverageFrom: ["./src/**/**.ts"], - coverageProvider: "babel", + coverageProvider: "v8", coverageThreshold: { global: { lines: 1 diff --git a/backend/package-lock.json b/backend/package-lock.json index e0d28bfc9..08ebf0619 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,7 +10,6 @@ "hasInstallScript": true, "license": "GNU Affero General Public License v3.0", "dependencies": { - "@babel/core": "^7.25.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", "axios": "1.7.2", @@ -26,8 +25,6 @@ "ws": "~8.18.0" }, "devDependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/core": "^7.25.2", "@types/compression": "^1.7.2", "@types/crypto-js": "^4.1.1", "@types/express": "^4.17.17", diff --git a/backend/package.json b/backend/package.json index 9ac3f9199..b1145cb13 100644 --- a/backend/package.json +++ b/backend/package.json @@ -39,7 +39,6 @@ "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" }, "dependencies": { - "@babel/core": "^7.25.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", "axios": "1.7.2", @@ -55,8 +54,6 @@ "ws": "~8.18.0" }, "devDependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/core": "^7.25.2", "@types/compression": "^1.7.2", "@types/crypto-js": "^4.1.1", "@types/express": "^4.17.17", From b42431f14a7bd6730ce89bd03933e50d5dc08852 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Tue, 21 Jan 2025 17:39:50 +0900 Subject: [PATCH 79/91] [auth] add login/signup with github support --- .../components/faucet/faucet.component.html | 10 +++----- .../github-login.component.html | 6 +++++ .../github-login.component.ts | 25 +++++++++++++++++++ frontend/src/app/shared/shared.module.ts | 3 +++ 4 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 frontend/src/app/components/github-login.component/github-login.component.html create mode 100644 frontend/src/app/components/github-login.component/github-login.component.ts diff --git a/frontend/src/app/components/faucet/faucet.component.html b/frontend/src/app/components/faucet/faucet.component.html index 3165ae9a7..19d76d9dd 100644 --- a/frontend/src/app/components/faucet/faucet.component.html +++ b/frontend/src/app/components/faucet/faucet.component.html @@ -21,10 +21,8 @@
To use the faucet, please  - login -  or
- +
} @else if (user && user.status === 'pending' && !user.email && user.snsId) { @@ -36,18 +34,18 @@
} @else if (error === 'not_available') { - +
To use the faucet, please
- +
} @else if (error === 'account_limited') {
- Your Twitter account does not allow you to access the faucet + Your account does not allow you to access the faucet
} diff --git a/frontend/src/app/components/github-login.component/github-login.component.html b/frontend/src/app/components/github-login.component/github-login.component.html new file mode 100644 index 000000000..de9c743b5 --- /dev/null +++ b/frontend/src/app/components/github-login.component/github-login.component.html @@ -0,0 +1,6 @@ + + + + + {{ buttonString }} + \ No newline at end of file diff --git a/frontend/src/app/components/github-login.component/github-login.component.ts b/frontend/src/app/components/github-login.component/github-login.component.ts new file mode 100644 index 000000000..52f2584b9 --- /dev/null +++ b/frontend/src/app/components/github-login.component/github-login.component.ts @@ -0,0 +1,25 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +@Component({ + selector: 'app-github-login', + templateUrl: './github-login.component.html', +}) +export class GithubLogin { + @Input() width: string | null = null; + @Input() customClass: string | null = null; + @Input() buttonString: string= 'unset'; + @Input() redirectTo: string | null = null; + @Output() clicked = new EventEmitter(); + @Input() disabled: boolean = false; + + constructor() {} + + githubLogin() { + this.clicked.emit(true); + if (this.redirectTo) { + location.replace(`/api/v1/services/auth/login/github?redirectTo=${encodeURIComponent(this.redirectTo)}`); + } else { + location.replace(`/api/v1/services/auth/login/github?redirectTo=${location.href}`); + } + return false; + } +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 283f9eb54..d63b54632 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -125,6 +125,7 @@ import { TwitterLogin } from '@components/twitter-login/twitter-login.component' import { BitcoinInvoiceComponent } from '@components/bitcoin-invoice/bitcoin-invoice.component'; import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/weight-directives/weight-directives'; +import { GithubLogin } from '../components/github-login.component/github-login.component'; @NgModule({ declarations: [ @@ -242,6 +243,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/ TwitterWidgetComponent, FaucetComponent, TwitterLogin, + GithubLogin, BitcoinInvoiceComponent, ], imports: [ @@ -376,6 +378,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/ HttpErrorComponent, TwitterWidgetComponent, TwitterLogin, + GithubLogin, BitcoinInvoiceComponent, BitcoinsatoshisPipe, From 665a12a040b5fcb743b5ffa5e1d54a1dce56b77c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:57:18 +0000 Subject: [PATCH 80/91] Bump mysql2 from 3.11.0 to 3.12.0 in /backend Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.11.0 to 3.12.0. - [Release notes](https://github.com/sidorares/node-mysql2/releases) - [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md) - [Commits](https://github.com/sidorares/node-mysql2/compare/v3.11.0...v3.12.0) --- updated-dependencies: - dependency-name: mysql2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- backend/package-lock.json | 52 ++++++++++++++++++++++----------------- backend/package.json | 2 +- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 08ebf0619..3f66fa25b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,7 +17,7 @@ "crypto-js": "~4.2.0", "express": "~4.21.1", "maxmind": "~4.3.11", - "mysql2": "~3.11.0", + "mysql2": "~3.12.0", "redis": "^4.7.0", "rust-gbt": "file:./rust-gbt", "socks-proxy-agent": "~7.0.0", @@ -5997,6 +5997,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", + "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -6158,16 +6173,17 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mysql2": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz", - "integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", + "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==", + "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", "long": "^5.2.1", - "lru-cache": "^8.0.0", + "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" @@ -6187,14 +6203,6 @@ "node": ">=0.10.0" } }, - "node_modules/mysql2/node_modules/lru-cache": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", - "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", - "engines": { - "node": ">=16.14" - } - }, "node_modules/named-placeholders": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", @@ -12210,6 +12218,11 @@ "yallist": "^3.0.2" } }, + "lru.min": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", + "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -12324,16 +12337,16 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mysql2": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz", - "integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", + "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==", "requires": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", "long": "^5.2.1", - "lru-cache": "^8.0.0", + "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" @@ -12346,11 +12359,6 @@ "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } - }, - "lru-cache": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", - "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==" } } }, diff --git a/backend/package.json b/backend/package.json index b1145cb13..ee5944f93 100644 --- a/backend/package.json +++ b/backend/package.json @@ -46,7 +46,7 @@ "crypto-js": "~4.2.0", "express": "~4.21.1", "maxmind": "~4.3.11", - "mysql2": "~3.11.0", + "mysql2": "~3.12.0", "rust-gbt": "file:./rust-gbt", "redis": "^4.7.0", "socks-proxy-agent": "~7.0.0", From ff4aca83702e595ac12de1c7c717f34f44faeb56 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 22 Jan 2025 11:26:34 +0900 Subject: [PATCH 81/91] [blocks] update sha in memory/db before re-indexing blocks --- backend/src/tasks/pools-updater.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts index 502e3613f..38e816d74 100644 --- a/backend/src/tasks/pools-updater.ts +++ b/backend/src/tasks/pools-updater.ts @@ -88,8 +88,8 @@ class PoolsUpdater { try { await DB.query('START TRANSACTION;'); - await poolsParser.migratePoolsJson(); await this.updateDBSha(githubSha); + await poolsParser.migratePoolsJson(); await DB.query('COMMIT;'); } catch (e) { logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag); From 23713a11c25504ca216a39ea3000cf961c165dac Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 22 Jan 2025 05:06:02 +0000 Subject: [PATCH 82/91] link to merkle branch first tx --- frontend/src/app/components/pool/pool.component.html | 6 +++++- frontend/src/app/components/pool/pool.component.scss | 1 + frontend/src/app/components/pool/pool.component.ts | 4 ++++ .../stratum/stratum-list/stratum-list.component.html | 8 +++++++- .../stratum/stratum-list/stratum-list.component.scss | 10 ++++++++++ .../stratum/stratum-list/stratum-list.component.ts | 4 ++++ 6 files changed, 31 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index faa0003c4..f98794b68 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -267,7 +267,11 @@
@for (branch of job.merkleBranches; track $index) { - + @if ($index === 0 && branch) { + + } @else { + + } } @for (_ of [].constructor(Math.max(0, 12 - job.merkleBranches.length)); track $index) { diff --git a/frontend/src/app/components/pool/pool.component.scss b/frontend/src/app/components/pool/pool.component.scss index 31d12474f..fa94227bd 100644 --- a/frontend/src/app/components/pool/pool.component.scss +++ b/frontend/src/app/components/pool/pool.component.scss @@ -220,6 +220,7 @@ div.scrollable { .merkle { width: 100px; + text-align: center; } .empty-branch { diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 23b795613..4b4b643a2 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -361,6 +361,10 @@ export class PoolComponent implements OnInit { return block.height; } + reverseHash(hash: string) { + return hash.match(/../g).reverse().join(''); + } + ngOnDestroy(): void { this.slugSubscription.unsubscribe(); } diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html index 08d7fb0ef..41707e37f 100644 --- a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html @@ -30,7 +30,13 @@ @for (cell of row.merkleCells; track $index) { }
-
+ @if ($index === 0 && cell.hash) { + +
+
+ } @else { +
+ }
diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss index 6679f2257..15ee074c2 100644 --- a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss @@ -92,6 +92,16 @@ td { } } } + + .cell-link { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + color: inherit; + text-decoration: none; + } } .badge { diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts index 6f252babe..b28f4ff11 100644 --- a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts @@ -196,6 +196,10 @@ export class StratumList implements OnInit, OnDestroy { }[type]; } + reverseHash(hash: string) { + return hash.match(/../g).reverse().join(''); + } + ngOnDestroy(): void { this.websocketService.stopTrackStratum(); } From cac62765a18d25dddd1be3e090b4c57fdf2115ad Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 22 Jan 2025 09:10:22 +0000 Subject: [PATCH 83/91] fix stratum tree branch level --- .../stratum/stratum-list/stratum-list.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts index b28f4ff11..481447b07 100644 --- a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.ts @@ -48,14 +48,16 @@ function parseTag(scriptSig: string): string { return (ascii.match(/\/.*\//)?.[0] || ascii).trim(); } -function getMerkleBranchIds(merkleBranches: string[], numBranches: number): string[] { +function getMerkleBranchIds(merkleBranches: string[], numBranches: number, poolId: number): string[] { let lastHash = ''; const ids: string[] = []; for (let i = 0; i < numBranches; i++) { if (merkleBranches[i]) { lastHash = merkleBranches[i]; + ids.push(`${i}-${lastHash}`); + } else { + ids.push(`${i}-${lastHash}-${poolId}`); } - ids.push(`${i}-${lastHash}`); } return ids; } @@ -98,7 +100,7 @@ export class StratumList implements OnInit, OnDestroy { const numBranches = Math.max(...Object.values(rawJobs).map(job => job.merkleBranches.length)); const jobs: Record = {}; for (const [id, job] of Object.entries(rawJobs)) { - jobs[id] = { ...job, tag: parseTag(job.scriptsig), merkleBranchIds: getMerkleBranchIds(job.merkleBranches, numBranches) }; + jobs[id] = { ...job, tag: parseTag(job.scriptsig), merkleBranchIds: getMerkleBranchIds(job.merkleBranches, numBranches, job.pool) }; } if (Object.keys(jobs).length === 0) { return []; From 2e44ea3f012bf599f468f06849a8d5c2513e4152 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 22 Jan 2025 09:13:44 +0000 Subject: [PATCH 84/91] reorder stratum job table --- .../stratum-list/stratum-list.component.html | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html index 41707e37f..24801cf2c 100644 --- a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.html @@ -7,27 +7,18 @@ - - - + + + @for (row of rows; track row.job.pool) { - - - @for (cell of row.merkleCells; track $index) { + + + } From 363fa3d8779e07cc9d048234b31cf9d51f60b639 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 22 Jan 2025 09:16:25 +0000 Subject: [PATCH 85/91] improve stratum table layout on mobile --- .../stratum-list/stratum-list.component.scss | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss index 15ee074c2..3d274ef2a 100644 --- a/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss +++ b/frontend/src/app/components/stratum/stratum-list/stratum-list.component.scss @@ -104,6 +104,26 @@ td { } } +@media (max-width: 800px) { + .stratum-table { + td { + &.tag { + display: none; + } + } + } +} + +@media (max-width: 650px) { + .stratum-table { + td { + &.reward { + display: none; + } + } + } +} + .badge { position: relative; color: #FFF; From 3b91a1437aa72d7e0b8c8866fd13c7f0fe839eb4 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 22 Jan 2025 18:58:17 +0900 Subject: [PATCH 86/91] [accelerator] differentiate failed/canceled accelerations --- .../accelerations-list/accelerations-list.component.html | 3 ++- frontend/src/app/interfaces/node-api.interface.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html index 225bf1955..6756b23e4 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html @@ -64,7 +64,8 @@ Pending Completed ⌛ Mined ⌛ - Canceled ⌛ + Canceled ⌛ + Failed ⌛ - + From fc4a0f746134bd15e12ad4480573de714be99898 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Wed, 22 Jan 2025 21:59:15 -0800 Subject: [PATCH 88/91] Fix the missing frontend Stratum config for Docker builds --- docker/frontend/entrypoint.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/frontend/entrypoint.sh b/docker/frontend/entrypoint.sh index 2086188c9..9727adb2d 100644 --- a/docker/frontend/entrypoint.sh +++ b/docker/frontend/entrypoint.sh @@ -45,6 +45,7 @@ __SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services} __PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} +__STRATUM_ENABLED__=${STRATUM_ENABLED:=false} # Export as environment variables to be used by envsubst export __MAINNET_ENABLED__ @@ -76,6 +77,7 @@ export __SERVICES_API__ export __PUBLIC_ACCELERATIONS__ export __HISTORICAL_PRICE__ export __ADDITIONAL_CURRENCIES__ +export __STRATUM_ENABLED__ folder=$(find /var/www/mempool -name "config.js" | xargs dirname) echo ${folder} From e6965dac80fe937bfd3a7189fc98ce24f63c9c65 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Thu, 23 Jan 2025 18:00:27 +0900 Subject: [PATCH 89/91] [accelerator] fix sca card on file --- .../accelerate-checkout/accelerate-checkout.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index f15faccf7..2a4d681d9 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -757,9 +757,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { billing: { givenName: assumedGivenName, familyName: assumedFamilyName, - addressLines: [cardOnFile.card.billing.addressLine1], - city: cardOnFile.card.billing.locality, - state: cardOnFile.card.billing.administrativeDistrictLevel1, + addressLines: [cardOnFile.card.billing.addressLine1 ?? ''], + city: cardOnFile.card.billing.locality ?? '', + state: cardOnFile.card.billing.administrativeDistrictLevel1 ?? '', countyCode: cardOnFile.card.billing.country, } } From 24d0ed4ced4cd0fb8e32b39e94c0225b326290ad Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 24 Jan 2025 01:56:15 +0000 Subject: [PATCH 90/91] fix next block merkle row layout --- .../src/app/components/pool/pool.component.html | 12 +++++++----- .../src/app/components/pool/pool.component.scss | 13 +++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index f98794b68..35bad3af7 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -267,11 +267,13 @@ @for (branch of job.merkleBranches; track $index) { - @if ($index === 0 && branch) { - - } @else { - - } + } @for (_ of [].constructor(Math.max(0, 12 - job.merkleBranches.length)); track $index) { diff --git a/frontend/src/app/components/pool/pool.component.scss b/frontend/src/app/components/pool/pool.component.scss index fa94227bd..26e6008b6 100644 --- a/frontend/src/app/components/pool/pool.component.scss +++ b/frontend/src/app/components/pool/pool.component.scss @@ -241,6 +241,19 @@ div.scrollable { td { position: relative; height: 2em; + + .cell-link { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + color: inherit; + text-decoration: none; + display: flex; + justify-content: center; + align-items: center; + } } } From b826227b3626f383247a18bcc6c6d9774f41306b Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sat, 25 Jan 2025 15:46:21 +0900 Subject: [PATCH 91/91] [services] twitter -> X --- frontend/src/app/components/faucet/faucet.component.html | 8 ++++---- .../github-login.component/github-login.component.html | 4 ++-- .../components/twitter-login/twitter-login.component.html | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/faucet/faucet.component.html b/frontend/src/app/components/faucet/faucet.component.html index 19d76d9dd..dbe0f25ea 100644 --- a/frontend/src/app/components/faucet/faucet.component.html +++ b/frontend/src/app/components/faucet/faucet.component.html @@ -19,10 +19,10 @@ } @else if (!user) {
-
- To use the faucet, please  +
+ To use the faucet, please
- +
} @else if (user && user.status === 'pending' && !user.email && user.snsId) { @@ -39,7 +39,7 @@
To use the faucet, please
- +
} @else if (error === 'account_limited') { diff --git a/frontend/src/app/components/github-login.component/github-login.component.html b/frontend/src/app/components/github-login.component/github-login.component.html index de9c743b5..e57019a26 100644 --- a/frontend/src/app/components/github-login.component/github-login.component.html +++ b/frontend/src/app/components/github-login.component/github-login.component.html @@ -1,6 +1,6 @@ - + {{ buttonString }} + - {{ buttonString }} \ No newline at end of file diff --git a/frontend/src/app/components/twitter-login/twitter-login.component.html b/frontend/src/app/components/twitter-login/twitter-login.component.html index 6ff40bd50..5e687341c 100644 --- a/frontend/src/app/components/twitter-login/twitter-login.component.html +++ b/frontend/src/app/components/twitter-login/twitter-login.component.html @@ -1,6 +1,6 @@ - + style="background-color: rgb(31, 35, 40)" [style]="width ? 'width: ' + width : ''"> {{ buttonString }} +
HeightRewardCoinbase Tag Merkle Branches PoolCoinbase TagRewardHeight
- {{ row.job.height }} - - - - {{ row.job.tag }} - @if ($index === 0 && cell.hash) { @@ -47,6 +38,15 @@ } + {{ row.job.tag }} + + + + {{ row.job.height }} +
diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 4d85a938d..05f0855a9 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -412,13 +412,13 @@ export interface Acceleration { feeDelta: number; blockHash: string; blockHeight: number; - acceleratedFeeRate?: number; boost?: number; bidBoost?: number; boostCost?: number; boostRate?: number; minedByPoolUniqueId?: number; + canceled?: number; } export interface AccelerationHistoryParams { From b3f21a10b9a77d7198ca641e4139256301ccf2ef Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Thu, 23 Jan 2025 14:55:31 +0900 Subject: [PATCH 87/91] [accelerator] truncate dashboard title --- .../accelerations-list/accelerations-list.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html index 225bf1955..f4f7277ae 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html @@ -14,7 +14,7 @@ Requested Bid BoostBid Boost Block Pool Status
+ @if ($index === 0 && branch) { + + + + } +