Merge branch 'master' into natsoni/contrast-theme
This commit is contained in:
commit
c251b5831b
@ -103,7 +103,7 @@ In particular, make sure:
|
|||||||
- the correct Bitcoin Core RPC credentials are specified in `CORE_RPC`
|
- the correct Bitcoin Core RPC credentials are specified in `CORE_RPC`
|
||||||
- the correct `BACKEND` is specified in `MEMPOOL`:
|
- 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)
|
- "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
|
- "none" if you're not using any Electrum Server
|
||||||
|
|
||||||
### 6. Run Mempool Backend
|
### 6. Run Mempool Backend
|
||||||
|
75
backend/src/api/acceleration/acceleration.routes.ts
Normal file
75
backend/src/api/acceleration/acceleration.routes.ts
Normal 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();
|
@ -1,6 +1,6 @@
|
|||||||
import logger from '../logger';
|
import logger from '../../logger';
|
||||||
import { MempoolTransactionExtended } from '../mempool.interfaces';
|
import { MempoolTransactionExtended } from '../../mempool.interfaces';
|
||||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||||
|
|
||||||
const BLOCK_WEIGHT_UNITS = 4_000_000;
|
const BLOCK_WEIGHT_UNITS = 4_000_000;
|
||||||
const BLOCK_SIGOPS = 80_000;
|
const BLOCK_SIGOPS = 80_000;
|
@ -373,6 +373,21 @@ export class Common {
|
|||||||
].includes(pubkey);
|
].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 {
|
static getTransactionFlags(tx: TransactionExtended): number {
|
||||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||||
|
|
||||||
@ -409,30 +424,31 @@ export class Common {
|
|||||||
if (vin.sequence < 0xfffffffe) {
|
if (vin.sequence < 0xfffffffe) {
|
||||||
rbf = true;
|
rbf = true;
|
||||||
}
|
}
|
||||||
switch (vin.prevout?.scriptpubkey_type) {
|
if (vin.prevout?.scriptpubkey_type) {
|
||||||
case 'p2pk': flags |= TransactionFlags.p2pk; break;
|
switch (vin.prevout?.scriptpubkey_type) {
|
||||||
case 'multisig': flags |= TransactionFlags.p2ms; break;
|
case 'p2pk': flags |= TransactionFlags.p2pk; break;
|
||||||
case 'p2pkh': flags |= TransactionFlags.p2pkh; break;
|
case 'multisig': flags |= TransactionFlags.p2ms; break;
|
||||||
case 'p2sh': flags |= TransactionFlags.p2sh; break;
|
case 'p2pkh': flags |= TransactionFlags.p2pkh; break;
|
||||||
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
|
case 'p2sh': flags |= TransactionFlags.p2sh; break;
|
||||||
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
|
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
|
||||||
case 'v1_p2tr': {
|
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
|
||||||
if (!vin.witness?.length) {
|
case 'v1_p2tr': {
|
||||||
throw new Error('Taproot input missing witness data');
|
if (!vin.witness?.length) {
|
||||||
}
|
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 |= TransactionFlags.p2tr;
|
||||||
|
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
|
||||||
}
|
}
|
||||||
} break;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sighash flags
|
// sighash flags
|
||||||
|
@ -655,6 +655,7 @@ class DatabaseMigration {
|
|||||||
|
|
||||||
await this.$executeQuery('TRUNCATE hashrates');
|
await this.$executeQuery('TRUNCATE hashrates');
|
||||||
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
||||||
|
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||||
|
|
||||||
await this.updateToSchemaVersion(75);
|
await this.updateToSchemaVersion(75);
|
||||||
}
|
}
|
||||||
|
@ -343,7 +343,7 @@ class MempoolBlocks {
|
|||||||
if (txid in mempool) {
|
if (txid in mempool) {
|
||||||
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
|
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
|
||||||
mempool[txid].effectiveFeePerVsize = rate;
|
mempool[txid].effectiveFeePerVsize = rate;
|
||||||
mempool[txid].cpfpChecked = false;
|
mempool[txid].cpfpChecked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -739,7 +739,7 @@ class WebsocketHandler {
|
|||||||
accelerated: mempoolTx.acceleration || undefined,
|
accelerated: mempoolTx.acceleration || undefined,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (!mempoolTx.cpfpChecked) {
|
if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) {
|
||||||
calculateCpfp(mempoolTx, newMempool);
|
calculateCpfp(mempoolTx, newMempool);
|
||||||
}
|
}
|
||||||
if (mempoolTx.cpfpDirty) {
|
if (mempoolTx.cpfpDirty) {
|
||||||
|
@ -43,6 +43,7 @@ import redisCache from './api/redis-cache';
|
|||||||
import accelerationApi from './api/services/acceleration';
|
import accelerationApi from './api/services/acceleration';
|
||||||
import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
|
import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
|
||||||
import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
|
import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
|
||||||
|
import accelerationRoutes from './api/acceleration/acceleration.routes';
|
||||||
import aboutRoutes from './api/about.routes';
|
import aboutRoutes from './api/about.routes';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
@ -306,6 +307,9 @@ class Server {
|
|||||||
nodesRoutes.initRoutes(this.app);
|
nodesRoutes.initRoutes(this.app);
|
||||||
channelsRoutes.initRoutes(this.app);
|
channelsRoutes.initRoutes(this.app);
|
||||||
}
|
}
|
||||||
|
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||||
|
accelerationRoutes.initRoutes(this.app);
|
||||||
|
}
|
||||||
aboutRoutes.initRoutes(this.app);
|
aboutRoutes.initRoutes(this.app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration';
|
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration/acceleration';
|
||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
@ -7,7 +7,7 @@ import { Common } from '../api/common';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import blocks from '../api/blocks';
|
import blocks from '../api/blocks';
|
||||||
import accelerationApi, { Acceleration } from '../api/services/acceleration';
|
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 bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||||
import transactionUtils from '../api/transaction-utils';
|
import transactionUtils from '../api/transaction-utils';
|
||||||
import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces';
|
import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces';
|
||||||
|
3
contributors/daweilv.txt
Normal file
3
contributors/daweilv.txt
Normal 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
|
@ -32,7 +32,7 @@
|
|||||||
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
|
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
<ng-container *ngIf="officialMempoolSpace">
|
<ng-container>
|
||||||
<app-about-sponsors></app-about-sponsors>
|
<app-about-sponsors></app-about-sponsors>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
@ -181,7 +181,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="officialMempoolSpace">
|
<ng-container>
|
||||||
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
|
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
|
||||||
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
|
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
|
||||||
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
|
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
|
||||||
|
@ -52,7 +52,7 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
|||||||
rate: option.rate,
|
rate: option.rate,
|
||||||
style: this.getStyle(option.rate, maxRate, baseHeight),
|
style: this.getStyle(option.rate, maxRate, baseHeight),
|
||||||
class: 'max',
|
class: 'max',
|
||||||
label: 'maximum',
|
label: $localize`maximum`,
|
||||||
active: option.index === this.maxRateIndex,
|
active: option.index === this.maxRateIndex,
|
||||||
rateIndex: option.index,
|
rateIndex: option.index,
|
||||||
fee: option.fee,
|
fee: option.fee,
|
||||||
@ -63,7 +63,7 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
|||||||
rate: this.estimate.targetFeeRate,
|
rate: this.estimate.targetFeeRate,
|
||||||
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||||
class: 'target',
|
class: 'target',
|
||||||
label: 'next block',
|
label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(),
|
||||||
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -32,18 +32,16 @@
|
|||||||
<div class="alert alert-mempool">You are currently on the waitlist</div>
|
<div class="alert alert-mempool">You are currently on the waitlist</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h5>Your transaction</h5>
|
<h5 i18n="accelerator.your-transaction">Your transaction</h5>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<small *ngIf="hasAncestors" class="form-text text-muted mb-2">
|
<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>
|
</small>
|
||||||
<table class="table table-borderless table-border table-dark table-accelerator">
|
<table class="table table-borderless table-border table-dark table-accelerator">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="group-first">
|
<tr class="group-first">
|
||||||
<td class="item">
|
<td class="item" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||||
Virtual size
|
|
||||||
</td>
|
|
||||||
<td style="text-align: end;" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
|
<td style="text-align: end;" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="info">
|
<tr class="info">
|
||||||
@ -52,9 +50,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="item">
|
<td class="item" i18n="accelerator.in-band-fees">In-band fees</td>
|
||||||
In-band fees
|
|
||||||
</td>
|
|
||||||
<td style="text-align: end;">
|
<td style="text-align: end;">
|
||||||
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span>
|
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||||
</td>
|
</td>
|
||||||
@ -69,13 +65,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<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="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<small class="form-text text-muted mb-2">
|
<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>
|
||||||
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>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="fee-card">
|
<div class="fee-card">
|
||||||
<div class="d-flex mb-0">
|
<div class="d-flex mb-0">
|
||||||
@ -99,9 +92,7 @@
|
|||||||
<!-- ESTIMATED FEE -->
|
<!-- ESTIMATED FEE -->
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<tr class="group-first">
|
<tr class="group-first">
|
||||||
<td class="item">
|
<td class="item" i18n="accelerator.next-block-rate">Next block market rate</td>
|
||||||
Next block market rate
|
|
||||||
</td>
|
|
||||||
<td class="amt" style="font-size: 16px">
|
<td class="amt" style="font-size: 16px">
|
||||||
{{ estimate.targetFeeRate | number : '1.0-0' }}
|
{{ estimate.targetFeeRate | number : '1.0-0' }}
|
||||||
</td>
|
</td>
|
||||||
@ -109,7 +100,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr class="info">
|
<tr class="info">
|
||||||
<td 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>
|
||||||
<td class="amt">
|
<td class="amt">
|
||||||
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
|
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
|
||||||
@ -123,13 +114,11 @@
|
|||||||
|
|
||||||
<!-- MEMPOOL BASE FEE -->
|
<!-- MEMPOOL BASE FEE -->
|
||||||
<tr>
|
<tr>
|
||||||
<td class="item">
|
<td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator™ fees</td>
|
||||||
Mempool Accelerator™ fees
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="info">
|
<tr class="info">
|
||||||
<td 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>
|
||||||
<td class="amt">
|
<td class="amt">
|
||||||
+{{ estimate.mempoolBaseFee | number }}
|
+{{ estimate.mempoolBaseFee | number }}
|
||||||
@ -141,7 +130,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr class="info group-last">
|
<tr class="info group-last">
|
||||||
<td class="info">
|
<td class="info">
|
||||||
<i><small>Transaction Size Surcharge</small></i>
|
<i><small i18n="accelerator.tx-size-surcharge">Transaction Size Surcharge</small></i>
|
||||||
</td>
|
</td>
|
||||||
<td class="amt">
|
<td class="amt">
|
||||||
+{{ estimate.vsizeFee | number }}
|
+{{ estimate.vsizeFee | number }}
|
||||||
@ -156,7 +145,7 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
|
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
|
||||||
<td class="item">
|
<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>
|
||||||
<td class="amt">
|
<td class="amt">
|
||||||
<span style="background-color: #5E35B1" class="p-1 pl-0">
|
<span style="background-color: #5E35B1" class="p-1 pl-0">
|
||||||
@ -170,7 +159,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
|
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
|
||||||
<td class="info" colspan=3>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -179,7 +168,7 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
<tr class="group-first">
|
<tr class="group-first">
|
||||||
<td class="item">
|
<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>
|
||||||
<td class="amt">
|
<td class="amt">
|
||||||
<span style="background-color: var(--primary)" class="p-1 pl-0">
|
<span style="background-color: var(--primary)" class="p-1 pl-0">
|
||||||
@ -195,7 +184,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr class="info group-last">
|
<tr class="info group-last">
|
||||||
<td class="info" colspan=3>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -203,9 +192,7 @@
|
|||||||
<!-- USER BALANCE -->
|
<!-- USER BALANCE -->
|
||||||
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
|
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
|
||||||
<tr class="group-first group-last" style="border-top: 1px dashed grey">
|
<tr class="group-first group-last" style="border-top: 1px dashed grey">
|
||||||
<td class="item">
|
<td class="item" i18n="accelerator.available-balance">Available balance</td>
|
||||||
Available balance
|
|
||||||
</td>
|
|
||||||
<td class="amt">
|
<td class="amt">
|
||||||
{{ estimate.userBalance | number }}
|
{{ estimate.userBalance | number }}
|
||||||
</td>
|
</td>
|
||||||
@ -219,12 +206,12 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- LOGIN CTA -->
|
<!-- 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">
|
<tr class="group-first group-last" style="border-top: 1px dashed grey">
|
||||||
<td class="item"></td>
|
<td class="item"></td>
|
||||||
<td class="amt"></td>
|
<td class="amt"></td>
|
||||||
<td class="units d-flex">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -233,7 +220,7 @@
|
|||||||
<td class="item"></td>
|
<td class="item"></td>
|
||||||
<td class="amt"></td>
|
<td class="amt"></td>
|
||||||
<td class="units d-flex">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -242,13 +229,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-3" *ngIf="isLoggedIn()">
|
<div class="row mb-3" *ngIf="isLoggedIn() && paymentType === 'bitcoin'">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess">
|
<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>
|
||||||
</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>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -257,4 +253,6 @@
|
|||||||
<ng-template #loadingEstimate>
|
<ng-template #loadingEstimate>
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
<br>
|
<br>
|
||||||
</ng-template>
|
</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>
|
@ -1,5 +1,4 @@
|
|||||||
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core';
|
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 { Subscription, catchError, of, tap } from 'rxjs';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { Transaction } from '../../interfaces/electrs.interface';
|
import { Transaction } from '../../interfaces/electrs.interface';
|
||||||
@ -63,18 +62,44 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
|
|
||||||
maxRateOptions: RateOption[] = [];
|
maxRateOptions: RateOption[] = [];
|
||||||
|
|
||||||
|
// Cashapp payment
|
||||||
|
paymentType: 'bitcoin' | 'cashapp' = 'bitcoin';
|
||||||
|
cashAppSubscription: Subscription;
|
||||||
|
conversionsSubscription: Subscription;
|
||||||
|
payments: any;
|
||||||
|
showSpinner = false;
|
||||||
|
square: any;
|
||||||
|
cashAppPay: any;
|
||||||
|
hideCashApp = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private servicesApiService: ServicesApiServices,
|
private servicesApiService: ServicesApiServices,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private audioService: AudioService,
|
private audioService: AudioService,
|
||||||
private cd: ChangeDetectorRef
|
private cd: ChangeDetectorRef
|
||||||
) { }
|
) {
|
||||||
|
if (this.stateService.ref === 'https://cash.app/') {
|
||||||
|
this.insertSquare();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
if (this.estimateSubscription) {
|
if (this.estimateSubscription) {
|
||||||
this.estimateSubscription.unsubscribe();
|
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 {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
@ -83,69 +108,87 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngAfterViewInit() {
|
||||||
|
if (this.paymentType === 'cashapp') {
|
||||||
|
this.showSpinner = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.user = this.storageService.getAuth()?.user ?? null;
|
this.user = this.storageService.getAuth()?.user ?? null;
|
||||||
|
|
||||||
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
|
this.servicesApiService.setupSquare$().subscribe(ids => {
|
||||||
tap((response) => {
|
this.square = {
|
||||||
if (response.status === 204) {
|
appId: ids.squareAppId,
|
||||||
this.estimate = undefined;
|
locationId: ids.squareLocationId
|
||||||
this.error = `cannot_accelerate_tx`;
|
};
|
||||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
|
||||||
this.estimateSubscription.unsubscribe();
|
tap((response) => {
|
||||||
} else {
|
if (response.status === 204) {
|
||||||
this.estimate = response.body;
|
this.estimate = undefined;
|
||||||
if (!this.estimate) {
|
|
||||||
this.error = `cannot_accelerate_tx`;
|
this.error = `cannot_accelerate_tx`;
|
||||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||||
this.estimateSubscription.unsubscribe();
|
this.estimateSubscription.unsubscribe();
|
||||||
}
|
} else {
|
||||||
|
this.estimate = response.body;
|
||||||
if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) {
|
if (!this.estimate) {
|
||||||
if (this.isLoggedIn()) {
|
this.error = `cannot_accelerate_tx`;
|
||||||
this.error = `not_enough_balance`;
|
|
||||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||||
|
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`;
|
||||||
|
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
|
||||||
|
|
||||||
|
// Make min extra fee at least 50% of the current tx fee
|
||||||
|
this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
|
||||||
|
|
||||||
|
this.maxRateOptions = [1, 2, 4].map((multiplier, index) => {
|
||||||
|
return {
|
||||||
|
fee: this.minExtraCost * multiplier,
|
||||||
|
rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize,
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO;
|
||||||
|
this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO;
|
||||||
|
this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO;
|
||||||
|
|
||||||
|
this.userBid = this.defaultBid;
|
||||||
|
if (this.userBid < this.minBidAllowed) {
|
||||||
|
this.userBid = this.minBidAllowed;
|
||||||
|
} else if (this.userBid > this.maxBidAllowed) {
|
||||||
|
this.userBid = this.maxBidAllowed;
|
||||||
|
}
|
||||||
|
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
||||||
|
|
||||||
|
if (!this.error) {
|
||||||
|
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
||||||
|
if (this.paymentType === 'cashapp') {
|
||||||
|
this.setupSquare();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
|
catchError((response) => {
|
||||||
|
this.estimate = undefined;
|
||||||
// Make min extra fee at least 50% of the current tx fee
|
this.error = response.error;
|
||||||
this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
|
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||||
|
this.estimateSubscription.unsubscribe();
|
||||||
this.maxRateOptions = [1, 2, 4].map((multiplier, index) => {
|
return of(null);
|
||||||
return {
|
})
|
||||||
fee: this.minExtraCost * multiplier,
|
).subscribe();
|
||||||
rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize,
|
});
|
||||||
index,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO;
|
|
||||||
this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO;
|
|
||||||
this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO;
|
|
||||||
|
|
||||||
this.userBid = this.defaultBid;
|
|
||||||
if (this.userBid < this.minBidAllowed) {
|
|
||||||
this.userBid = this.minBidAllowed;
|
|
||||||
} else if (this.userBid > this.maxBidAllowed) {
|
|
||||||
this.userBid = this.maxBidAllowed;
|
|
||||||
}
|
|
||||||
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
|
||||||
|
|
||||||
if (!this.error) {
|
|
||||||
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
catchError((response) => {
|
|
||||||
this.estimate = undefined;
|
|
||||||
this.error = response.error;
|
|
||||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
|
||||||
this.estimateSubscription.unsubscribe();
|
|
||||||
return of(null);
|
|
||||||
})
|
|
||||||
).subscribe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -216,4 +259,112 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
onResize(): void {
|
onResize(): void {
|
||||||
this.isMobile = window.innerWidth <= 767.98;
|
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);
|
||||||
|
})();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div class="container-lg widget-container" [class.widget]="widget" [class.full-height]="!widget">
|
<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 *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
|
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
|
||||||
<a class="title-link" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
|
<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™ : <ng-container i18n="accelerator.accelerations">Accelerations</ng-container></h5>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
|
@ -11,11 +11,12 @@ import TxView from '../../block-overview-graph/tx-view';
|
|||||||
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../../app.constants';
|
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../../app.constants';
|
||||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||||
import { detectWebGL } from '../../../shared/graphs.utils';
|
import { detectWebGL } from '../../../shared/graphs.utils';
|
||||||
|
import { AudioService } from '../../../services/audio.service';
|
||||||
import { ThemeService } from '../../../services/theme.service';
|
import { ThemeService } from '../../../services/theme.service';
|
||||||
|
|
||||||
const acceleratedColor: Color = hexToColor('8F5FF6');
|
const acceleratedColor: Color = hexToColor('8F5FF6');
|
||||||
const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F'));
|
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 {
|
interface AccelerationBlock extends BlockExtended {
|
||||||
accelerationCount: number,
|
accelerationCount: number,
|
||||||
@ -34,6 +35,8 @@ export class AcceleratorDashboardComponent implements OnInit {
|
|||||||
minedAccelerations$: Observable<Acceleration[]>;
|
minedAccelerations$: Observable<Acceleration[]>;
|
||||||
loadingBlocks: boolean = true;
|
loadingBlocks: boolean = true;
|
||||||
webGlEnabled = true;
|
webGlEnabled = true;
|
||||||
|
seen: Set<string> = new Set();
|
||||||
|
firstLoad = true;
|
||||||
|
|
||||||
graphHeight: number = 300;
|
graphHeight: number = 300;
|
||||||
theme: ThemeService;
|
theme: ThemeService;
|
||||||
@ -43,6 +46,7 @@ export class AcceleratorDashboardComponent implements OnInit {
|
|||||||
private ogService: OpenGraphService,
|
private ogService: OpenGraphService,
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private serviceApiServices: ServicesApiServices,
|
private serviceApiServices: ServicesApiServices,
|
||||||
|
private audioService: AudioService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
@Inject(PLATFORM_ID) private platformId: Object,
|
@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(),
|
share(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import { ChainStats } from '../../interfaces/electrs.interface';
|
|||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-address-graph',
|
selector: 'app-address-graph',
|
||||||
@ -46,6 +47,7 @@ export class AddressGraphComponent implements OnChanges {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private amountShortenerPipe: AmountShortenerPipe,
|
private amountShortenerPipe: AmountShortenerPipe,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
@ -122,7 +124,7 @@ export class AddressGraphComponent implements OnChanges {
|
|||||||
</div>
|
</div>
|
||||||
<span>${date}</span>
|
<span>${date}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
@ -178,7 +180,7 @@ export class AddressGraphComponent implements OnChanges {
|
|||||||
|
|
||||||
onChartClick(e) {
|
onChartClick(e) {
|
||||||
if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) {
|
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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,8 +20,7 @@
|
|||||||
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
|
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
|
||||||
<span i18n="shared.confidential">Confidential</span>
|
<span i18n="shared.confidential">Confidential</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #default>
|
<ng-template #default>‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
|
||||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
|
|
||||||
<span class="symbol"><ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
|
<span class="symbol"><ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
|
||||||
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
|
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
|
||||||
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
|
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
|
||||||
|
@ -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">
|
<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™ tooltip" ngbTooltip="select filter categories to highlight matching transactions">
|
||||||
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
|
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
|
||||||
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
|
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<button class="menu-toggle" (click)="menuOpen = !menuOpen" title="Mempool Goggles">
|
<button class="menu-toggle" (click)="menuOpen = !menuOpen" title="Mempool Goggles™">
|
||||||
<app-svg-images name="goggles" width="100%" height="100%"></app-svg-images>
|
<app-svg-images name="goggles" width="100%" height="100%"></app-svg-images>
|
||||||
</button>
|
</button>
|
||||||
<div class="active-tags">
|
<div class="active-tags">
|
||||||
@ -14,14 +14,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-menu" *ngIf="menuOpen && cssWidth > 280">
|
<div class="filter-menu" *ngIf="menuOpen && cssWidth > 280">
|
||||||
<h5>Match</h5>
|
<div class="filter-row">
|
||||||
<div class="btn-group btn-group-toggle">
|
<div class="filter-element">
|
||||||
<label class="btn btn-xs blue mode-toggle" [class.active]="filterMode === 'and'">
|
<h5 i18n="mempool-goggles.match">Match</h5>
|
||||||
<input type="radio" [value]="'all'" fragment="all" (click)="setFilterMode('and')">All
|
<div class="btn-group btn-group-toggle">
|
||||||
</label>
|
<label class="btn btn-xs blue mode-toggle" [class.active]="filterMode === 'and'">
|
||||||
<label class="btn btn-xs green mode-toggle" [class.active]="filterMode === 'or'">
|
<input type="radio" [value]="'all'" fragment="all" (click)="setFilterMode('and')"><ng-container i18n>All</ng-container>
|
||||||
<input type="radio" [value]="'any'" fragment="any" (click)="setFilterMode('or')">Any
|
</label>
|
||||||
</label>
|
<label class="btn btn-xs green mode-toggle" [class.active]="filterMode === 'or'">
|
||||||
|
<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>
|
</div>
|
||||||
<ng-container *ngFor="let group of filterGroups;">
|
<ng-container *ngFor="let group of filterGroups;">
|
||||||
<h5>{{ group.label }}</h5>
|
<h5>{{ group.label }}</h5>
|
||||||
|
@ -45,6 +45,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filter-menu {
|
.filter-menu {
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: white;
|
color: white;
|
||||||
@ -118,6 +125,12 @@
|
|||||||
background: var(--success);
|
background: var(--success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.yellow {
|
||||||
|
border: solid 1px #bf7815;
|
||||||
|
&.active {
|
||||||
|
background: #bf7815;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(.block-overview-graph:hover) &, &:hover, &:active {
|
:host-context(.block-overview-graph:hover) &, &:hover, &:active {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
|
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 { StateService } from '../../services/state.service';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@ -22,6 +22,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
activeFilters: string[] = [];
|
activeFilters: string[] = [];
|
||||||
filterFlags: { [key: string]: boolean } = {};
|
filterFlags: { [key: string]: boolean } = {};
|
||||||
filterMode: FilterMode = 'and';
|
filterMode: FilterMode = 'and';
|
||||||
|
gradientMode: GradientMode = 'fee';
|
||||||
menuOpen: boolean = false;
|
menuOpen: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -32,6 +33,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
|
this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
|
||||||
this.filterMode = active.mode;
|
this.filterMode = active.mode;
|
||||||
|
this.gradientMode = active.gradient;
|
||||||
for (const key of Object.keys(this.filterFlags)) {
|
for (const key of Object.keys(this.filterFlags)) {
|
||||||
this.filterFlags[key] = false;
|
this.filterFlags[key] = false;
|
||||||
}
|
}
|
||||||
@ -39,7 +41,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.filterFlags[key] = !this.disabledFilters[key];
|
this.filterFlags[key] = !this.disabledFilters[key];
|
||||||
}
|
}
|
||||||
this.activeFilters = [...active.filters.filter(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 {
|
setFilterMode(mode): void {
|
||||||
this.filterMode = mode;
|
this.filterMode = mode;
|
||||||
this.onFilterChanged.emit({ 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] });
|
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 {
|
toggleFilter(key): void {
|
||||||
@ -81,8 +89,8 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.activeFilters = this.activeFilters.filter(f => f != key);
|
this.activeFilters = this.activeFilters.filter(f => f != key);
|
||||||
}
|
}
|
||||||
const booleanFlags = this.getBooleanFlags();
|
const booleanFlags = this.getBooleanFlags();
|
||||||
this.onFilterChanged.emit({ 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] });
|
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters], gradient: this.gradientMode });
|
||||||
}
|
}
|
||||||
|
|
||||||
getBooleanFlags(): bigint | null {
|
getBooleanFlags(): bigint | null {
|
||||||
|
@ -9,14 +9,11 @@ import { Price } from '../../services/price.service';
|
|||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { ThemeService } from 'src/app/services/theme.service';
|
import { ThemeService } from 'src/app/services/theme.service';
|
||||||
import { Subscription } from 'rxjs';
|
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 { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
|
||||||
import { detectWebGL } from '../../shared/graphs.utils';
|
import { detectWebGL } from '../../shared/graphs.utils';
|
||||||
|
|
||||||
const unmatchedOpacity = 0.2;
|
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 = {
|
const unmatchedAuditColors = {
|
||||||
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
|
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
|
||||||
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
|
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
|
||||||
@ -57,6 +54,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
@Input() excludeFilters: string[] = [];
|
@Input() excludeFilters: string[] = [];
|
||||||
@Input() filterFlags: bigint | null = null;
|
@Input() filterFlags: bigint | null = null;
|
||||||
@Input() filterMode: FilterMode = 'and';
|
@Input() filterMode: FilterMode = 'and';
|
||||||
|
@Input() gradientMode: 'fee' | 'age' = 'fee';
|
||||||
@Input() relativeTime: number | null;
|
@Input() relativeTime: number | null;
|
||||||
@Input() blockConversion: Price;
|
@Input() blockConversion: Price;
|
||||||
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
||||||
@ -137,21 +135,22 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
this.setHighlightingEnabled(this.auditHighlighting);
|
this.setHighlightingEnabled(this.auditHighlighting);
|
||||||
}
|
}
|
||||||
if (changes.overrideColor && this.scene) {
|
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();
|
this.setFilterFlags();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilterFlags(goggle?: ActiveFilter): void {
|
setFilterFlags(goggle?: ActiveFilter): void {
|
||||||
this.filterMode = goggle?.mode || this.filterMode;
|
this.filterMode = goggle?.mode || this.filterMode;
|
||||||
|
this.gradientMode = goggle?.gradient || this.gradientMode;
|
||||||
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
|
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
|
||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
if (this.activeFilterFlags != null && this.filtersAvailable) {
|
if (this.activeFilterFlags != null && this.filtersAvailable) {
|
||||||
this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags));
|
this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode));
|
||||||
} else {
|
} else {
|
||||||
this.scene.setColorFunction(this.overrideColors);
|
this.scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.start();
|
this.start();
|
||||||
@ -229,6 +228,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
remove = remove.filter(txid => this.scene.txs[txid]);
|
remove = remove.filter(txid => this.scene.txs[txid]);
|
||||||
change = change.filter(tx => this.scene.txs[tx.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.scene.update(add, remove, change, direction, resetLayout);
|
||||||
this.start();
|
this.start();
|
||||||
this.updateSearchHighlight();
|
this.updateSearchHighlight();
|
||||||
@ -564,32 +566,27 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
}
|
}
|
||||||
|
|
||||||
getColorFunction(): ((tx: TxView) => Color) {
|
getColorFunction(): ((tx: TxView) => Color) {
|
||||||
if (this.filterFlags) {
|
if (this.overrideColors) {
|
||||||
return this.getFilterColorFunction(this.filterFlags);
|
|
||||||
} else if (this.activeFilterFlags) {
|
|
||||||
return this.getFilterColorFunction(this.activeFilterFlags);
|
|
||||||
} else {
|
|
||||||
return 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) => {
|
return (tx: TxView) => {
|
||||||
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
|
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 {
|
} else {
|
||||||
return this.themeService.theme !== 'default' ? contrastColorFunction(
|
return defaultColorFunction(
|
||||||
tx,
|
tx,
|
||||||
unmatchedContrastFeeColors,
|
defaultColors.unmatchedfee,
|
||||||
unmatchedContrastAuditFeeColors,
|
unmatchedAuditColors,
|
||||||
unmatchedContrastMarginalFeeColors,
|
this.relativeTime || (Date.now() / 1000)
|
||||||
unmatchedContrastAuditColors
|
|
||||||
) : defaultColorFunction(
|
|
||||||
tx,
|
|
||||||
unmatchedFeeColors,
|
|
||||||
unmatchedAuditFeeColors,
|
|
||||||
unmatchedMarginalFeeColors,
|
|
||||||
unmatchedAuditColors
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -69,7 +69,7 @@ export default class BlockScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void {
|
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;
|
this.dirty = true;
|
||||||
if (this.initialised && this.scene) {
|
if (this.initialised && this.scene) {
|
||||||
this.updateColors(performance.now(), 50);
|
this.updateColors(performance.now(), 50);
|
||||||
|
@ -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
|
// 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 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 defaultMarginalFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.8), 1.1));
|
||||||
export const defaultAuditColors = {
|
export const defaultAuditColors = {
|
||||||
@ -62,22 +69,21 @@ export const contrastAuditColors = {
|
|||||||
|
|
||||||
export function defaultColorFunction(
|
export function defaultColorFunction(
|
||||||
tx: TxView,
|
tx: TxView,
|
||||||
feeColors: Color[] = defaultFeeColors,
|
colors: { base: Color[], audit: Color[], marginal: Color[], baseLevel: (tx: TxView, rate: number, time: number) => number } = defaultColors.fee,
|
||||||
auditFeeColors: Color[] = defaultAuditFeeColors,
|
auditColors: { [status: string]: Color } = defaultAuditColors,
|
||||||
marginalFeeColors: Color[] = defaultMarginalFeeColors,
|
relativeTime?: number,
|
||||||
auditColors: { [status: string]: Color } = defaultAuditColors
|
|
||||||
): Color {
|
): Color {
|
||||||
const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
|
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 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
|
// Normal mode
|
||||||
if (!tx.scene?.highlightingEnabled) {
|
if (!tx.scene?.highlightingEnabled) {
|
||||||
if (tx.acc) {
|
if (tx.acc) {
|
||||||
return auditColors.accelerated;
|
return auditColors.accelerated;
|
||||||
} else {
|
} else {
|
||||||
return feeLevelColor;
|
return levelColor;
|
||||||
}
|
}
|
||||||
return feeLevelColor;
|
return levelColor;
|
||||||
}
|
}
|
||||||
// Block audit
|
// Block audit
|
||||||
switch(tx.status) {
|
switch(tx.status) {
|
||||||
@ -86,7 +92,7 @@ export function defaultColorFunction(
|
|||||||
case 'missing':
|
case 'missing':
|
||||||
case 'sigop':
|
case 'sigop':
|
||||||
case 'rbf':
|
case 'rbf':
|
||||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[defaultMempoolFeeColors.length - 1];
|
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||||
case 'fresh':
|
case 'fresh':
|
||||||
case 'freshcpfp':
|
case 'freshcpfp':
|
||||||
return auditColors.missing;
|
return auditColors.missing;
|
||||||
@ -95,24 +101,45 @@ export function defaultColorFunction(
|
|||||||
case 'prioritized':
|
case 'prioritized':
|
||||||
return auditColors.prioritized;
|
return auditColors.prioritized;
|
||||||
case 'selected':
|
case 'selected':
|
||||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[defaultMempoolFeeColors.length - 1];
|
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||||
case 'accelerated':
|
case 'accelerated':
|
||||||
return auditColors.accelerated;
|
return auditColors.accelerated;
|
||||||
case 'found':
|
case 'found':
|
||||||
if (tx.context === 'projected') {
|
if (tx.context === 'projected') {
|
||||||
return auditFeeColors[feeLevelIndex] || auditFeeColors[defaultMempoolFeeColors.length - 1];
|
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||||
} else {
|
} else {
|
||||||
return feeLevelColor;
|
return levelColor;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if (tx.acc) {
|
if (tx.acc) {
|
||||||
return auditColors.accelerated;
|
return auditColors.accelerated;
|
||||||
} else {
|
} 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(
|
export function contrastColorFunction(
|
||||||
tx: TxView,
|
tx: TxView,
|
||||||
feeColors: Color[] = contrastFeeColors,
|
feeColors: Color[] = contrastFeeColors,
|
||||||
|
@ -53,13 +53,13 @@
|
|||||||
<td i18n="block.miner">Miner</td>
|
<td i18n="block.miner">Miner</td>
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
<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"
|
<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 }}
|
{{ block?.extras.pool.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||||
<span [attr.data-cy]="'block-details-miner-badge'" placement="bottom" class="badge"
|
<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 }}
|
{{ block?.extras.pool.name }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -182,13 +182,13 @@
|
|||||||
<td i18n="block.miner">Miner</td>
|
<td i18n="block.miner">Miner</td>
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
<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 }}
|
{{ block.extras.pool.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||||
<span placement="bottom" class="badge"
|
<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 }}
|
{{ block.extras.pool.name }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -7,7 +7,6 @@ import { EnterpriseService } from '../../services/enterprise.service';
|
|||||||
import { NavigationService } from '../../services/navigation.service';
|
import { NavigationService } from '../../services/navigation.service';
|
||||||
import { MenuComponent } from '../menu/menu.component';
|
import { MenuComponent } from '../menu/menu.component';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { ApiService } from '../../services/api.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-master-page',
|
selector: 'app-master-page',
|
||||||
@ -45,7 +44,6 @@ export class MasterPageComponent implements OnInit, OnDestroy {
|
|||||||
private enterpriseService: EnterpriseService,
|
private enterpriseService: EnterpriseService,
|
||||||
private navigationService: NavigationService,
|
private navigationService: NavigationService,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private apiService: ApiService,
|
|
||||||
private router: Router,
|
private router: Router,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
[showFilters]="showFilters"
|
[showFilters]="showFilters"
|
||||||
[filterFlags]="filterFlags"
|
[filterFlags]="filterFlags"
|
||||||
[filterMode]="filterMode"
|
[filterMode]="filterMode"
|
||||||
|
[gradientMode]="gradientMode"
|
||||||
[excludeFilters]="['nonstandard']"
|
[excludeFilters]="['nonstandard']"
|
||||||
[overrideColors]="overrideColors"
|
[overrideColors]="overrideColors"
|
||||||
(txClickEvent)="onTxClick($event)"
|
(txClickEvent)="onTxClick($event)"
|
||||||
|
@ -11,7 +11,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Color } from '../block-overview-graph/sprite-types';
|
import { Color } from '../block-overview-graph/sprite-types';
|
||||||
import TxView from '../block-overview-graph/tx-view';
|
import TxView from '../block-overview-graph/tx-view';
|
||||||
import { FilterMode } from '../../shared/filters.utils';
|
import { FilterMode, GradientMode } from '../../shared/filters.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-mempool-block-overview',
|
selector: 'app-mempool-block-overview',
|
||||||
@ -25,6 +25,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
|||||||
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
||||||
@Input() filterFlags: bigint | undefined = undefined;
|
@Input() filterFlags: bigint | undefined = undefined;
|
||||||
@Input() filterMode: FilterMode = 'and';
|
@Input() filterMode: FilterMode = 'and';
|
||||||
|
@Input() gradientMode: GradientMode = 'fee';
|
||||||
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
||||||
|
|
||||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||||
|
@ -5,8 +5,7 @@
|
|||||||
height: calc(100vh - 65px);
|
height: calc(100vh - 65px);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 65px;
|
top: 65px;
|
||||||
left: -250px;
|
transition: 0.25s;
|
||||||
transition: left 0.25s;
|
|
||||||
box-shadow: 5px 0px 30px 0px #000;
|
box-shadow: 5px 0px 30px 0px #000;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
@media (max-width: 613px) {
|
@media (max-width: 613px) {
|
||||||
@ -14,6 +13,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host-context(.ltr-layout) .sidenav {
|
||||||
|
left: -250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.rtl-layout) .sidenav {
|
||||||
|
right: -250px;
|
||||||
|
}
|
||||||
|
|
||||||
.ellipsis {
|
.ellipsis {
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -26,11 +33,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidenav.open {
|
.sidenav.open {
|
||||||
left: 0px;
|
|
||||||
display: block;
|
display: block;
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host-context(.ltr-layout) .sidenav.open {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.rtl-layout) .sidenav.open {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.sidenav a, button{
|
.sidenav a, button{
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: lightgray;
|
color: lightgray;
|
||||||
|
@ -175,13 +175,15 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
} as PieSeriesOption);
|
} as PieSeriesOption);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const percentage = totalShareOther.toFixed(2) + '%';
|
||||||
|
|
||||||
// 'Other'
|
// 'Other'
|
||||||
data.push({
|
data.push({
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: '#6b6b6b',
|
color: '#6b6b6b',
|
||||||
},
|
},
|
||||||
value: totalShareOther,
|
value: totalShareOther,
|
||||||
name: 'Other' + (isMobile() ? `` : ` (${totalShareOther.toFixed(2)}%)`),
|
name: $localize`Other (${percentage})`,
|
||||||
label: {
|
label: {
|
||||||
overflow: 'none',
|
overflow: 'none',
|
||||||
color: 'var(--tooltip-grey)',
|
color: 'var(--tooltip-grey)',
|
||||||
@ -197,7 +199,6 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
borderColor: '#000',
|
borderColor: '#000',
|
||||||
formatter: () => {
|
formatter: () => {
|
||||||
const percentage = totalShareOther.toFixed(2) + '%';
|
|
||||||
const i = totalBlockOther.toString();
|
const i = totalBlockOther.toString();
|
||||||
if (this.miningWindowPreference === '24h') {
|
if (this.miningWindowPreference === '24h') {
|
||||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
|
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
|
||||||
|
@ -40,70 +40,45 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
<ng-template [ngIf]="!isLoadingTx && !error">
|
@if (!error) {
|
||||||
|
<div class="box">
|
||||||
<ng-template [ngIf]="tx?.status?.confirmed" [ngIfElse]="unconfirmedTemplate">
|
<div class="row">
|
||||||
|
@if (isMobile) {
|
||||||
<div class="box">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<ng-container *ngTemplateOutlet="detailsLeft"></ng-container>
|
||||||
<td i18n="block.timestamp">Timestamp</td>
|
<ng-container *ngTemplateOutlet="detailsRight"></ng-container>
|
||||||
<td>
|
</tbody>
|
||||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
</table>
|
||||||
<div class="lg-inline">
|
</div>
|
||||||
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
|
} @else {
|
||||||
</div>
|
<div class="col-sm">
|
||||||
</td>
|
<table class="table table-borderless table-striped">
|
||||||
</tr>
|
<tbody>
|
||||||
<ng-template [ngIf]="transactionTime > 0">
|
<ng-container *ngTemplateOutlet="detailsLeft"></ng-container>
|
||||||
<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>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<ng-container *ngTemplateOutlet="feeTable"></ng-container>
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<ng-container *ngTemplateOutlet="detailsRight"></ng-container>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
</ng-template>
|
<ng-template [ngIf]="!isLoadingTx && !error">
|
||||||
|
|
||||||
<!-- Accelerator -->
|
<!-- Accelerator -->
|
||||||
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary">
|
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary">
|
||||||
<br>
|
<br>
|
||||||
<div class="title float-left">
|
<div class="title float-left">
|
||||||
<h2>Accelerate</h2>
|
<h2 i18n="transaction.accelerate|Accelerate button label">Accelerate</h2>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-outline-info accelerator-toggle btn-sm float-right" (click)="showAccelerationSummary = false" i18n="hide-accelerator">Hide accelerator</button>
|
<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>
|
<div class="clearfix"></div>
|
||||||
@ -113,69 +88,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</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">
|
<ng-template [ngIf]="showCpfpDetails">
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@ -364,42 +276,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template [ngIf]="(isLoadingTx && !error) || loadingCachedTx">
|
<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>
|
<br>
|
||||||
|
|
||||||
<ng-container *ngIf="flowEnabled">
|
<ng-container *ngIf="flowEnabled">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h2 i18n="transaction.flow|Transaction flow">Flow</h2>
|
<h2 i18n="transaction.flow|Transaction flow">Flow</h2>
|
||||||
@ -525,65 +402,264 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #feeTable>
|
<ng-template #detailsLeft>
|
||||||
<table class="table table-borderless table-striped">
|
@if (tx?.status?.confirmed) {
|
||||||
<tbody>
|
<ng-container *ngTemplateOutlet="timestampRow"></ng-container>
|
||||||
<tr *ngIf="isMobile && (network === 'liquid' || network === 'liquidtestnet' || !featuresEnabled || network === '')"></tr>
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
‎{{ 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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @else {
|
||||||
|
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #confirmedAfterRow>
|
||||||
|
@if (!isLoadingTx) {
|
||||||
|
@if (transactionTime > 0) {
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
<td i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</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>
|
<td><app-time kind="span" [time]="tx.status.block_time - transactionTime" [fastRender]="true" [relative]="true"></app-time></td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
|
||||||
<td>
|
<td>
|
||||||
<app-fee-rate [fee]="tx.feePerVsize"></app-fee-rate>
|
@if (this.mempoolPosition?.block == null) {
|
||||||
<ng-template [ngIf]="tx?.status?.confirmed">
|
<span class="skeleton-loader"></span>
|
||||||
|
} @else if (this.mempoolPosition.block >= 7) {
|
||||||
<app-tx-fee-rating *ngIf="tx.fee && !hasEffectiveFeeRate && !accelerationInfo" [tx]="tx"></app-tx-fee-rating>
|
<span [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
|
||||||
</ng-template>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="(cpfpInfo && hasEffectiveFeeRate) || accelerationInfo">
|
}
|
||||||
<td *ngIf="tx.acceleration || accelerationInfo" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
|
} @else {
|
||||||
<td *ngIf="!(tx.acceleration || accelerationInfo)" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
<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">
|
||||||
|
@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) {
|
||||||
|
|
||||||
|
<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>
|
<td>
|
||||||
<div class="effective-fee-container">
|
<div class="effective-fee-container">
|
||||||
<app-fee-rate *ngIf="accelerationInfo" [fee]="accelerationInfo.acceleratedFee" [weight]="accelerationInfo.effectiveVsize * 4"></app-fee-rate>
|
@if (accelerationInfo) {
|
||||||
<app-fee-rate *ngIf="!accelerationInfo" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
<app-fee-rate [fee]="accelerationInfo.acceleratedFee" [weight]="accelerationInfo.effectiveVsize * 4"></app-fee-rate>
|
||||||
|
} @else {
|
||||||
|
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||||
|
}
|
||||||
|
|
||||||
<ng-template [ngIf]="tx?.status?.confirmed || tx.acceleration || accelerationInfo">
|
@if (tx?.status?.confirmed && !tx.acceleration && !accelerationInfo && tx.fee && tx.effectiveFeePerVsize) {
|
||||||
<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>
|
<app-tx-fee-rating class="ml-2 mr-2 effective-fee-rating" [tx]="tx"></app-tx-fee-rating>
|
||||||
</ng-template>
|
}
|
||||||
</div>
|
</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>
|
@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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="network === '' && tx?.status?.confirmed">
|
}
|
||||||
<td class="td-width" i18n="block.miner">Miner</td>
|
} @else {
|
||||||
<td *ngIf="pool" class="wrap-cell">
|
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
|
||||||
<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>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #goggles>
|
<ng-template #minerRow>
|
||||||
<tr *ngIf="((auditStatus && auditStatus.accelerated) || accelerationInfo || (tx && tx.acceleration)) || filters.length">
|
@if (network === '') {
|
||||||
<td class="td-width">
|
@if (!isLoadingTx) {
|
||||||
<span class="goggles-icon"><app-svg-images name="goggles" width="100%" height="100%"></app-svg-images></span>
|
<tr>
|
||||||
</td>
|
<td class="td-width" i18n="block.miner">Miner</td>
|
||||||
<td class="wrap-cell">
|
@if (pool) {
|
||||||
<span *ngIf="((auditStatus && auditStatus.accelerated) || accelerationInfo || (tx && tx.acceleration))" class="badge badge-accelerated mr-1" i18n="transaction.audit.accelerated">Accelerated</span>
|
<td class="wrap-cell">
|
||||||
<ng-container *ngFor="let filter of filters;">
|
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge mr-1"
|
||||||
<span class="badge badge-primary filter-tag mr-1">{{ filter.label }}</span>
|
[class]="pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
</ng-container>
|
{{ pool.name }}
|
||||||
</td>
|
</a>
|
||||||
</tr>
|
</td>
|
||||||
|
} @else {
|
||||||
|
<td>
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
} @else {
|
||||||
|
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
|
||||||
|
}
|
||||||
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #skeletonDetailsRow>
|
||||||
|
<tr>
|
||||||
|
<td><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
@ -87,6 +87,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
rbfReplaces: string[];
|
rbfReplaces: string[];
|
||||||
rbfInfo: RbfTree;
|
rbfInfo: RbfTree;
|
||||||
cpfpInfo: CpfpInfo | null;
|
cpfpInfo: CpfpInfo | null;
|
||||||
|
hasCpfp: boolean = false;
|
||||||
accelerationInfo: Acceleration | null = null;
|
accelerationInfo: Acceleration | null = null;
|
||||||
sigops: number | null;
|
sigops: number | null;
|
||||||
adjustedVsize: number | null;
|
adjustedVsize: number | null;
|
||||||
@ -491,10 +492,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
txFeePerVSize: tx.effectiveFeePerVsize,
|
txFeePerVSize: tx.effectiveFeePerVsize,
|
||||||
mempoolPosition: this.mempoolPosition,
|
mempoolPosition: this.mempoolPosition,
|
||||||
});
|
});
|
||||||
this.cpfpInfo = {
|
this.setCpfpInfo({
|
||||||
ancestors: tx.ancestors,
|
ancestors: tx.ancestors,
|
||||||
bestDescendant: tx.bestDescendant,
|
bestDescendant: tx.bestDescendant,
|
||||||
};
|
});
|
||||||
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant);
|
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant);
|
||||||
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
|
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
|
||||||
} else {
|
} else {
|
||||||
@ -646,6 +647,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
setCpfpInfo(cpfpInfo: CpfpInfo): void {
|
setCpfpInfo(cpfpInfo: CpfpInfo): void {
|
||||||
if (!cpfpInfo || !this.tx) {
|
if (!cpfpInfo || !this.tx) {
|
||||||
this.cpfpInfo = null;
|
this.cpfpInfo = null;
|
||||||
|
this.hasCpfp = false;
|
||||||
this.hasEffectiveFeeRate = false;
|
this.hasEffectiveFeeRate = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -675,6 +677,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.sigops = this.cpfpInfo.sigops;
|
this.sigops = this.cpfpInfo.sigops;
|
||||||
this.adjustedVsize = this.cpfpInfo.adjustedVsize;
|
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));
|
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<div class="card graph-card">
|
<div class="card graph-card">
|
||||||
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
|
<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]">
|
<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™</span> : {{ goggleCycle[goggleIndex].name }}</h5>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
@ -35,6 +35,7 @@
|
|||||||
[resolution]="goggleResolution"
|
[resolution]="goggleResolution"
|
||||||
[filterFlags]="goggleFlags"
|
[filterFlags]="goggleFlags"
|
||||||
[filterMode]="goggleMode"
|
[filterMode]="goggleMode"
|
||||||
|
[gradientMode]="gradientMode"
|
||||||
></app-mempool-block-overview>
|
></app-mempool-block-overview>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@ import { ApiService } from '../services/api.service';
|
|||||||
import { StateService } from '../services/state.service';
|
import { StateService } from '../services/state.service';
|
||||||
import { WebsocketService } from '../services/websocket.service';
|
import { WebsocketService } from '../services/websocket.service';
|
||||||
import { SeoService } from '../services/seo.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';
|
import { detectWebGL } from '../shared/graphs.utils';
|
||||||
|
|
||||||
interface MempoolBlocksData {
|
interface MempoolBlocksData {
|
||||||
@ -74,14 +74,15 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
private lastReservesBlockUpdate: number = 0;
|
private lastReservesBlockUpdate: number = 0;
|
||||||
|
|
||||||
goggleResolution = 82;
|
goggleResolution = 82;
|
||||||
goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[] }[] = [
|
goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[], gradient: GradientMode }[] = [
|
||||||
{ index: 0, name: 'All', mode: 'and', filters: [] },
|
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
|
||||||
{ index: 1, name: 'Consolidation', mode: 'and', filters: ['consolidation'] },
|
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
|
||||||
{ index: 2, name: 'Coinjoin', mode: 'and', filters: ['coinjoin'] },
|
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
|
||||||
{ index: 3, name: 'Data', mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'] },
|
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
|
||||||
];
|
];
|
||||||
goggleFlags = 0n;
|
goggleFlags = 0n;
|
||||||
goggleMode: FilterMode = 'and';
|
goggleMode: FilterMode = 'and';
|
||||||
|
gradientMode: GradientMode = 'age';
|
||||||
goggleIndex = 0;
|
goggleIndex = 0;
|
||||||
|
|
||||||
private destroy$ = new Subject();
|
private destroy$ = new Subject();
|
||||||
@ -131,6 +132,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.goggleIndex = goggle.index;
|
this.goggleIndex = goggle.index;
|
||||||
this.goggleFlags = toFlags(goggle.filters);
|
this.goggleFlags = toFlags(goggle.filters);
|
||||||
this.goggleMode = goggle.mode;
|
this.goggleMode = goggle.mode;
|
||||||
|
this.gradientMode = active.gradient;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,6 +142,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
name: 'Custom',
|
name: 'Custom',
|
||||||
mode: active.mode,
|
mode: active.mode,
|
||||||
filters: active.filters,
|
filters: active.filters,
|
||||||
|
gradient: active.gradient,
|
||||||
});
|
});
|
||||||
this.goggleIndex = this.goggleCycle.length - 1;
|
this.goggleIndex = this.goggleCycle.length - 1;
|
||||||
this.goggleFlags = toFlags(active.filters);
|
this.goggleFlags = toFlags(active.filters);
|
||||||
|
@ -9584,7 +9584,7 @@ export const faqData = [
|
|||||||
category: "advanced",
|
category: "advanced",
|
||||||
showConditions: bitcoinNetworks,
|
showConditions: bitcoinNetworks,
|
||||||
fragment: "how-do-mempool-goggles-work",
|
fragment: "how-do-mempool-goggles-work",
|
||||||
title: "How do Mempool Goggles work?",
|
title: "How do Mempool Goggles™ work?",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "endpoint",
|
type: "endpoint",
|
||||||
|
@ -290,8 +290,8 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template type="how-do-mempool-goggles-work">
|
<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>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>There are currently 25 different Mempool Goggles™ filters, grouped into six categories:</p>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Features</dt>
|
<dt>Features</dt>
|
||||||
<dd>
|
<dd>
|
||||||
@ -418,5 +418,5 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template type="address-lookup-issues">
|
<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>
|
</ng-template>
|
||||||
|
@ -163,6 +163,7 @@ export interface PoolInfo {
|
|||||||
emptyBlocks: number;
|
emptyBlocks: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
poolUniqueId: number;
|
poolUniqueId: number;
|
||||||
|
unique_id: number;
|
||||||
}
|
}
|
||||||
export interface PoolStat {
|
export interface PoolStat {
|
||||||
pool: PoolInfo;
|
pool: PoolInfo;
|
||||||
|
@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
||||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
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';
|
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 { StateService } from './state.service';
|
||||||
import { Transaction } from '../interfaces/electrs.interface';
|
import { Transaction } from '../interfaces/electrs.interface';
|
||||||
import { Conversion } from './price.service';
|
import { Conversion } from './price.service';
|
||||||
@ -246,11 +246,29 @@ export class ApiService {
|
|||||||
return this.httpClient.get<any>(
|
return this.httpClient.get<any>(
|
||||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools` +
|
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools` +
|
||||||
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
|
(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> {
|
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> {
|
getPoolHashrate$(slug: string): Observable<any> {
|
||||||
|
@ -132,6 +132,10 @@ export class ServicesApiServices {
|
|||||||
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate`, { txInput: txInput, userBid: userBid });
|
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[]> {
|
getAccelerations$(): Observable<Acceleration[]> {
|
||||||
return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations`);
|
return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations`);
|
||||||
}
|
}
|
||||||
@ -151,4 +155,8 @@ export class ServicesApiServices {
|
|||||||
getAccelerationStats$(): Observable<AccelerationStats> {
|
getAccelerationStats$(): Observable<AccelerationStats> {
|
||||||
return this.httpClient.get<AccelerationStats>(`${SERVICES_API_PREFIX}/accelerator/accelerations/stats`);
|
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`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,7 @@ const defaultEnv: Env = {
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class StateService {
|
export class StateService {
|
||||||
|
ref: string = '';
|
||||||
isBrowser: boolean = isPlatformBrowser(this.platformId);
|
isBrowser: boolean = isPlatformBrowser(this.platformId);
|
||||||
isMempoolSpaceBuild = window['isMempoolSpaceBuild'] ?? false;
|
isMempoolSpaceBuild = window['isMempoolSpaceBuild'] ?? false;
|
||||||
backend: 'esplora' | 'electrum' | 'none' = 'esplora';
|
backend: 'esplora' | 'electrum' | 'none' = 'esplora';
|
||||||
@ -154,7 +155,7 @@ export class StateService {
|
|||||||
searchFocus$: Subject<boolean> = new Subject<boolean>();
|
searchFocus$: Subject<boolean> = new Subject<boolean>();
|
||||||
menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
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(
|
constructor(
|
||||||
@Inject(PLATFORM_ID) private platformId: any,
|
@Inject(PLATFORM_ID) private platformId: any,
|
||||||
@ -162,6 +163,8 @@ export class StateService {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
) {
|
) {
|
||||||
|
this.ref = window.document.referrer;
|
||||||
|
|
||||||
const browserWindow = window || {};
|
const browserWindow = window || {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const browserWindowEnv = browserWindow.__env || {};
|
const browserWindowEnv = browserWindow.__env || {};
|
||||||
|
@ -11,9 +11,12 @@ export interface Filter {
|
|||||||
|
|
||||||
export type FilterMode = 'and' | 'or';
|
export type FilterMode = 'and' | 'or';
|
||||||
|
|
||||||
|
export type GradientMode = 'fee' | 'age';
|
||||||
|
|
||||||
export interface ActiveFilter {
|
export interface ActiveFilter {
|
||||||
mode: FilterMode,
|
mode: FilterMode,
|
||||||
filters: string[],
|
filters: string[],
|
||||||
|
gradient: GradientMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
// binary flags for transaction classification
|
// 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_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, },
|
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, },
|
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 */
|
/* data */
|
||||||
op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return, important: true, tooltip: true, txPage: true, },
|
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, },
|
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, },
|
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,},
|
fake_scripthash: { key: 'fake_scripthash', label: 'Fake scripthash', flag: TransactionFlags.fake_scripthash, tooltip: true, txPage: true,},
|
||||||
/* heuristics */
|
/* heuristics */
|
||||||
coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin, important: true, tooltip: true, txPage: true, },
|
coinjoin: { key: 'coinjoin', label: $localize`Coinjoin`, flag: TransactionFlags.coinjoin, important: true, tooltip: true, txPage: true, },
|
||||||
consolidation: { key: 'consolidation', label: 'Consolidation', flag: TransactionFlags.consolidation, 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, },
|
batch_payout: { key: 'batch_payout', label: 'Batch payment', flag: TransactionFlags.batch_payout, tooltip: true, txPage: true, },
|
||||||
/* sighash */
|
/* sighash */
|
||||||
sighash_all: { key: 'sighash_all', label: 'sighash_all', flag: TransactionFlags.sighash_all },
|
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[]}[] = [
|
export const FilterGroups: { label: string, filters: Filter[]}[] = [
|
||||||
{ label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'v3', 'nonstandard'] },
|
{ label: $localize`:@@885666551418fd59011ceb09d5c481095940193b:Features`, filters: ['rbf', 'no_rbf', 'v1', 'v2', 'v3', 'nonstandard'] },
|
||||||
{ label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] },
|
{ label: $localize`Address Types`, filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] },
|
||||||
{ label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement', 'acceleration'] },
|
{ label: $localize`Behavior`, filters: ['cpfp_parent', 'cpfp_child', 'replacement', 'acceleration'] },
|
||||||
{ label: 'Data', filters: ['op_return', 'fake_pubkey', 'fake_scripthash', 'inscription'] },
|
{ label: $localize`Data`, filters: ['op_return', 'fake_pubkey', 'fake_scripthash', 'inscription'] },
|
||||||
{ label: 'Heuristics', filters: ['coinjoin', 'consolidation', 'batch_payout'] },
|
{ label: $localize`Heuristics`, filters: ['coinjoin', 'consolidation', 'batch_payout'] },
|
||||||
{ label: 'Sighash Flags', filters: ['sighash_all', 'sighash_none', 'sighash_single', 'sighash_default', 'sighash_acp'] },
|
{ 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) }));
|
].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
@ -7,15 +7,15 @@
|
|||||||
"declaration": false,
|
"declaration": false,
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"module": "ES2022",
|
"module": "ES2020",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"target": "ES2022",
|
"target": "ES2020",
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
"node_modules/@types"
|
"node_modules/@types"
|
||||||
],
|
],
|
||||||
"lib": [
|
"lib": [
|
||||||
"ES2022",
|
"ES2020",
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable"
|
"dom.iterable"
|
||||||
]
|
]
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"extends": "./tsconfig.app.json",
|
"extends": "./tsconfig.app.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/server",
|
"outDir": "./out-tsc/server",
|
||||||
"target": "ES2022",
|
"target": "ES2020",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"types": [
|
"types": [
|
||||||
"node"
|
"node"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user