Merge branch 'master' into natsoni/contrast-theme

This commit is contained in:
natsoni 2024-04-08 14:54:38 +09:00
commit c251b5831b
No known key found for this signature in database
GPG Key ID: C65917583181743B
79 changed files with 17384 additions and 6148 deletions

View File

@ -103,7 +103,7 @@ In particular, make sure:
- the correct Bitcoin Core RPC credentials are specified in `CORE_RPC`
- the correct `BACKEND` is specified in `MEMPOOL`:
- "electrum" if you're using [romanz/electrs](https://github.com/romanz/electrs) or [cculianu/Fulcrum](https://github.com/cculianu/Fulcrum)
- "esplora" if you're using [Blockstream/electrs](https://github.com/Blockstream/electrs)
- "esplora" if you're using [mempool/electrs](https://github.com/mempool/electrs)
- "none" if you're not using any Electrum Server
### 6. Run Mempool Backend

View File

@ -0,0 +1,75 @@
import { Application, Request, Response } from "express";
import config from "../../config";
import axios from "axios";
import logger from "../../logger";
class AccelerationRoutes {
private tag = 'Accelerator';
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations', this.$getAcceleratorAccelerations.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this))
;
}
private async $getAcceleratorAccelerations(req: Request, res: Response) {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get current accelerations from ${url} in $getAcceleratorAccelerations(), ${e}`, this.tag);
res.status(500).end();
}
}
private async $getAcceleratorAccelerationsHistory(req: Request, res: Response) {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get acceleration history from ${url} in $getAcceleratorAccelerationsHistory(), ${e}`, this.tag);
res.status(500).end();
}
}
private async $getAcceleratorAccelerationsHistoryAggregated(req: Request, res: Response) {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get aggregated acceleration history from ${url} in $getAcceleratorAccelerationsHistoryAggregated(), ${e}`, this.tag);
res.status(500).end();
}
}
private async $getAcceleratorAccelerationsStats(req: Request, res: Response) {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get acceleration stats from ${url} in $getAcceleratorAccelerationsStats(), ${e}`, this.tag);
res.status(500).end();
}
}
}
export default new AccelerationRoutes();

View File

@ -1,6 +1,6 @@
import logger from '../logger';
import { MempoolTransactionExtended } from '../mempool.interfaces';
import { IEsploraApi } from './bitcoin/esplora-api.interface';
import logger from '../../logger';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
const BLOCK_WEIGHT_UNITS = 4_000_000;
const BLOCK_SIGOPS = 80_000;

View File

@ -373,6 +373,21 @@ export class Common {
].includes(pubkey);
}
static isInscription(vin, flags): bigint {
// in taproot, if the last witness item begins with 0x50, it's an annex
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
// script spends have more than one witness item, not counting the annex (if present)
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
// the script itself is the second-to-last witness item, not counting the annex
const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]);
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
if (asm?.includes('OP_0 OP_IF')) {
flags |= TransactionFlags.inscription;
}
}
return flags;
}
static getTransactionFlags(tx: TransactionExtended): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n;
@ -409,6 +424,7 @@ export class Common {
if (vin.sequence < 0xfffffffe) {
rbf = true;
}
if (vin.prevout?.scriptpubkey_type) {
switch (vin.prevout?.scriptpubkey_type) {
case 'p2pk': flags |= TransactionFlags.p2pk; break;
case 'multisig': flags |= TransactionFlags.p2ms; break;
@ -421,19 +437,19 @@ export class Common {
throw new Error('Taproot input missing witness data');
}
flags |= TransactionFlags.p2tr;
// in taproot, if the last witness item begins with 0x50, it's an annex
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
// script spends have more than one witness item, not counting the annex (if present)
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
// the script itself is the second-to-last witness item, not counting the annex
const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]);
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
if (asm?.includes('OP_0 OP_IF')) {
flags |= TransactionFlags.inscription;
}
}
flags = Common.isInscription(vin, flags);
} break;
}
} else {
// no prevouts, optimistically check witness-bearing inputs
if (vin.witness?.length >= 2) {
try {
flags = Common.isInscription(vin, flags);
} catch {
// witness script parsing will fail if this isn't really a taproot output
}
}
}
// sighash flags
if (vin.prevout?.scriptpubkey_type === 'v1_p2tr') {

View File

@ -655,6 +655,7 @@ class DatabaseMigration {
await this.$executeQuery('TRUNCATE hashrates');
await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
await this.updateToSchemaVersion(75);
}

View File

@ -343,7 +343,7 @@ class MempoolBlocks {
if (txid in mempool) {
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
mempool[txid].effectiveFeePerVsize = rate;
mempool[txid].cpfpChecked = false;
mempool[txid].cpfpChecked = true;
}
}

View File

@ -739,7 +739,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
}
};
if (!mempoolTx.cpfpChecked) {
if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) {
calculateCpfp(mempoolTx, newMempool);
}
if (mempoolTx.cpfpDirty) {

View File

@ -43,6 +43,7 @@ import redisCache from './api/redis-cache';
import accelerationApi from './api/services/acceleration';
import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes';
class Server {
@ -306,6 +307,9 @@ class Server {
nodesRoutes.initRoutes(this.app);
channelsRoutes.initRoutes(this.app);
}
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
accelerationRoutes.initRoutes(this.app);
}
aboutRoutes.initRoutes(this.app);
}

View File

@ -1,4 +1,4 @@
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration';
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration/acceleration';
import { RowDataPacket } from 'mysql2';
import DB from '../database';
import logger from '../logger';
@ -7,7 +7,7 @@ import { Common } from '../api/common';
import config from '../config';
import blocks from '../api/blocks';
import accelerationApi, { Acceleration } from '../api/services/acceleration';
import accelerationCosts from '../api/acceleration';
import accelerationCosts from '../api/acceleration/acceleration';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import transactionUtils from '../api/transaction-utils';
import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces';

3
contributors/daweilv.txt Normal file
View File

@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of April 7, 2024.
Signed: daweilv

View File

@ -32,7 +32,7 @@
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video>
<ng-container *ngIf="officialMempoolSpace">
<ng-container>
<app-about-sponsors></app-about-sponsors>
</ng-container>
@ -181,7 +181,7 @@
</div>
</div>
<ng-container *ngIf="officialMempoolSpace">
<ng-container>
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>

View File

@ -52,7 +52,7 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
rate: option.rate,
style: this.getStyle(option.rate, maxRate, baseHeight),
class: 'max',
label: 'maximum',
label: $localize`maximum`,
active: option.index === this.maxRateIndex,
rateIndex: option.index,
fee: option.fee,
@ -63,7 +63,7 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
rate: this.estimate.targetFeeRate,
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
class: 'target',
label: 'next block',
label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(),
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
});
}

View File

@ -32,18 +32,16 @@
<div class="alert alert-mempool">You are currently on the waitlist</div>
</div>
<h5>Your transaction</h5>
<h5 i18n="accelerator.your-transaction">Your transaction</h5>
<div class="row">
<div class="col">
<small *ngIf="hasAncestors" class="form-text text-muted mb-2">
Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor{{ estimate.txSummary.ancestorCount > 2 ? 's' : ''}}.
<ng-container i18n="accelerator.plus-unconfirmed-ancestors">Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s)</ng-container>
</small>
<table class="table table-borderless table-border table-dark table-accelerator">
<tbody>
<tr class="group-first">
<td class="item">
Virtual size
</td>
<td class="item" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td style="text-align: end;" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
</tr>
<tr class="info">
@ -52,9 +50,7 @@
</td>
</tr>
<tr>
<td class="item">
In-band fees
</td>
<td class="item" i18n="accelerator.in-band-fees">In-band fees</td>
<td style="text-align: end;">
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span>
</td>
@ -69,13 +65,10 @@
</div>
</div>
<br>
<h5>How much more are you willing to pay?</h5>
<h5 i18n="accelerator.pay-how-much">How much more are you willing to pay?</h5>
<div class="row">
<div class="col">
<small class="form-text text-muted mb-2">
Choose the maximum extra transaction fee you're willing to pay to get into the next block.<br>
If the estimated next block rate rises beyond this limit, we will automatically cancel your acceleration request.
</small>
<small class="form-text text-muted mb-2" i18n="accelerator.transaction-fee-description">Choose the maximum extra transaction fee you're willing to pay to get into the next block.</small>
<div class="form-group">
<div class="fee-card">
<div class="d-flex mb-0">
@ -99,9 +92,7 @@
<!-- ESTIMATED FEE -->
<ng-container>
<tr class="group-first">
<td class="item">
Next block market rate
</td>
<td class="item" i18n="accelerator.next-block-rate">Next block market rate</td>
<td class="amt" style="font-size: 16px">
{{ estimate.targetFeeRate | number : '1.0-0' }}
</td>
@ -109,7 +100,7 @@
</tr>
<tr class="info">
<td class="info">
<i><small>Estimated extra fee required</small></i>
<i><small i18n="accelerator.estimated-extra-fee-required">Estimated extra fee required</small></i>
</td>
<td class="amt">
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
@ -123,13 +114,11 @@
<!-- MEMPOOL BASE FEE -->
<tr>
<td class="item">
Mempool Accelerator™ fees
</td>
<td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator™ fees</td>
</tr>
<tr class="info">
<td class="info">
<i><small>Accelerator Service Fee</small></i>
<i><small i18n="accelerator.service-fee">Accelerator Service Fee</small></i>
</td>
<td class="amt">
+{{ estimate.mempoolBaseFee | number }}
@ -141,7 +130,7 @@
</tr>
<tr class="info group-last">
<td class="info">
<i><small>Transaction Size Surcharge</small></i>
<i><small i18n="accelerator.tx-size-surcharge">Transaction Size Surcharge</small></i>
</td>
<td class="amt">
+{{ estimate.vsizeFee | number }}
@ -156,7 +145,7 @@
<ng-container>
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
<td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
<b style="background-color: #5E35B1" class="p-1 pl-0" i18n="accelerator.estimated-cost">Estimated acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: #5E35B1" class="p-1 pl-0">
@ -170,7 +159,7 @@
</tr>
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<td class="info" colspan=3>
<i><small>If your tx is accelerated to </small><small>{{ estimate.targetFeeRate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
<i><small><ng-container *ngTemplateOutlet="acceleratedTo; context: {$implicit: estimate.targetFeeRate }"></ng-container></small></i>
</td>
</tr>
</ng-container>
@ -179,7 +168,7 @@
<ng-container>
<tr class="group-first">
<td class="item">
<b style="background-color: var(--primary);" class="p-1 pl-0">Maximum acceleration cost</b>
<b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.maximum-cost">Maximum acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: var(--primary)" class="p-1 pl-0">
@ -195,7 +184,7 @@
</tr>
<tr class="info group-last">
<td class="info" colspan=3>
<i><small>If your tx is accelerated to </small><small>~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
<i><small><ng-container *ngTemplateOutlet="acceleratedTo; context: {$implicit: (estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize }"></ng-container></small></i>
</td>
</tr>
</ng-container>
@ -203,9 +192,7 @@
<!-- USER BALANCE -->
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item">
Available balance
</td>
<td class="item" i18n="accelerator.available-balance">Available balance</td>
<td class="amt">
{{ estimate.userBalance | number }}
</td>
@ -219,12 +206,12 @@
</ng-container>
<!-- LOGIN CTA -->
<ng-container *ngIf="stateService.isMempoolSpaceBuild && !isLoggedIn()">
<ng-container *ngIf="stateService.isMempoolSpaceBuild && !isLoggedIn() && paymentType === 'bitcoin'">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td>
<td class="amt"></td>
<td class="units d-flex">
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1">Login</a>
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1" i18n="shared.sign-in">Sign In</a>
</td>
</tr>
</ng-container>
@ -233,7 +220,7 @@
<td class="item"></td>
<td class="amt"></td>
<td class="units d-flex">
<a [href]="'https://mempool.space/tx/' + tx.txid + '#accelerate'" class="btn btn-purple flex-grow-1">Accelerate on mempool.space</a>
<a [href]="'https://mempool.space/tx/' + tx.txid + '#accelerate'" class="btn btn-purple flex-grow-1" i18n="accelerator.accelerate-on-mempoolspace">Accelerate on mempool.space</a>
</td>
</tr>
</ng-container>
@ -242,14 +229,23 @@
</div>
</div>
<div class="row mb-3" *ngIf="isLoggedIn()">
<div class="row mb-3" *ngIf="isLoggedIn() && paymentType === 'bitcoin'">
<div class="col">
<div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess">
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button>
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()" i18n="transaction.accelerate|Accelerate button label">Accelerate</button>
</div>
</div>
</div>
<div class="row d-flex justify-content-end align-items-center mr-1" style="height: 48px" *ngIf="!hideCashApp && paymentType === 'cashapp'">
<div [style]="showSpinner ? 'opacity: 0' : 'opacity: 1'" class="p-2">Accelerate with</div>
<div id="cash-app-pay" style="max-width: 320px" [style]="showSpinner ? 'opacity: 0' : 'opacity: 1'"></div>
<div *ngIf="showSpinner" class="d-flex align-items-center">
<span class="mr-2">Loading</span>
<div class="spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
</div>
</div>
</ng-container>
</div>
@ -258,3 +254,5 @@
<div class="skeleton-loader"></div>
<br>
</ng-template>
<ng-template #acceleratedTo let-i i18n="accelerator.accelerated-to-description">If your tx is accelerated to ~{{ i | number : '1.0-0' }} sat/vB</ng-template>

View File

@ -1,5 +1,4 @@
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core';
import { ApiService } from '../../services/api.service';
import { Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service';
import { Transaction } from '../../interfaces/electrs.interface';
@ -63,18 +62,44 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
maxRateOptions: RateOption[] = [];
// Cashapp payment
paymentType: 'bitcoin' | 'cashapp' = 'bitcoin';
cashAppSubscription: Subscription;
conversionsSubscription: Subscription;
payments: any;
showSpinner = false;
square: any;
cashAppPay: any;
hideCashApp = false;
constructor(
public stateService: StateService,
private servicesApiService: ServicesApiServices,
private storageService: StorageService,
private audioService: AudioService,
private cd: ChangeDetectorRef
) { }
) {
if (this.stateService.ref === 'https://cash.app/') {
this.insertSquare();
}
}
ngOnDestroy(): void {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
if (this.cashAppPay) {
this.cashAppPay.destroy();
}
}
ngOnInit() {
if (this.stateService.ref === 'https://cash.app/') {
this.paymentType = 'cashapp';
this.stateService.ref = '';
} else {
this.paymentType = 'bitcoin';
}
}
ngOnChanges(changes: SimpleChanges): void {
@ -83,9 +108,18 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
}
}
ngOnInit() {
ngAfterViewInit() {
if (this.paymentType === 'cashapp') {
this.showSpinner = true;
}
this.user = this.storageService.getAuth()?.user ?? null;
this.servicesApiService.setupSquare$().subscribe(ids => {
this.square = {
appId: ids.squareAppId,
locationId: ids.squareLocationId
};
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
tap((response) => {
if (response.status === 204) {
@ -101,6 +135,11 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.estimateSubscription.unsubscribe();
}
if (this.paymentType === 'cashapp') {
this.estimate.userBalance = 999999999;
this.estimate.enoughBalance = true;
}
if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) {
if (this.isLoggedIn()) {
this.error = `not_enough_balance`;
@ -135,6 +174,9 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
if (!this.error) {
this.scrollToPreview('acceleratePreviewAnchor', 'start');
if (this.paymentType === 'cashapp') {
this.setupSquare();
}
}
}
}),
@ -146,6 +188,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
return of(null);
})
).subscribe();
});
}
/**
@ -216,4 +259,112 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
/**
* CashApp payment
*/
setupSquare() {
const init = () => {
this.initSquare();
};
//@ts-ignore
if (!window.Square) {
console.warn('Square.js failed to load properly. Retrying in 1 second.');
setTimeout(init, 1000);
} else {
init();
}
}
async initSquare(): Promise<void> {
try {
//@ts-ignore
this.payments = window.Square.payments(this.square.appId, this.square.locationId)
await this.requestCashAppPayment();
} catch (e) {
console.error(e);
this.error = 'Error loading Square Payments';
return;
}
}
async requestCashAppPayment() {
if (this.cashAppSubscription) {
this.cashAppSubscription.unsubscribe();
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.hideCashApp = false;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
const maxCostUsd = this.maxCost / 100_000_000 * conversions.USD;
const paymentRequest = this.payments.paymentRequest({
countryCode: 'US',
currencyCode: 'USD',
total: {
amount: maxCostUsd.toString(),
label: 'Total',
pending: true,
productUrl: `https://mempool.space/tx/${this.tx.txid}`,
}
});
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
redirectURL: `https://mempool.space/tx/${this.tx.txid}`,
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
});
await this.cashAppPay.attach('#cash-app-pay');
this.showSpinner = false;
const that = this;
this.cashAppPay.addEventListener('ontokenization', function (event) {
const { tokenResult, error } = event.detail;
if (error) {
this.error = error;
} else if (tokenResult.status === 'OK') {
that.hideCashApp = true;
that.accelerationSubscription = that.servicesApiService.accelerateWithCashApp$(
that.tx.txid,
that.userBid,
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId
).subscribe({
next: () => {
that.audioService.playSound('ascend-chime-cartoon');
that.showSuccess = true;
that.scrollToPreviewWithTimeout('successAlert', 'center');
that.estimateSubscription.unsubscribe();
},
error: (response) => {
if (response.status === 403 && response.error === 'not_available') {
that.error = 'waitlisted';
} else {
that.error = response.error;
}
that.scrollToPreviewWithTimeout('mempoolError', 'center');
}
});
}
});
}
);
}
insertSquare(): void {
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
if (document.location.hostname === 'mempool-staging.tk7.mempool.space' || document.location.hostname === 'mempool.space') {
statsUrl = 'https://web.squarecdn.com/v1/square.js';
}
(function() {
const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
// @ts-ignore
g.type='text/javascript'; g.src=statsUrl; s.parentNode.insertBefore(g, s);
})();
}
}

View File

@ -1,5 +1,5 @@
<div class="container-lg widget-container" [class.widget]="widget" [class.full-height]="!widget">
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1>
<h1 *ngIf="!widget" class="float-left" i18n="accelerator.accelerations">Accelerations</h1>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div>

View File

@ -38,7 +38,7 @@
<div class="card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<a class="title-link" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.mempool-goggles-accelerations">Mempool Goggles: Accelerations</h5>
<h5 class="card-title d-inline">Mempool Goggles&trade; : <ng-container i18n="accelerator.accelerations">Accelerations</ng-container></h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>

View File

@ -11,11 +11,12 @@ import TxView from '../../block-overview-graph/tx-view';
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../../app.constants';
import { ServicesApiServices } from '../../../services/services-api.service';
import { detectWebGL } from '../../../shared/graphs.utils';
import { AudioService } from '../../../services/audio.service';
import { ThemeService } from '../../../services/theme.service';
const acceleratedColor: Color = hexToColor('8F5FF6');
const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F'));
const contrastColors = contrastMempoolFeeColors.map(hex => hexToColor(hex + '5F'));
const contrastColors = contrastMempoolFeeColors.map(hex => hexToColor(hex.slice(0,6) + '5F'));
interface AccelerationBlock extends BlockExtended {
accelerationCount: number,
@ -34,6 +35,8 @@ export class AcceleratorDashboardComponent implements OnInit {
minedAccelerations$: Observable<Acceleration[]>;
loadingBlocks: boolean = true;
webGlEnabled = true;
seen: Set<string> = new Set();
firstLoad = true;
graphHeight: number = 300;
theme: ThemeService;
@ -43,6 +46,7 @@ export class AcceleratorDashboardComponent implements OnInit {
private ogService: OpenGraphService,
private websocketService: WebsocketService,
private serviceApiServices: ServicesApiServices,
private audioService: AudioService,
private stateService: StateService,
@Inject(PLATFORM_ID) private platformId: Object,
) {
@ -64,6 +68,15 @@ export class AcceleratorDashboardComponent implements OnInit {
}),
);
}),
tap(accelerations => {
if (!this.firstLoad && accelerations.some(acc => !this.seen.has(acc.txid))) {
this.audioService.playSound('bright-harmony');
}
for(const acc of accelerations) {
this.seen.add(acc.txid);
}
this.firstLoad = false;
}),
share(),
);

View File

@ -6,6 +6,7 @@ import { ChainStats } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
import { Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-address-graph',
@ -46,6 +47,7 @@ export class AddressGraphComponent implements OnChanges {
private router: Router,
private amountShortenerPipe: AmountShortenerPipe,
private cd: ChangeDetectorRef,
private relativeUrlPipe: RelativeUrlPipe,
) {}
ngOnChanges(changes: SimpleChanges): void {
@ -178,7 +180,7 @@ export class AddressGraphComponent implements OnChanges {
onChartClick(e) {
if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) {
this.router.navigate(['/tx/', this.hoverData[0][2].txid]);
this.router.navigate([this.relativeUrlPipe.transform('/tx/'), this.hoverData[0][2].txid]);
}
}

View File

@ -20,8 +20,7 @@
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
<span i18n="shared.confidential">Confidential</span>
</ng-template>
<ng-template #default>
&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
<ng-template #default>&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
<span class="symbol"><ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
<ng-template [ngIf]="network === 'testnet'">t</ng-template>

View File

@ -1,10 +1,10 @@
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.any-mode]="filterMode === 'or'" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
<a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles tooltip" ngbTooltip="select filter categories to highlight matching transactions">
<a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles&trade; tooltip" ngbTooltip="select filter categories to highlight matching transactions">
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
</a>
<div class="filter-bar">
<button class="menu-toggle" (click)="menuOpen = !menuOpen" title="Mempool Goggles">
<button class="menu-toggle" (click)="menuOpen = !menuOpen" title="Mempool Goggles&trade;">
<app-svg-images name="goggles" width="100%" height="100%"></app-svg-images>
</button>
<div class="active-tags">
@ -14,15 +14,30 @@
</div>
</div>
<div class="filter-menu" *ngIf="menuOpen && cssWidth > 280">
<h5>Match</h5>
<div class="filter-row">
<div class="filter-element">
<h5 i18n="mempool-goggles.match">Match</h5>
<div class="btn-group btn-group-toggle">
<label class="btn btn-xs blue mode-toggle" [class.active]="filterMode === 'and'">
<input type="radio" [value]="'all'" fragment="all" (click)="setFilterMode('and')">All
<input type="radio" [value]="'all'" fragment="all" (click)="setFilterMode('and')"><ng-container i18n>All</ng-container>
</label>
<label class="btn btn-xs green mode-toggle" [class.active]="filterMode === 'or'">
<input type="radio" [value]="'any'" fragment="any" (click)="setFilterMode('or')">Any
<input type="radio" [value]="'any'" fragment="any" (click)="setFilterMode('or')"><ng-container i18n="mempool-goggles.any">Any</ng-container>
</label>
</div>
</div>
<div class="filter-element">
<h5 i18n="mempool-goggles.tint">Tint</h5>
<div class="btn-group btn-group-toggle">
<label class="btn btn-xs yellow mode-toggle" [class.active]="gradientMode === 'fee'">
<input type="radio" [value]="'fee'" fragment="classic" (click)="setGradientMode('fee')"><ng-container i18n="mempool-goggles.classic">Classic</ng-container>
</label>
<label class="btn btn-xs blue mode-toggle" [class.active]="gradientMode === 'age'">
<input type="radio" [value]="'age'" fragment="age" (click)="setGradientMode('age')"><ng-container i18n="mempool-goggles.age">Age</ng-container>
</label>
</div>
</div>
</div>
<ng-container *ngFor="let group of filterGroups;">
<h5>{{ group.label }}</h5>
<div class="filter-group">

View File

@ -45,6 +45,13 @@
}
.filter-menu {
.filter-row {
display: flex;
flex-direction: row;
justify-content: start;
align-items: baseline;
}
h5 {
font-size: 0.8rem;
color: white;
@ -118,6 +125,12 @@
background: var(--success);
}
}
&.yellow {
border: solid 1px #bf7815;
&.active {
background: #bf7815;
}
}
}
:host-context(.block-overview-graph:hover) &, &:hover, &:active {

View File

@ -1,5 +1,5 @@
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
import { ActiveFilter, FilterGroups, FilterMode, TransactionFilters } from '../../shared/filters.utils';
import { ActiveFilter, FilterGroups, FilterMode, GradientMode, TransactionFilters } from '../../shared/filters.utils';
import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs';
@ -22,6 +22,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
activeFilters: string[] = [];
filterFlags: { [key: string]: boolean } = {};
filterMode: FilterMode = 'and';
gradientMode: GradientMode = 'fee';
menuOpen: boolean = false;
constructor(
@ -32,6 +33,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
ngOnInit(): void {
this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
this.filterMode = active.mode;
this.gradientMode = active.gradient;
for (const key of Object.keys(this.filterFlags)) {
this.filterFlags[key] = false;
}
@ -39,7 +41,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
this.filterFlags[key] = !this.disabledFilters[key];
}
this.activeFilters = [...active.filters.filter(key => !this.disabledFilters[key])];
this.onFilterChanged.emit({ mode: active.mode, filters: this.activeFilters });
this.onFilterChanged.emit({ mode: active.mode, filters: this.activeFilters, gradient: this.gradientMode });
});
}
@ -57,8 +59,14 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
setFilterMode(mode): void {
this.filterMode = mode;
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters });
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] });
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters, gradient: this.gradientMode });
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters], gradient: this.gradientMode });
}
setGradientMode(mode): void {
this.gradientMode = mode;
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters, gradient: this.gradientMode });
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters], gradient: this.gradientMode });
}
toggleFilter(key): void {
@ -81,8 +89,8 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
this.activeFilters = this.activeFilters.filter(f => f != key);
}
const booleanFlags = this.getBooleanFlags();
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters });
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] });
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters, gradient: this.gradientMode });
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters], gradient: this.gradientMode });
}
getBooleanFlags(): bigint | null {

View File

@ -9,14 +9,11 @@ import { Price } from '../../services/price.service';
import { StateService } from '../../services/state.service';
import { ThemeService } from 'src/app/services/theme.service';
import { Subscription } from 'rxjs';
import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors, contrastFeeColors, contrastAuditFeeColors, contrastMarginalFeeColors, contrastAuditColors, contrastColorFunction } from './utils';
import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils';
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
import { detectWebGL } from '../../shared/graphs.utils';
const unmatchedOpacity = 0.2;
const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity));
const unmatchedAuditFeeColors = defaultAuditFeeColors.map(c => setOpacity(c, unmatchedOpacity));
const unmatchedMarginalFeeColors = defaultMarginalFeeColors.map(c => setOpacity(c, unmatchedOpacity));
const unmatchedAuditColors = {
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
@ -57,6 +54,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null;
@Input() filterMode: FilterMode = 'and';
@Input() gradientMode: 'fee' | 'age' = 'fee';
@Input() relativeTime: number | null;
@Input() blockConversion: Price;
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
@ -137,21 +135,22 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.setHighlightingEnabled(this.auditHighlighting);
}
if (changes.overrideColor && this.scene) {
this.scene.setColorFunction(this.overrideColors);
this.scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
}
if ((changes.filterFlags || changes.showFilters || changes.filterMode)) {
if ((changes.filterFlags || changes.showFilters || changes.filterMode || changes.gradientMode)) {
this.setFilterFlags();
}
}
setFilterFlags(goggle?: ActiveFilter): void {
this.filterMode = goggle?.mode || this.filterMode;
this.gradientMode = goggle?.gradient || this.gradientMode;
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
if (this.scene) {
if (this.activeFilterFlags != null && this.filtersAvailable) {
this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags));
this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode));
} else {
this.scene.setColorFunction(this.overrideColors);
this.scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
}
}
this.start();
@ -229,6 +228,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
remove = remove.filter(txid => this.scene.txs[txid]);
change = change.filter(tx => this.scene.txs[tx.txid]);
if (this.gradientMode === 'age') {
this.scene.updateAllColors();
}
this.scene.update(add, remove, change, direction, resetLayout);
this.start();
this.updateSearchHighlight();
@ -564,32 +566,27 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
getColorFunction(): ((tx: TxView) => Color) {
if (this.filterFlags) {
return this.getFilterColorFunction(this.filterFlags);
} else if (this.activeFilterFlags) {
return this.getFilterColorFunction(this.activeFilterFlags);
} else {
if (this.overrideColors) {
return this.overrideColors;
} else if (this.filterFlags) {
return this.getFilterColorFunction(this.filterFlags, this.gradientMode);
} else if (this.activeFilterFlags) {
return this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode);
} else {
return this.getFilterColorFunction(0n, this.gradientMode);
}
}
getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) {
getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) {
return (tx: TxView) => {
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
return this.themeService.theme !== 'default' ? contrastColorFunction(tx) : defaultColorFunction(tx);
return defaultColorFunction(tx);
} else {
return this.themeService.theme !== 'default' ? contrastColorFunction(
return defaultColorFunction(
tx,
unmatchedContrastFeeColors,
unmatchedContrastAuditFeeColors,
unmatchedContrastMarginalFeeColors,
unmatchedContrastAuditColors
) : defaultColorFunction(
tx,
unmatchedFeeColors,
unmatchedAuditFeeColors,
unmatchedMarginalFeeColors,
unmatchedAuditColors
defaultColors.unmatchedfee,
unmatchedAuditColors,
this.relativeTime || (Date.now() / 1000)
);
}
};

View File

@ -69,7 +69,7 @@ export default class BlockScene {
}
setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void {
this.theme.theme !== 'default' ? this.getColor = colorFunction || contrastColorFunction : this.getColor = colorFunction || defaultColorFunction;
this.getColor = colorFunction || defaultColorFunction;
this.dirty = true;
if (this.initialised && this.scene) {
this.updateColors(performance.now(), 50);

View File

@ -37,8 +37,15 @@ export function setOpacity(color: Color, opacity: number): Color {
};
}
interface ColorPalette {
base: Color[],
audit: Color[],
marginal: Color[],
baseLevel: (tx: TxView, rate: number, time: number) => number,
}
// precomputed colors
export const defaultFeeColors = defaultMempoolFeeColors.map(hexToColor);
export const defaultFeeColors = mempoolFeeColors.map(hexToColor);
export const defaultAuditFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
export const defaultMarginalFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.8), 1.1));
export const defaultAuditColors = {
@ -62,22 +69,21 @@ export const contrastAuditColors = {
export function defaultColorFunction(
tx: TxView,
feeColors: Color[] = defaultFeeColors,
auditFeeColors: Color[] = defaultAuditFeeColors,
marginalFeeColors: Color[] = defaultMarginalFeeColors,
auditColors: { [status: string]: Color } = defaultAuditColors
colors: { base: Color[], audit: Color[], marginal: Color[], baseLevel: (tx: TxView, rate: number, time: number) => number } = defaultColors.fee,
auditColors: { [status: string]: Color } = defaultAuditColors,
relativeTime?: number,
): Color {
const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[defaultMempoolFeeColors.length - 1];
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
// Normal mode
if (!tx.scene?.highlightingEnabled) {
if (tx.acc) {
return auditColors.accelerated;
} else {
return feeLevelColor;
return levelColor;
}
return feeLevelColor;
return levelColor;
}
// Block audit
switch(tx.status) {
@ -86,7 +92,7 @@ export function defaultColorFunction(
case 'missing':
case 'sigop':
case 'rbf':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[defaultMempoolFeeColors.length - 1];
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'fresh':
case 'freshcpfp':
return auditColors.missing;
@ -95,24 +101,45 @@ export function defaultColorFunction(
case 'prioritized':
return auditColors.prioritized;
case 'selected':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[defaultMempoolFeeColors.length - 1];
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'accelerated':
return auditColors.accelerated;
case 'found':
if (tx.context === 'projected') {
return auditFeeColors[feeLevelIndex] || auditFeeColors[defaultMempoolFeeColors.length - 1];
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
} else {
return feeLevelColor;
return levelColor;
}
default:
if (tx.acc) {
return auditColors.accelerated;
} else {
return feeLevelColor;
return levelColor;
}
}
}
export function ageColorFunction(
tx: TxView,
colors: { base: Color[], audit: Color[], marginal: Color[], baseLevel: (tx: TxView, rate: number, time: number) => number } = defaultColors.fee,
auditColors: { [status: string]: Color } = defaultAuditColors,
relativeTime?: number,
): Color {
if (tx.acc || tx.status === 'accelerated') {
return auditColors.accelerated;
}
const color = defaultColorFunction(tx, colors, auditColors, relativeTime);
const ageLevel = (!tx.time ? 0 : (0.8 * Math.tanh((1 / 15) * Math.log2((Math.max(1, 0.6 * ((relativeTime - tx.time) - 60)))))));
return {
r: color.r,
g: color.g,
b: color.b,
a: color.a * (1 - ageLevel)
};
}
export function contrastColorFunction(
tx: TxView,
feeColors: Color[] = contrastFeeColors,

View File

@ -53,13 +53,13 @@
<td i18n="block.miner">Miner</td>
<td *ngIf="stateService.env.MINING_DASHBOARD">
<a [attr.data-cy]="'block-details-miner-badge'" placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block?.extras.pool.slug]" class="badge"
[class]="!block?.extras.pool.name || block?.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
[class]="!block?.extras.pool.name || block?.extras.pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block?.extras.pool.name }}
</a>
</td>
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
<span [attr.data-cy]="'block-details-miner-badge'" placement="bottom" class="badge"
[class]="!block?.extras.pool.name || block?.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
[class]="!block?.extras.pool.name || block?.extras.pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block?.extras.pool.name }}
</span>
</td>

View File

@ -182,13 +182,13 @@
<td i18n="block.miner">Miner</td>
<td *ngIf="stateService.env.MINING_DASHBOARD">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
[class]="block.extras.pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block.extras.pool.name }}
</a>
</td>
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
<span placement="bottom" class="badge"
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
[class]="block.extras.pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block.extras.pool.name }}
</span>
</td>

View File

@ -7,7 +7,6 @@ import { EnterpriseService } from '../../services/enterprise.service';
import { NavigationService } from '../../services/navigation.service';
import { MenuComponent } from '../menu/menu.component';
import { StorageService } from '../../services/storage.service';
import { ApiService } from '../../services/api.service';
@Component({
selector: 'app-master-page',
@ -45,7 +44,6 @@ export class MasterPageComponent implements OnInit, OnDestroy {
private enterpriseService: EnterpriseService,
private navigationService: NavigationService,
private storageService: StorageService,
private apiService: ApiService,
private router: Router,
) { }

View File

@ -8,6 +8,7 @@
[showFilters]="showFilters"
[filterFlags]="filterFlags"
[filterMode]="filterMode"
[gradientMode]="gradientMode"
[excludeFilters]="['nonstandard']"
[overrideColors]="overrideColors"
(txClickEvent)="onTxClick($event)"

View File

@ -11,7 +11,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
import { Router } from '@angular/router';
import { Color } from '../block-overview-graph/sprite-types';
import TxView from '../block-overview-graph/tx-view';
import { FilterMode } from '../../shared/filters.utils';
import { FilterMode, GradientMode } from '../../shared/filters.utils';
@Component({
selector: 'app-mempool-block-overview',
@ -25,6 +25,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Input() filterFlags: bigint | undefined = undefined;
@Input() filterMode: FilterMode = 'and';
@Input() gradientMode: GradientMode = 'fee';
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;

View File

@ -5,8 +5,7 @@
height: calc(100vh - 65px);
position: absolute;
top: 65px;
left: -250px;
transition: left 0.25s;
transition: 0.25s;
box-shadow: 5px 0px 30px 0px #000;
padding-bottom: 20px;
@media (max-width: 613px) {
@ -14,6 +13,14 @@
}
}
:host-context(.ltr-layout) .sidenav {
left: -250px;
}
:host-context(.rtl-layout) .sidenav {
right: -250px;
}
.ellipsis {
display: block;
overflow: hidden;
@ -26,11 +33,18 @@
}
.sidenav.open {
left: 0px;
display: block;
background-color: var(--bg);
}
:host-context(.ltr-layout) .sidenav.open {
left: 0;
}
:host-context(.rtl-layout) .sidenav.open {
right: 0;
}
.sidenav a, button{
text-decoration: none;
color: lightgray;

View File

@ -175,13 +175,15 @@ export class PoolRankingComponent implements OnInit {
} as PieSeriesOption);
});
const percentage = totalShareOther.toFixed(2) + '%';
// 'Other'
data.push({
itemStyle: {
color: '#6b6b6b',
},
value: totalShareOther,
name: 'Other' + (isMobile() ? `` : ` (${totalShareOther.toFixed(2)}%)`),
name: $localize`Other (${percentage})`,
label: {
overflow: 'none',
color: 'var(--tooltip-grey)',
@ -197,7 +199,6 @@ export class PoolRankingComponent implements OnInit {
},
borderColor: '#000',
formatter: () => {
const percentage = totalShareOther.toFixed(2) + '%';
const i = totalBlockOther.toString();
if (this.miningWindowPreference === '24h') {
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +

View File

@ -40,70 +40,45 @@
</div>
<div class="clearfix"></div>
<ng-template [ngIf]="!isLoadingTx && !error">
<ng-template [ngIf]="tx?.status?.confirmed" [ngIfElse]="unconfirmedTemplate">
@if (!error) {
<div class="box">
<div class="row">
@if (isMobile) {
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
<ng-container *ngTemplateOutlet="detailsLeft"></ng-container>
<ng-container *ngTemplateOutlet="detailsRight"></ng-container>
</tbody>
</table>
</div>
</td>
</tr>
<ng-template [ngIf]="transactionTime > 0">
<tr>
<td i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</td>
<td><app-time kind="span" [time]="tx.status.block_time - transactionTime" [fastRender]="true" [relative]="true"></app-time></td>
</tr>
</ng-template>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet' && featuresEnabled">
<td class="td-width" i18n="transaction.features|Transaction features">Features</td>
<td>
<app-tx-features [tx]="tx"></app-tx-features>
</td>
</tr>
<tr *ngIf="network === '' && auditStatus">
<td class="td-width" i18n="block.toggle-audit|Toggle Audit">Audit</td>
<td *ngIf="pool" class="wrap-cell">
<ng-container>
<span *ngIf="auditStatus.coinbase; else expected" class="badge badge-primary mr-1" i18n="transactions-list.coinbase">Coinbase</span>
<ng-template #expected><span *ngIf="auditStatus.expected; else seen" class="badge badge-success mr-1" i18n-ngbTooltip="Expected in block tooltip" ngbTooltip="This transaction was projected to be included in the block" placement="bottom" i18n="tx-features.tag.expected|Expected in Block">Expected in Block</span></ng-template>
<ng-template #seen><span *ngIf="auditStatus.seen; else notSeen" class="badge badge-success mr-1" i18n-ngbTooltip="Seen in mempool tooltip" ngbTooltip="This transaction was seen in the mempool prior to mining" placement="bottom" i18n="tx-features.tag.seen|Seen in Mempool">Seen in Mempool</span></ng-template>
<ng-template #notSeen><span *ngIf="!auditStatus.conflict" class="badge badge-warning mr-1" i18n-ngbTooltip="Not seen in mempool tooltip" ngbTooltip="This transaction was missing from our mempool prior to mining" placement="bottom" i18n="tx-features.tag.not-seen|Not seen in Mempool">Not seen in Mempool</span></ng-template>
<span *ngIf="auditStatus.added" class="badge badge-warning mr-1" i18n-ngbTooltip="Added transaction tooltip" ngbTooltip="This transaction may have been added out-of-band" placement="bottom" i18n="tx-features.tag.added|Added">Added</span>
<span *ngIf="auditStatus.prioritized" class="badge badge-warning mr-1" i18n-ngbTooltip="Prioritized transaction tooltip" ngbTooltip="This transaction may have been prioritized out-of-band" placement="bottom" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
<span *ngIf="auditStatus.conflict" class="badge badge-warning mr-1" i18n-ngbTooltip="Conflict in mempool tooltip" ngbTooltip="This transaction conflicted with another version in our mempool" placement="bottom" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
</ng-container>
</td>
<td *ngIf="!pool">
<span class="skeleton-loader"></span>
</td>
</tr>
<ng-container *ngTemplateOutlet="goggles"></ng-container>
} @else {
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<ng-container *ngTemplateOutlet="detailsLeft"></ng-container>
</tbody>
</table>
</div>
<div class="col-sm">
<ng-container *ngTemplateOutlet="feeTable"></ng-container>
</div>
<table class="table table-borderless table-striped">
<tbody>
<ng-container *ngTemplateOutlet="detailsRight"></ng-container>
</tbody>
</table>
</div>
}
</div>
</div>
}
</ng-template>
<ng-template [ngIf]="!isLoadingTx && !error">
<!-- Accelerator -->
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary">
<br>
<div class="title float-left">
<h2>Accelerate</h2>
<h2 i18n="transaction.accelerate|Accelerate button label">Accelerate</h2>
</div>
<button type="button" class="btn btn-outline-info accelerator-toggle btn-sm float-right" (click)="showAccelerationSummary = false" i18n="hide-accelerator">Hide accelerator</button>
<div class="clearfix"></div>
@ -113,69 +88,6 @@
</div>
</ng-container>
<ng-template #unconfirmedTemplate>
<div class="box">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<ng-template [ngIf]="transactionTime !== 0">
<tr *ngIf="transactionTime === -1; else firstSeenTmpl">
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<ng-template #firstSeenTmpl>
<tr>
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
<td><i><app-time kind="since" [time]="transactionTime" [fastRender]="true"></app-time></i></td>
</tr>
</ng-template>
</ng-template>
<tr *ngIf="!replaced && !isCached">
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
<td>
<ng-template [ngIf]="this.mempoolPosition?.block == null" [ngIfElse]="estimationTmpl">
<span class="skeleton-loader"></span>
</ng-template>
<ng-template #estimationTmpl>
<ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit">
<span [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
<a *ngIf="!tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
</span>
</ng-template>
<ng-template #belowBlockLimit>
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeEstimateDefault">
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template>
<ng-template #timeEstimateDefault>
<span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''">
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.adjustedTimeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
<a *ngIf="!tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
</span>
</ng-template>
</ng-template>
</ng-template>
</td>
</tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" id="acceleratePreviewAnchor">
<td class="td-width" i18n="transaction.features|Transaction Features">Features</td>
<td>
<app-tx-features [tx]="tx"></app-tx-features>
</td>
</tr>
<ng-container *ngTemplateOutlet="goggles"></ng-container>
</tbody>
</table>
</div>
<div class="col-sm">
<ng-container *ngTemplateOutlet="feeTable"></ng-container>
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="showCpfpDetails">
<br>
@ -364,42 +276,7 @@
</ng-template>
<ng-template [ngIf]="(isLoadingTx && !error) || loadingCachedTx">
<div class="box">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<ng-container *ngIf="flowEnabled">
<div class="title">
<h2 i18n="transaction.flow|Transaction flow">Flow</h2>
@ -525,65 +402,264 @@
</div>
<ng-template #feeTable>
<table class="table table-borderless table-striped">
<tbody>
<tr *ngIf="isMobile && (network === 'liquid' || network === 'liquidtestnet' || !featuresEnabled || network === '')"></tr>
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span></td>
</tr>
<tr>
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
<td>
<app-fee-rate [fee]="tx.feePerVsize"></app-fee-rate>
<ng-template [ngIf]="tx?.status?.confirmed">
&nbsp;
<app-tx-fee-rating *ngIf="tx.fee && !hasEffectiveFeeRate && !accelerationInfo" [tx]="tx"></app-tx-fee-rating>
<ng-template #detailsLeft>
@if (tx?.status?.confirmed) {
<ng-container *ngTemplateOutlet="timestampRow"></ng-container>
<ng-container *ngTemplateOutlet="confirmedAfterRow"></ng-container>
} @else {
<ng-container *ngTemplateOutlet="firstSeenRow"></ng-container>
<ng-container *ngTemplateOutlet="etaRow"></ng-container>
}
<ng-container *ngTemplateOutlet="featuresRow"></ng-container>
@if (tx?.status?.confirmed) {
<ng-container *ngTemplateOutlet="auditRow"></ng-container>
}
<ng-container *ngTemplateOutlet="gogglesRow"></ng-container>
</ng-template>
</td>
</tr>
<tr *ngIf="(cpfpInfo && hasEffectiveFeeRate) || accelerationInfo">
<td *ngIf="tx.acceleration || accelerationInfo" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
<td *ngIf="!(tx.acceleration || accelerationInfo)" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td>
<div class="effective-fee-container">
<app-fee-rate *ngIf="accelerationInfo" [fee]="accelerationInfo.acceleratedFee" [weight]="accelerationInfo.effectiveVsize * 4"></app-fee-rate>
<app-fee-rate *ngIf="!accelerationInfo" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
<ng-template [ngIf]="tx?.status?.confirmed || tx.acceleration || accelerationInfo">
<app-tx-fee-rating *ngIf="!(tx.acceleration || accelerationInfo) && (tx.fee || tx.effectiveFeePerVsize)" class="ml-2 mr-2 effective-fee-rating" [tx]="tx"></app-tx-fee-rating>
<ng-template #detailsRight>
<ng-container *ngTemplateOutlet="feeRow"></ng-container>
<ng-container *ngTemplateOutlet="feeRateRow"></ng-container>
<ng-container *ngTemplateOutlet="effectiveRateRow"></ng-container>
@if (tx?.status?.confirmed) {
<ng-container *ngTemplateOutlet="minerRow"></ng-container>
}
</ng-template>
<ng-template #timestampRow>
@if (!isLoadingTx) {
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
</div>
<button *ngIf="cpfpInfo?.bestDescendant || cpfpInfo?.descendants?.length || cpfpInfo?.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
</td>
</tr>
<tr *ngIf="network === '' && tx?.status?.confirmed">
<td class="td-width" i18n="block.miner">Miner</td>
<td *ngIf="pool" class="wrap-cell">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge mr-1"
[class]="pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
{{ pool.name }}
</a>
</td>
<td *ngIf="!pool">
<span class="skeleton-loader"></span>
</td>
</tr>
</tbody>
</table>
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
}
</ng-template>
<ng-template #goggles>
<tr *ngIf="((auditStatus && auditStatus.accelerated) || accelerationInfo || (tx && tx.acceleration)) || filters.length">
<ng-template #confirmedAfterRow>
@if (!isLoadingTx) {
@if (transactionTime > 0) {
<tr>
<td i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</td>
<td><app-time kind="span" [time]="tx.status.block_time - transactionTime" [fastRender]="true" [relative]="true"></app-time></td>
</tr>
}
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
}
</ng-template>
<ng-template #firstSeenRow>
@if (!isLoadingTx && transactionTime !== -1) {
<tr>
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
<td><i><app-time kind="since" [time]="transactionTime" [fastRender]="true"></app-time></i></td>
</tr>
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
}
</ng-template>
<ng-template #featuresRow>
@if (network !== 'liquid' && network !== 'liquidtestnet') {
@if (!isLoadingTx) {
@if (featuresEnabled) {
<tr>
<td class="td-width" i18n="transaction.features|Transaction features" id="acceleratePreviewAnchor">Features</td>
<td>
<app-tx-features [tx]="tx"></app-tx-features>
</td>
</tr>
}
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
}
}
</ng-template>
<ng-template #auditRow>
@if (network === '') {
@if (!isLoadingTx) {
@if (auditStatus) {
<tr>
<td class="td-width" i18n="block.toggle-audit|Toggle Audit">Audit</td>
<td class="wrap-cell">
<ng-container>
@if (auditStatus.coinbase) {
<span class="badge badge-primary mr-1" i18n="transactions-list.coinbase">Coinbase</span>
} @else if (auditStatus.expected) {
<span class="badge badge-success mr-1" i18n-ngbTooltip="Expected in block tooltip" ngbTooltip="This transaction was projected to be included in the block" placement="bottom" i18n="tx-features.tag.expected|Expected in Block">Expected in Block</span>
} @else if (auditStatus.seen) {
<span class="badge badge-success mr-1" i18n-ngbTooltip="Seen in mempool tooltip" ngbTooltip="This transaction was seen in the mempool prior to mining" placement="bottom" i18n="tx-features.tag.seen|Seen in Mempool">Seen in Mempool</span>
} @else if (!auditStatus.conflict) {
<span class="badge badge-warning mr-1" i18n-ngbTooltip="Not seen in mempool tooltip" ngbTooltip="This transaction was missing from our mempool prior to mining" placement="bottom" i18n="tx-features.tag.not-seen|Not seen in Mempool">Not seen in Mempool</span>
}
@if (auditStatus.added) {
<span class="badge badge-warning mr-1" i18n-ngbTooltip="Added transaction tooltip" ngbTooltip="This transaction may have been added out-of-band" placement="bottom" i18n="tx-features.tag.added|Added">Added</span>
}
@if (auditStatus.prioritized) {
<span class="badge badge-warning mr-1" i18n-ngbTooltip="Prioritized transaction tooltip" ngbTooltip="This transaction may have been prioritized out-of-band" placement="bottom" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
}
@if (auditStatus.conflict) {
<span class="badge badge-warning mr-1" i18n-ngbTooltip="Conflict in mempool tooltip" ngbTooltip="This transaction conflicted with another version in our mempool" placement="bottom" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
}
</ng-container>
</td>
</tr>
}
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
}
}
</ng-template>
<ng-template #etaRow>
@if (!isLoadingTx) {
@if (!replaced && !isCached) {
<tr>
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
<td>
@if (this.mempoolPosition?.block == null) {
<span class="skeleton-loader"></span>
} @else if (this.mempoolPosition.block >= 7) {
<span [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
@if (!tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) {
<a [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
}
</span>
} @else if (network === 'liquid' || network === 'liquidtestnet') {
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
} @else {
<span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''">
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.adjustedTimeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
@if (!tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) {
<a [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
}
</span>
}
</td>
</tr>
}
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
}
</ng-template>
<ng-template #gogglesRow>
@if (!isLoadingTx) {
@if (((auditStatus && auditStatus.accelerated) || accelerationInfo || (tx && tx.acceleration)) || filters.length) {
<tr>
<td class="td-width">
<span class="goggles-icon"><app-svg-images name="goggles" width="100%" height="100%"></app-svg-images></span>
</td>
<td class="wrap-cell">
<span *ngIf="((auditStatus && auditStatus.accelerated) || accelerationInfo || (tx && tx.acceleration))" class="badge badge-accelerated mr-1" i18n="transaction.audit.accelerated">Accelerated</span>
@if ((auditStatus && auditStatus.accelerated) || accelerationInfo || (tx && tx.acceleration)) {
<span class="badge badge-accelerated mr-1" i18n="transaction.audit.accelerated">Accelerated</span>
}
<ng-container *ngFor="let filter of filters;">
<span class="badge badge-primary filter-tag mr-1">{{ filter.label }}</span>
</ng-container>
</td>
</tr>
}
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
}
</ng-template>
<ng-template #feeRow>
@if (!isLoadingTx) {
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span></td>
</tr>
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
}
</ng-template>
<ng-template #feeRateRow>
@if (!isLoadingTx) {
<tr>
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
<td>
<app-fee-rate [fee]="tx.feePerVsize"></app-fee-rate>
@if (tx?.status?.confirmed && tx.fee && !hasEffectiveFeeRate && !accelerationInfo) {
&nbsp;
<app-tx-fee-rating [tx]="tx"></app-tx-fee-rating>
}
</td>
</tr>
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
}
</ng-template>
<ng-template #effectiveRateRow>
@if (!isLoadingTx) {
@if ((cpfpInfo && hasEffectiveFeeRate) || accelerationInfo) {
<tr>
@if (tx.acceleration || accelerationInfo) {
<td i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
} @else {
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
}
<td>
<div class="effective-fee-container">
@if (accelerationInfo) {
<app-fee-rate [fee]="accelerationInfo.acceleratedFee" [weight]="accelerationInfo.effectiveVsize * 4"></app-fee-rate>
} @else {
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
}
@if (tx?.status?.confirmed && !tx.acceleration && !accelerationInfo && tx.fee && tx.effectiveFeePerVsize) {
<app-tx-fee-rating class="ml-2 mr-2 effective-fee-rating" [tx]="tx"></app-tx-fee-rating>
}
</div>
@if (hasCpfp) {
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
}
</td>
</tr>
}
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
}
</ng-template>
<ng-template #minerRow>
@if (network === '') {
@if (!isLoadingTx) {
<tr>
<td class="td-width" i18n="block.miner">Miner</td>
@if (pool) {
<td class="wrap-cell">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge mr-1"
[class]="pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
{{ pool.name }}
</a>
</td>
} @else {
<td>
<span class="skeleton-loader"></span>
</td>
}
</tr>
} @else {
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
}
}
</ng-template>
<ng-template #skeletonDetailsRow>
<tr>
<td><span class="skeleton-loader"></span></td>
</tr>
</ng-template>

View File

@ -87,6 +87,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
rbfReplaces: string[];
rbfInfo: RbfTree;
cpfpInfo: CpfpInfo | null;
hasCpfp: boolean = false;
accelerationInfo: Acceleration | null = null;
sigops: number | null;
adjustedVsize: number | null;
@ -491,10 +492,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
txFeePerVSize: tx.effectiveFeePerVsize,
mempoolPosition: this.mempoolPosition,
});
this.cpfpInfo = {
this.setCpfpInfo({
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant,
};
});
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant);
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
} else {
@ -646,6 +647,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
setCpfpInfo(cpfpInfo: CpfpInfo): void {
if (!cpfpInfo || !this.tx) {
this.cpfpInfo = null;
this.hasCpfp = false;
this.hasEffectiveFeeRate = false;
return;
}
@ -675,6 +677,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.sigops = this.cpfpInfo.sigops;
this.adjustedVsize = this.cpfpInfo.adjustedVsize;
}
this.hasCpfp =!!(this.cpfpInfo && (this.cpfpInfo.bestDescendant || this.cpfpInfo.descendants?.length || this.cpfpInfo.ancestors?.length));
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
}

View File

@ -18,7 +18,7 @@
<div class="card graph-card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
<h5 class="card-title d-inline"><span i18n="dashboard.mempool-goggles">Mempool Goggles</span>: {{ goggleCycle[goggleIndex].name }}</h5>
<h5 class="card-title d-inline"><span>Mempool Goggles&trade;</span> : {{ goggleCycle[goggleIndex].name }}</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
@ -35,6 +35,7 @@
[resolution]="goggleResolution"
[filterFlags]="goggleFlags"
[filterMode]="goggleMode"
[gradientMode]="gradientMode"
></app-mempool-block-overview>
</div>
</div>

View File

@ -7,7 +7,7 @@ import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service';
import { WebsocketService } from '../services/websocket.service';
import { SeoService } from '../services/seo.service';
import { ActiveFilter, FilterMode, toFlags } from '../shared/filters.utils';
import { ActiveFilter, FilterMode, GradientMode, toFlags } from '../shared/filters.utils';
import { detectWebGL } from '../shared/graphs.utils';
interface MempoolBlocksData {
@ -74,14 +74,15 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
private lastReservesBlockUpdate: number = 0;
goggleResolution = 82;
goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[] }[] = [
{ index: 0, name: 'All', mode: 'and', filters: [] },
{ index: 1, name: 'Consolidation', mode: 'and', filters: ['consolidation'] },
{ index: 2, name: 'Coinjoin', mode: 'and', filters: ['coinjoin'] },
{ index: 3, name: 'Data', mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'] },
goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[], gradient: GradientMode }[] = [
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
];
goggleFlags = 0n;
goggleMode: FilterMode = 'and';
gradientMode: GradientMode = 'age';
goggleIndex = 0;
private destroy$ = new Subject();
@ -131,6 +132,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
this.goggleIndex = goggle.index;
this.goggleFlags = toFlags(goggle.filters);
this.goggleMode = goggle.mode;
this.gradientMode = active.gradient;
return;
}
}
@ -140,6 +142,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
name: 'Custom',
mode: active.mode,
filters: active.filters,
gradient: active.gradient,
});
this.goggleIndex = this.goggleCycle.length - 1;
this.goggleFlags = toFlags(active.filters);

View File

@ -9584,7 +9584,7 @@ export const faqData = [
category: "advanced",
showConditions: bitcoinNetworks,
fragment: "how-do-mempool-goggles-work",
title: "How do Mempool Goggles work?",
title: "How do Mempool Goggles work?",
},
{
type: "endpoint",

View File

@ -290,8 +290,8 @@
</ng-template>
<ng-template type="how-do-mempool-goggles-work">
<p>Mempool Goggles are a set of filters that can be applied to the <a [routerLink]="['/mempool-block/0' | relativeUrl]">mempool block visualizations</a> to highlight different types of transactions.</p>
<p>There are currently 25 different Mempool Goggles filters, grouped into six categories:</p>
<p>Mempool Goggles&trade; are a set of filters that can be applied to the <a [routerLink]="['/mempool-block/0' | relativeUrl]">mempool block visualizations</a> to highlight different types of transactions.</p>
<p>There are currently 25 different Mempool Goggles&trade; filters, grouped into six categories:</p>
<dl>
<dt>Features</dt>
<dd>
@ -418,5 +418,5 @@
</ng-template>
<ng-template type="address-lookup-issues">
<p>If you're getting errors when doing address lookups, it's probably because of your Electrum server backend.</p><p>Mempool uses an Electrum server to do address lookups. There are several implementations of the Electrum server protocol, and Mempool can use any of them, but the implementation you use affects performance:</p><ol><li><a href="https://github.com/romanz/electrs" target="_blank">romanz/electrs</a>. This is a common choice for its low resource requirements, and most full-node distros use it. But while this implementation works great for basic queries, it will struggle with heavier ones (e.g. looking up addresses with many transactions)—especially when running on low-power hardware like a Raspberry Pi.</li><li><a href="https://github.com/cculianu/Fulcrum" target="_blank">Fulcrum</a>. Fulcrum requires more resources than romanz/electrs but it can still run on a Raspberry Pi, and it handles heavy queries much more efficiently. If you're having issues with romanz/electrs, Fulcrum is worth a try.</li><li><a href="https://github.com/Blockstream/electrs" target="_blank">blockstream/electrs</a>. If you have stronger hardware, consider running Blockstream's electrs implementation. It's the backend mempool.space uses, and is also what powers blockstream.info.</li></ol>
<p>If you're getting errors when doing address lookups, it's probably because of your Electrum server backend.</p><p>Mempool uses an Electrum server to do address lookups. There are several implementations of the Electrum server protocol, and Mempool can use any of them, but the implementation you use affects performance:</p><ol><li><a href="https://github.com/romanz/electrs" target="_blank">romanz/electrs</a>. This is a common choice for its low resource requirements, and most full-node distros use it. But while this implementation works great for basic queries, it will struggle with heavier ones (e.g. looking up addresses with many transactions)—especially when running on low-power hardware like a Raspberry Pi.</li><li><a href="https://github.com/cculianu/Fulcrum" target="_blank">Fulcrum</a>. Fulcrum requires more resources than romanz/electrs but it can still run on a Raspberry Pi, and it handles heavy queries much more efficiently. If you're having issues with romanz/electrs, Fulcrum is worth a try.</li><li><a href="https://github.com/mempool/electrs" target="_blank">mempool/electrs</a>. If you have stronger hardware, you could consider running mempool/electrs, the backend that powers mempool.space. It's a fork of Blockstream's Esplora, which is in turn a fork of romanz/electrs, intended for maximum performance and larger-scale deployments.</li></ol>
</ng-template>

View File

@ -163,6 +163,7 @@ export interface PoolInfo {
emptyBlocks: number;
slug: string;
poolUniqueId: number;
unique_id: number;
}
export interface PoolStat {
pool: PoolInfo;

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo } from '../interfaces/node-api.interface';
import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs';
import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, take, tap } from 'rxjs';
import { StateService } from './state.service';
import { Transaction } from '../interfaces/electrs.interface';
import { Conversion } from './price.service';
@ -246,11 +246,29 @@ export class ApiService {
return this.httpClient.get<any>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools` +
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
)
.pipe(
map((response) => {
response.body.pools.forEach((pool) => {
if (pool.poolUniqueId === 0) {
pool.name = $localize`:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown`;
}
});
return response;
})
);
}
getPoolStats$(slug: string): Observable<PoolStat> {
return this.httpClient.get<PoolStat>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${slug}`);
return this.httpClient.get<PoolStat>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${slug}`)
.pipe(
map((poolStats) => {
if (poolStats.pool.unique_id === 0) {
poolStats.pool.name = $localize`:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown`;
}
return poolStats;
})
);
}
getPoolHashrate$(slug: string): Observable<any> {

View File

@ -132,6 +132,10 @@ export class ServicesApiServices {
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate`, { txInput: txInput, userBid: userBid });
}
accelerateWithCashApp$(txInput: string, userBid: number, token: string, cashtag: string, referenceId: string) {
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate/cashapp`, { txInput: txInput, userBid: userBid, token: token, cashtag: cashtag, referenceId: referenceId });
}
getAccelerations$(): Observable<Acceleration[]> {
return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations`);
}
@ -151,4 +155,8 @@ export class ServicesApiServices {
getAccelerationStats$(): Observable<AccelerationStats> {
return this.httpClient.get<AccelerationStats>(`${SERVICES_API_PREFIX}/accelerator/accelerations/stats`);
}
setupSquare$(): Observable<{squareAppId: string, squareLocationId: string}> {
return this.httpClient.get<{squareAppId: string, squareLocationId: string}>(`${SERVICES_API_PREFIX}/square/setup`);
}
}

View File

@ -86,6 +86,7 @@ const defaultEnv: Env = {
providedIn: 'root'
})
export class StateService {
ref: string = '';
isBrowser: boolean = isPlatformBrowser(this.platformId);
isMempoolSpaceBuild = window['isMempoolSpaceBuild'] ?? false;
backend: 'esplora' | 'electrum' | 'none' = 'esplora';
@ -154,7 +155,7 @@ export class StateService {
searchFocus$: Subject<boolean> = new Subject<boolean>();
menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false);
activeGoggles$: BehaviorSubject<ActiveFilter> = new BehaviorSubject({ mode: 'and', filters: [] });
activeGoggles$: BehaviorSubject<ActiveFilter> = new BehaviorSubject({ mode: 'and', filters: [], gradient: 'age' });
constructor(
@Inject(PLATFORM_ID) private platformId: any,
@ -162,6 +163,8 @@ export class StateService {
private router: Router,
private storageService: StorageService,
) {
this.ref = window.document.referrer;
const browserWindow = window || {};
// @ts-ignore
const browserWindowEnv = browserWindow.__env || {};

View File

@ -11,9 +11,12 @@ export interface Filter {
export type FilterMode = 'and' | 'or';
export type GradientMode = 'fee' | 'age';
export interface ActiveFilter {
mode: FilterMode,
filters: string[],
gradient: GradientMode,
}
// binary flags for transaction classification
@ -93,15 +96,15 @@ export const TransactionFilters: { [key: string]: Filter } = {
cpfp_parent: { key: 'cpfp_parent', label: 'Paid for by child', flag: TransactionFlags.cpfp_parent, important: true, tooltip: true, txPage: false, },
cpfp_child: { key: 'cpfp_child', label: 'Pays for parent', flag: TransactionFlags.cpfp_child, important: true, tooltip: true, txPage: false, },
replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement, important: true, tooltip: true, txPage: false, },
acceleration: window?.['__env']?.ACCELERATOR ? { key: 'acceleration', label: 'Accelerated', flag: TransactionFlags.acceleration, important: false } : undefined,
acceleration: window?.['__env']?.ACCELERATOR ? { key: 'acceleration', label: $localize`:@@b484583f0ce10f3341ab36750d05271d9d22c9a1:Accelerated`, flag: TransactionFlags.acceleration, important: false } : undefined,
/* data */
op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return, important: true, tooltip: true, txPage: true, },
fake_pubkey: { key: 'fake_pubkey', label: 'Fake pubkey', flag: TransactionFlags.fake_pubkey, tooltip: true, txPage: true, },
inscription: { key: 'inscription', label: 'Inscription', flag: TransactionFlags.inscription, important: true, tooltip: true, txPage: true, },
fake_scripthash: { key: 'fake_scripthash', label: 'Fake scripthash', flag: TransactionFlags.fake_scripthash, tooltip: true, txPage: true,},
/* heuristics */
coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin, important: true, tooltip: true, txPage: true, },
consolidation: { key: 'consolidation', label: 'Consolidation', flag: TransactionFlags.consolidation, tooltip: true, txPage: true, },
coinjoin: { key: 'coinjoin', label: $localize`Coinjoin`, flag: TransactionFlags.coinjoin, important: true, tooltip: true, txPage: true, },
consolidation: { key: 'consolidation', label: $localize`Consolidation`, flag: TransactionFlags.consolidation, tooltip: true, txPage: true, },
batch_payout: { key: 'batch_payout', label: 'Batch payment', flag: TransactionFlags.batch_payout, tooltip: true, txPage: true, },
/* sighash */
sighash_all: { key: 'sighash_all', label: 'sighash_all', flag: TransactionFlags.sighash_all },
@ -112,10 +115,10 @@ export const TransactionFilters: { [key: string]: Filter } = {
};
export const FilterGroups: { label: string, filters: Filter[]}[] = [
{ label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'v3', 'nonstandard'] },
{ label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] },
{ label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement', 'acceleration'] },
{ label: 'Data', filters: ['op_return', 'fake_pubkey', 'fake_scripthash', 'inscription'] },
{ label: 'Heuristics', filters: ['coinjoin', 'consolidation', 'batch_payout'] },
{ label: 'Sighash Flags', filters: ['sighash_all', 'sighash_none', 'sighash_single', 'sighash_default', 'sighash_acp'] },
{ label: $localize`:@@885666551418fd59011ceb09d5c481095940193b:Features`, filters: ['rbf', 'no_rbf', 'v1', 'v2', 'v3', 'nonstandard'] },
{ label: $localize`Address Types`, filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] },
{ label: $localize`Behavior`, filters: ['cpfp_parent', 'cpfp_child', 'replacement', 'acceleration'] },
{ label: $localize`Data`, filters: ['op_return', 'fake_pubkey', 'fake_scripthash', 'inscription'] },
{ label: $localize`Heuristics`, filters: ['coinjoin', 'consolidation', 'batch_payout'] },
{ label: $localize`Sighash Flags`, filters: ['sighash_all', 'sighash_none', 'sighash_single', 'sighash_default', 'sighash_acp'] },
].map(group => ({ label: group.label, filters: group.filters.map(filter => TransactionFilters[filter] || null).filter(f => f != null) }));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,15 +7,15 @@
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "ES2022",
"module": "ES2020",
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2022",
"target": "ES2020",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"ES2022",
"ES2020",
"dom",
"dom.iterable"
]

View File

@ -2,7 +2,7 @@
"extends": "./tsconfig.app.json",
"compilerOptions": {
"outDir": "./out-tsc/server",
"target": "ES2022",
"target": "ES2020",
"sourceMap": true,
"types": [
"node"