diff --git a/backend/src/api/fee-api.ts b/backend/src/api/fee-api.ts index 0914a1098..56106c7f5 100644 --- a/backend/src/api/fee-api.ts +++ b/backend/src/api/fee-api.ts @@ -34,7 +34,17 @@ class FeeApi { const mPool = mempool.getMempoolInfo(); // minimum non-zero minrelaytxfee / incrementalrelayfee is 1 sat/kvB = 0.001 sat/vB - return this.calculateRecommendedFee(pBlocks, mPool, 0.001); + const recommendations = this.calculateRecommendedFee(pBlocks, mPool, 0.001); + // enforce floor & offset for highest priority recommendations while <100% hashrate accepts sub-sat fees + recommendations.fastestFee = Math.max(recommendations.fastestFee + this.priorityFactor, this.minFastestFee); + recommendations.halfHourFee = Math.max(recommendations.halfHourFee + (this.priorityFactor / 2), this.minHalfHourFee); + return { + 'fastestFee': Math.round(recommendations.fastestFee * 1000) / 1000, + 'halfHourFee': Math.round(recommendations.halfHourFee * 1000) / 1000, + 'hourFee': Math.round(recommendations.hourFee * 1000) / 1000, + 'economyFee': Math.round(recommendations.economyFee * 1000) / 1000, + 'minimumFee': Math.round(recommendations.minimumFee * 1000) / 1000, + }; } public calculateRecommendedFee(pBlocks: MempoolBlock[], mPool: IBitcoinApi.MempoolInfo, minIncrement: number = this.minimumIncrement): RecommendedFees { @@ -70,8 +80,8 @@ class FeeApi { hourFee = Math.max(hourFee, economyFee); return { - 'fastestFee': Math.max(this.roundToNearest(fastestFee + this.priorityFactor, minIncrement), this.minFastestFee), - 'halfHourFee': Math.max(this.roundToNearest(halfHourFee + (this.priorityFactor / 2), minIncrement), this.minHalfHourFee), + 'fastestFee': this.roundToNearest(fastestFee, minIncrement), + 'halfHourFee': this.roundToNearest(halfHourFee, minIncrement), 'hourFee': this.roundToNearest(hourFee, minIncrement), 'economyFee': this.roundToNearest(economyFee, minIncrement), 'minimumFee': this.roundToNearest(minimumFee, minIncrement), diff --git a/backend/src/indexer.ts b/backend/src/indexer.ts index b3bfd2521..ca0b2303c 100644 --- a/backend/src/indexer.ts +++ b/backend/src/indexer.ts @@ -26,6 +26,7 @@ class Indexer { private indexerRunning = false; private tasksRunning: { [key in TaskName]?: boolean; } = {}; private tasksScheduled: { [key in TaskName]?: NodeJS.Timeout; } = {}; + private reindexTimeout: NodeJS.Timeout | undefined; private coreIndexes: CoreIndex[] = []; public indexerIsRunning(): boolean { @@ -76,10 +77,23 @@ class Indexer { public reindex(): void { if (Common.indexingEnabled()) { + if (this.reindexTimeout) { + clearTimeout(this.reindexTimeout); + this.reindexTimeout = undefined; + } this.runIndexer = true; } } + private scheduleNextRun(timeout: number): void { + if (!this.reindexTimeout) { // Only one future run should be planned, ignore if already scheduled + this.reindexTimeout = setTimeout(() => { + this.reindexTimeout = undefined; + this.reindex(); + }, timeout); + } + } + /** * schedules a single task to run in `timeout` ms * only one task of each type may be scheduled @@ -156,34 +170,40 @@ class Indexer { return; } - if (config.FIAT_PRICE.ENABLED) { - try { - await priceUpdater.$run(); - } catch (e) { - logger.err(`Running priceUpdater failed. Reason: ` + (e instanceof Error ? e.message : e)); - } - } - - // Do not attempt to index anything unless Bitcoin Core is fully synced - const blockchainInfo = await bitcoinClient.getBlockchainInfo(); - if (blockchainInfo.blocks !== blockchainInfo.headers) { - return; - } - this.runIndexer = false; this.indexerRunning = true; - logger.debug(`Running mining indexer`); - - await this.checkAvailableCoreIndexes(); + const retryDelay = 10000; + const runEvery = 1000 * 3600; // 1 hour + let nextRunDelay = runEvery; + let runSuccessful = false; try { + if (config.FIAT_PRICE.ENABLED) { + try { + await priceUpdater.$run(); + } catch (e) { + logger.err(`Running priceUpdater failed. Reason: ` + (e instanceof Error ? e.message : e)); + } + } + + // Do not attempt to index anything unless Bitcoin Core is fully synced + const blockchainInfo = await bitcoinClient.getBlockchainInfo(); + if (blockchainInfo.blocks !== blockchainInfo.headers) { + logger.debug(`Bitcoin Core not fully synced, retrying index run in 10 seconds.`); + nextRunDelay = retryDelay; + return; + } + + logger.debug(`Running mining indexer`); + + await this.checkAvailableCoreIndexes(); + const chainValid = await blocks.$generateBlockDatabase(); if (chainValid === false) { // Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`, logger.tags.mining); - setTimeout(() => this.reindex(), 10000); - this.indexerRunning = false; + nextRunDelay = retryDelay; return; } @@ -202,19 +222,20 @@ class Indexer { await BlocksRepository.$migrateBlocks(); // do not wait for classify blocks to finish blocks.$classifyBlocks(); + runSuccessful = true; } catch (e) { - this.indexerRunning = false; + nextRunDelay = retryDelay; logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e)); - setTimeout(() => this.reindex(), 10000); + } finally { this.indexerRunning = false; - return; + const nextRunAt = new Date(Date.now() + nextRunDelay).toUTCString(); + if (runSuccessful) { + logger.debug(`Indexing completed. Next run planned at ${nextRunAt}`); + } else { + logger.debug(`Indexing did not complete, next run planned at ${nextRunAt}`); + } + this.scheduleNextRun(nextRunDelay); } - - this.indexerRunning = false; - - const runEvery = 1000 * 3600; // 1 hour - logger.debug(`Indexing completed. Next run planned at ${new Date(new Date().getTime() + runEvery).toUTCString()}`); - setTimeout(() => this.reindex(), runEvery); } } 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 2888965a2..0240fe00c 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -216,8 +216,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } if (this._step === 'checkout' && this.canPayWithBitcoin) { this.btcpayInvoiceFailed = false; - this.invoice = null; + this.invoice = undefined; this.requestBTCPayInvoice(); + this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 100); } else if (this._step === 'cashapp') { this.loadingCashapp = true; this.setupSquare(); diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html index 9146c8e34..114a20913 100644 --- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html +++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html @@ -11,12 +11,6 @@
- - 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 74860be22..e8a1e3ee2 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 @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; -import { EChartsOption } from '@app/graphs/echarts'; +import { echarts, EChartsOption } from '@app/graphs/echarts'; import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs'; import { startWith, switchMap, tap } from 'rxjs/operators'; import { SeoService } from '@app/services/seo.service'; @@ -12,7 +12,6 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Acceleration } from '@interfaces/node-api.interface'; import { ServicesApiServices } from '@app/services/services-api.service'; import { StateService } from '@app/services/state.service'; -import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; @Component({ selector: 'app-acceleration-fees-graph', @@ -32,9 +31,9 @@ import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pip export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDestroy { @Input() widget: boolean = false; @Input() height: number = 300; - @Input() right: number | string = 45; - @Input() left: number | string = 75; - @Input() period: '24h' | '3d' | '1w' | '1m' | 'all' = '1w'; + @Input() right: number | string = 70; + @Input() left: number | string = 55; + @Input() period: '24h' | '1w' | '1m' | '1y' | 'all' = '1y'; @Input() accelerations$: Observable; miningWindowPreference: string; @@ -52,7 +51,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest isLoading = true; formatNumber = formatNumber; timespan = ''; - periodSubject$: Subject<'24h' | '3d' | '1w' | '1m' | 'all'> = new Subject(); + periodSubject$: Subject<'24h' | '1w' | '1m' | '1y' | 'all'> = new Subject(); chartInstance: any = undefined; daysAvailable: number = 0; @@ -65,9 +64,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest private miningService: MiningService, private route: ActivatedRoute, public stateService: StateService, - private cd: ChangeDetectorRef, - private router: Router, - private zone: NgZone, + private cd: ChangeDetectorRef ) { this.radioGroupForm = this.formBuilder.group({ dateSpan: '1w' }); this.radioGroupForm.controls.dateSpan.setValue('1w'); @@ -84,7 +81,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); this.fragmentSubscription = this.route.fragment.subscribe((fragment) => { - if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) { + if (['1w', '1m', '1y', 'all'].indexOf(fragment) > -1) { this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); } }); @@ -99,7 +96,9 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest if (!this.widget) { this.storageService.setValue('miningWindowPreference', timespan); } - this.isLoading = true; + if (timespan !== this.timespan) { + this.isLoading = true; + } this.timespan = timespan; return this.servicesApiService.getAggregatedAccelerationHistory$({timeframe: this.timespan}); }) @@ -121,6 +120,9 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest ngOnChanges(changes: SimpleChanges): void { if (changes.period) { + if (this.period === '24h') { + this.period = '1m'; + } this.periodSubject$.next(this.period); } } @@ -142,13 +144,19 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest this.chartOptions = { title: title, color: [ - '#8F5FF6', - '#6b6b6b', + new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [ + { offset: 0, color: '#F4511E' }, + { offset: 0.25, color: '#FB8C00' }, + { offset: 0.5, color: '#FFB300' }, + { offset: 0.75, color: '#FDD835' }, + { offset: 1, color: '#7CB342' } + ]), + '#ab2dce', ], animation: false, grid: { - height: (this.widget && this.height) ? this.height - 30 : undefined, - top: this.widget ? 20 : 40, + height: (this.widget && this.height) ? this.height - 50 : undefined, + top: this.widget ? 40 : 60, bottom: this.widget ? 30 : 80, right: this.right, left: this.left, @@ -170,17 +178,18 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest formatter: (ticks) => { let tooltip = `${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10))}
`; - if (ticks[0].data[1] > 10_000_000) { - tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1] / 100_000_000, this.locale, '1.0-8')} BTC
`; - } else { - tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1], this.locale, '1.0-0')} sats
`; - } - - if (['24h', '3d'].includes(this.timespan)) { - tooltip += `` + $localize`At block: ${ticks[0].data[2]}` + ``; - } else { - tooltip += `` + $localize`Around block: ${ticks[0].data[2]}` + ``; + for (const tick of ticks) { + if (tick.seriesName === 'Total bid boost') { + if (tick.data[1] > 10_000_000) { + tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1] / 100_000_000, this.locale, '1.0-8')} BTC
`; + } else { + tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')} sats
`; + } + } else if (tick && tick.seriesName === 'Accelerated') { + tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}
`; + } } + tooltip += `` + $localize`Around block: ${ticks[0].data[2]}` + ``; return tooltip; } @@ -212,6 +221,17 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest textStyle: { color: 'white', }, + itemStyle: { + color: '#FFB300', + }, + icon: 'roundRect', + }, + { + name: 'Accelerated', + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, icon: 'roundRect', }, ], @@ -223,6 +243,13 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest yAxis: data.length === 0 ? undefined : [ { type: 'value', + name: 'Total bid boost', + position: 'right', + nameTextStyle: { + align: 'right', + padding: [0, -65, 0, 0], + fontStyle: 'italic', + }, axisLabel: { color: 'rgb(110, 112, 121)', formatter: (val) => { @@ -233,6 +260,20 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest } } }, + splitLine: null + }, + { + type: 'value', + name: 'Accelerated', + position: 'left', + axisLabel: { + color: 'rgb(110, 112, 121)', + }, + nameTextStyle: { + align: 'right', + padding: [0, -35, 0, 0], + fontStyle: 'italic', + }, splitLine: { lineStyle: { type: 'dotted', @@ -241,33 +282,28 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest } }, }, - { - type: 'value', - position: 'right', - axisLabel: { - color: 'rgb(110, 112, 121)', - formatter: function(val) { - return `${val}`; - }.bind(this) - }, - splitLine: { - show: false, - }, - }, ], series: data.length === 0 ? undefined : [ { - legendHoverLink: false, - zlevel: 1, name: 'Total bid boost', data: data.map(h => { return [h.timestamp * 1000, h.sumBidBoost, h.avgHeight] }), - stack: 'Total', + type: 'line', + symbol: 'none', + lineStyle: { + width: 1, + }, + smooth: true, + }, + { + name: 'Accelerated', + yAxisIndex: 1, + data: data.map(h => { + return [h.timestamp * 1000, h.count, h.avgHeight] + }), type: 'bar', barWidth: '90%', - large: true, - barMinHeight: 3, }, ], dataZoom: (this.widget || data.length === 0 )? undefined : [{ @@ -300,19 +336,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest onChartInit(ec) { this.chartInstance = ec; - - this.chartInstance.on('click', (e) => { - this.zone.run(() => { - if (['24h', '3d'].includes(this.timespan)) { - const url = new RelativeUrlPipe(this.stateService).transform(`/block/${e.data[2]}`); - if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) { - window.open(url); - } else { - this.router.navigate([url]); - } - } - }); - }); } isMobile() { diff --git a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts index 5e077ba7d..f964fcc6b 100644 --- a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts +++ b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts @@ -17,7 +17,7 @@ export type AccelerationStats = { standalone: false, }) export class AccelerationStatsComponent implements OnInit, OnChanges { - @Input() timespan: '24h' | '3d' | '1w' | '1m' | 'all' = '1w'; + @Input() timespan: '24h' | '1m' | '1y' | 'all' = '1y'; accelerationStats$: Observable; blocksInPeriod: number = 7 * 144; @@ -39,15 +39,12 @@ export class AccelerationStatsComponent implements OnInit, OnChanges { case '24h': this.blocksInPeriod = 144; break; - case '3d': - this.blocksInPeriod = 3 * 144; - break; - case '1w': - this.blocksInPeriod = 7 * 144; - break; case '1m': - this.blocksInPeriod = 30 * 144; + this.blocksInPeriod = 30.5 * 144; break; + case '1y': + this.blocksInPeriod = 30.5 * 144 * 365; + break; case 'all': this.blocksInPeriod = Infinity; break; diff --git a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html index b2a0aad0f..b45b18526 100644 --- a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html +++ b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html @@ -26,12 +26,12 @@ @case ('24h') { (1 day) } - @case ('1w') { - (1 week) - } @case ('1m') { (1 month) } + @case ('1y') { + (1 year) + } @case ('all') { (all time) } @@ -45,12 +45,12 @@ 24h | - 1w - | 1m | + 1y + | all
@@ -79,13 +79,15 @@
-
Total Bid Boost
+
Historical Trend
diff --git a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts index 5fb7209fc..f45b4c91f 100644 --- a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts +++ b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts @@ -38,7 +38,7 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy { webGlEnabled = true; seen: Set = new Set(); firstLoad = true; - timespan: '24h' | '3d' | '1w' | '1m' | 'all' = '1w'; + timespan: '24h' | '1m' | '1y' | 'all' = '1y'; accelerationDeltaSubscription: Subscription;