Compare commits
4 Commits
v3.0
...
mononaut/u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79e2883ebe | ||
|
|
fdbca80920 | ||
|
|
64baade3b3 | ||
|
|
4d06636d83 |
12
LICENSE
12
LICENSE
@@ -1,5 +1,5 @@
|
||||
The Mempool Open Source Project®
|
||||
Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders
|
||||
Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
@@ -12,12 +12,10 @@ or any other contributor to The Mempool Open Source Project.
|
||||
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
|
||||
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
|
||||
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
|
||||
the mempool block visualization Logo, the mempool Blocks Logo, the mempool
|
||||
transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,
|
||||
the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are
|
||||
registered trademarks or trademarks of Mempool Space K.K in Japan,
|
||||
the United States, and/or other countries.
|
||||
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo,
|
||||
the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical
|
||||
Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks
|
||||
of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
|
||||
See our full Trademark Policy and Guidelines for more details, published on
|
||||
<https://mempool.space/trademark-policy>.
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||
"AUDIT": false,
|
||||
"RUST_GBT": true,
|
||||
"RUST_GBT": false,
|
||||
"LIMIT_GBT": false,
|
||||
"CPFP_INDEXING": false,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||
|
||||
738
backend/package-lock.json
generated
738
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0-dev",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
@@ -39,7 +39,7 @@
|
||||
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.7.2",
|
||||
@@ -47,16 +47,16 @@
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.11.0",
|
||||
"mysql2": "~3.10.0",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"redis": "^4.7.0",
|
||||
"redis": "^4.6.6",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/express": "^4.17.17",
|
||||
|
||||
@@ -70,7 +70,7 @@ class AboutRoutes {
|
||||
res.status(500).end();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username/:md5', async (req, res) => {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username', async (req, res) => {
|
||||
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
||||
|
||||
@@ -6,22 +6,20 @@ import rbfCache from './rbf-cache';
|
||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||
|
||||
class Audit {
|
||||
auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
|
||||
: { unseen: string[], censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
||||
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
|
||||
: { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||
return { unseen: [], censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
||||
return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
||||
}
|
||||
|
||||
const matches: string[] = []; // present in both mined block and template
|
||||
const added: string[] = []; // present in mined block, not in template
|
||||
const unseen: string[] = []; // present in the mined block, not in our mempool
|
||||
const prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
|
||||
const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool
|
||||
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
||||
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
||||
const isCensored = {}; // missing, without excuse
|
||||
const isDisplaced = {};
|
||||
const isAccelerated = {};
|
||||
let displacedWeight = 0;
|
||||
let matchedWeight = 0;
|
||||
let projectedWeight = 0;
|
||||
@@ -34,7 +32,6 @@ class Audit {
|
||||
inBlock[tx.txid] = tx;
|
||||
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
|
||||
accelerated.push(tx.txid);
|
||||
isAccelerated[tx.txid] = true;
|
||||
}
|
||||
}
|
||||
// coinbase is always expected
|
||||
@@ -116,16 +113,11 @@ class Audit {
|
||||
} else {
|
||||
if (rbfCache.has(tx.txid)) {
|
||||
rbf.push(tx.txid);
|
||||
if (!mempool[tx.txid] && !rbfCache.getReplacedBy(tx.txid)) {
|
||||
unseen.push(tx.txid);
|
||||
}
|
||||
} else {
|
||||
} else if (!isDisplaced[tx.txid]) {
|
||||
if (mempool[tx.txid]) {
|
||||
if (isDisplaced[tx.txid]) {
|
||||
added.push(tx.txid);
|
||||
}
|
||||
prioritized.push(tx.txid);
|
||||
} else {
|
||||
unseen.push(tx.txid);
|
||||
added.push(tx.txid);
|
||||
}
|
||||
}
|
||||
overflowWeight += tx.weight;
|
||||
@@ -133,24 +125,6 @@ class Audit {
|
||||
totalWeight += tx.weight;
|
||||
}
|
||||
|
||||
|
||||
// identify "prioritized" transactions
|
||||
let lastEffectiveRate = 0;
|
||||
// Iterate over the mined template from bottom to top (excluding the coinbase)
|
||||
// Transactions should appear in ascending order of mining priority.
|
||||
for (let i = transactions.length - 1; i > 0; i--) {
|
||||
const blockTx = transactions[i];
|
||||
// If a tx has a lower in-band effective fee rate than the previous tx,
|
||||
// it must have been prioritized out-of-band (in order to have a higher mining priority)
|
||||
// so exclude from the analysis.
|
||||
if ((blockTx.effectiveFeePerVsize || 0) < lastEffectiveRate) {
|
||||
prioritized.push(blockTx.txid);
|
||||
// accelerated txs may or may not have their prioritized fee rate applied, so don't use them as a reference
|
||||
} else if (!isAccelerated[blockTx.txid]) {
|
||||
lastEffectiveRate = blockTx.effectiveFeePerVsize || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// transactions missing from near the end of our template are probably not being censored
|
||||
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
||||
let maxOverflowRate = 0;
|
||||
@@ -191,7 +165,6 @@ class Audit {
|
||||
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
|
||||
|
||||
return {
|
||||
unseen,
|
||||
censored: Object.keys(isCensored),
|
||||
added,
|
||||
prioritized,
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface AbstractBitcoinApi {
|
||||
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
|
||||
$getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>;
|
||||
$getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]>;
|
||||
|
||||
startHealthChecks(): void;
|
||||
getHealthStatus(): HealthCheckHost[];
|
||||
|
||||
@@ -251,6 +251,10 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
return this.$getRawTransaction(txids[0]);
|
||||
}
|
||||
|
||||
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
|
||||
throw new Error('Method getAddressTransactionSummary not supported by the Bitcoin RPC API.');
|
||||
}
|
||||
|
||||
$getEstimatedHashrate(blockHeight: number): Promise<number> {
|
||||
// 120 is the default block span in Core
|
||||
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
|
||||
|
||||
@@ -165,7 +165,6 @@ class BitcoinRoutes {
|
||||
acceleration: tx.acceleration,
|
||||
acceleratedBy: tx.acceleratedBy || undefined,
|
||||
acceleratedAt: tx.acceleratedAt || undefined,
|
||||
feeDelta: tx.feeDelta || undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -179,4 +179,11 @@ export namespace IEsploraApi {
|
||||
burn_count: number;
|
||||
}
|
||||
|
||||
export interface AddressTxSummary {
|
||||
txid: string;
|
||||
value: number;
|
||||
height: number;
|
||||
time: number;
|
||||
tx_position?: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,6 +357,10 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid);
|
||||
}
|
||||
|
||||
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
|
||||
return this.failoverRouter.$get<IEsploraApi.AddressTxSummary[]>('/address/' + address + '/txs/summary');
|
||||
}
|
||||
|
||||
public startHealthChecks(): void {
|
||||
this.failoverRouter.startHealthChecks();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import AccelerationRepository from '../repositories/AccelerationRepository';
|
||||
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
|
||||
import mempool from './mempool';
|
||||
import CpfpRepository from '../repositories/CpfpRepository';
|
||||
import accelerationApi from './services/acceleration';
|
||||
|
||||
class Blocks {
|
||||
private blocks: BlockExtended[] = [];
|
||||
@@ -440,7 +439,7 @@ class Blocks {
|
||||
|
||||
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
|
||||
if (cpfpSummary) {
|
||||
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
|
||||
@@ -905,12 +904,7 @@ class Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
let accelerations = Object.values(mempool.getAccelerations());
|
||||
if (accelerations?.length > 0) {
|
||||
const pool = await this.$findBlockMiner(transactionUtils.stripCoinbaseTransaction(transactions[0]));
|
||||
accelerations = accelerations.filter(a => a.pools.includes(pool.uniqueId));
|
||||
}
|
||||
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
||||
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, Object.values(mempool.getAccelerations()).map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
||||
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
||||
@@ -933,12 +927,12 @@ class Blocks {
|
||||
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
||||
this.blocks.push(newBlock);
|
||||
this.updateTimerProgress(timer, `reindexed block`);
|
||||
let newCpfpSummary;
|
||||
let cpfpSummary;
|
||||
if (config.MEMPOOL.CPFP_INDEXING) {
|
||||
newCpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
|
||||
cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
|
||||
this.updateTimerProgress(timer, `reindexed block cpfp`);
|
||||
}
|
||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true, newCpfpSummary, newBlock.height);
|
||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height);
|
||||
this.updateTimerProgress(timer, `reindexed block summary`);
|
||||
}
|
||||
await mining.$indexDifficultyAdjustments();
|
||||
@@ -987,7 +981,7 @@ class Blocks {
|
||||
|
||||
// start async callbacks
|
||||
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
|
||||
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, cpfpSummary.transactions));
|
||||
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
|
||||
|
||||
if (block.height % 2016 === 0) {
|
||||
if (Common.indexingEnabled()) {
|
||||
@@ -1184,7 +1178,7 @@ class Blocks {
|
||||
};
|
||||
}),
|
||||
};
|
||||
summaryVersion = cpfpSummary.version;
|
||||
summaryVersion = 1;
|
||||
} else {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
@@ -1403,11 +1397,11 @@ class Blocks {
|
||||
return this.currentBlockHeight;
|
||||
}
|
||||
|
||||
public async $indexCPFP(hash: string, height: number, txs?: MempoolTransactionExtended[]): Promise<CpfpSummary | null> {
|
||||
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
|
||||
let transactions = txs;
|
||||
if (!transactions) {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
|
||||
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
}
|
||||
if (!transactions) {
|
||||
const block = await bitcoinClient.getBlock(hash, 2);
|
||||
@@ -1419,7 +1413,7 @@ class Blocks {
|
||||
}
|
||||
|
||||
if (transactions?.length != null) {
|
||||
const summary = calculateFastBlockCpfp(height, transactions);
|
||||
const summary = calculateFastBlockCpfp(height, transactions as TransactionExtended[]);
|
||||
|
||||
await this.$saveCpfp(hash, height, summary);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { Request } from 'express';
|
||||
import { EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
|
||||
import { CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
|
||||
import config from '../config';
|
||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
||||
import { isIP } from 'net';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Acceleration } from './acceleration/acceleration';
|
||||
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
|
||||
const MAX_CLUSTER_ITERATIONS = 100;
|
||||
|
||||
export function calculateFastBlockCpfp(height: number, transactions: MempoolTransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
|
||||
export function calculateFastBlockCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
|
||||
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
|
||||
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
|
||||
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
|
||||
@@ -93,7 +93,6 @@ export function calculateFastBlockCpfp(height: number, transactions: MempoolTran
|
||||
return {
|
||||
transactions,
|
||||
clusters,
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,7 +159,6 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran
|
||||
return {
|
||||
transactions: transactions.map(tx => txMap[tx.txid]),
|
||||
clusters: clusterArray,
|
||||
version: 2,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 81;
|
||||
private static currentVersion = 80;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -653,11 +653,9 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
|
||||
|
||||
if (isBitcoin === true) {
|
||||
await this.$executeQuery('TRUNCATE hashrates');
|
||||
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
||||
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||
}
|
||||
await this.$executeQuery('TRUNCATE hashrates');
|
||||
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
||||
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||
|
||||
await this.updateToSchemaVersion(75);
|
||||
}
|
||||
@@ -693,13 +691,6 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL');
|
||||
await this.updateToSchemaVersion(80);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 81 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
|
||||
await this.updateToSchemaVersion(81);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -453,7 +453,6 @@ class MempoolBlocks {
|
||||
mempoolTx.acceleration = true;
|
||||
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
||||
mempoolTx.acceleratedAt = acceleration?.added;
|
||||
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||
for (const ancestor of mempoolTx.ancestors || []) {
|
||||
if (!mempool[ancestor.txid].acceleration) {
|
||||
mempool[ancestor.txid].cpfpDirty = true;
|
||||
@@ -461,7 +460,6 @@ class MempoolBlocks {
|
||||
mempool[ancestor.txid].acceleration = true;
|
||||
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
|
||||
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
|
||||
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
|
||||
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -337,7 +337,7 @@ export function makeBlockTemplate(candidates: MempoolTransactionExtended[], acce
|
||||
let failures = 0;
|
||||
while (mempoolArray.length || modified.length) {
|
||||
// skip invalid transactions
|
||||
while (mempoolArray[0]?.used || mempoolArray[0]?.modified) {
|
||||
while (mempoolArray[0].used || mempoolArray[0].modified) {
|
||||
mempoolArray.shift();
|
||||
}
|
||||
|
||||
|
||||
@@ -465,12 +465,16 @@ class MiningRoutes {
|
||||
}
|
||||
|
||||
private async $requestAcceleration(req: Request, res: Response): Promise<void> {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS || config.MEMPOOL.OFFICIAL) {
|
||||
res.status(405).send('not available.');
|
||||
return;
|
||||
}
|
||||
res.setHeader('Pragma', 'no-cache');
|
||||
res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
|
||||
res.setHeader('expires', -1);
|
||||
try {
|
||||
accelerationApi.accelerationRequested(req.params.txid);
|
||||
res.status(200).send();
|
||||
res.status(200).send('ok');
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ export interface AccelerationHistory {
|
||||
};
|
||||
|
||||
class AccelerationApi {
|
||||
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
|
||||
private _accelerations: Acceleration[] | null = null;
|
||||
private lastPoll = 0;
|
||||
@@ -53,9 +52,7 @@ class AccelerationApi {
|
||||
}
|
||||
|
||||
public accelerationRequested(txid: string): void {
|
||||
if (this.onDemandPollingEnabled) {
|
||||
this.myAccelerations[txid] = { status: 'requested', added: Date.now() };
|
||||
}
|
||||
this.myAccelerations[txid] = { status: 'requested', added: Date.now() };
|
||||
}
|
||||
|
||||
public accelerationConfirmed(): void {
|
||||
@@ -73,7 +70,7 @@ class AccelerationApi {
|
||||
}
|
||||
|
||||
public async $updateAccelerations(): Promise<Acceleration[] | null> {
|
||||
if (!this.onDemandPollingEnabled) {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
const accelerations = await this.$fetchAccelerations();
|
||||
if (accelerations) {
|
||||
this._accelerations = accelerations;
|
||||
|
||||
26
backend/src/api/services/services-routes.ts
Normal file
26
backend/src/api/services/services-routes.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Application, Request, Response } from 'express';
|
||||
import config from '../../config';
|
||||
import WalletApi from './wallets';
|
||||
|
||||
class ServicesRoutes {
|
||||
public initRoutes(app: Application): void {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'wallet/:walletId', this.$getWallet)
|
||||
;
|
||||
}
|
||||
|
||||
private async $getWallet(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 5).toUTCString());
|
||||
const walletId = req.params.walletId;
|
||||
const wallet = await WalletApi.getWallet(walletId);
|
||||
res.status(200).send(wallet);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ServicesRoutes();
|
||||
131
backend/src/api/services/wallets.ts
Normal file
131
backend/src/api/services/wallets.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import config from '../../config';
|
||||
import logger from '../../logger';
|
||||
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||
import bitcoinApi from '../bitcoin/bitcoin-api-factory';
|
||||
import axios from 'axios';
|
||||
import { TransactionExtended } from '../../mempool.interfaces';
|
||||
|
||||
interface WalletAddress {
|
||||
address: string;
|
||||
active: boolean;
|
||||
transactions?: IEsploraApi.AddressTxSummary[];
|
||||
}
|
||||
|
||||
interface WalletConfig {
|
||||
url: string;
|
||||
name: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
interface Wallet extends WalletConfig {
|
||||
addresses: Record<string, WalletAddress>;
|
||||
lastPoll: number;
|
||||
}
|
||||
|
||||
const POLL_FREQUENCY = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
class WalletApi {
|
||||
private wallets: Record<string, Wallet> = {};
|
||||
private syncing = false;
|
||||
|
||||
constructor() {
|
||||
this.wallets = (config.WALLETS.WALLETS as WalletConfig[]).reduce((acc, wallet) => {
|
||||
acc[wallet.name] = { ...wallet, addresses: {}, lastPoll: 0 };
|
||||
return acc;
|
||||
}, {} as Record<string, Wallet>);
|
||||
}
|
||||
|
||||
public getWallet(wallet: string): Record<string, WalletAddress> {
|
||||
return this.wallets?.[wallet]?.addresses || {};
|
||||
}
|
||||
|
||||
// resync wallet addresses from the provided API
|
||||
async $syncWallets(): Promise<void> {
|
||||
this.syncing = true;
|
||||
for (const walletKey of Object.keys(this.wallets)) {
|
||||
const wallet = this.wallets[walletKey];
|
||||
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
|
||||
try {
|
||||
const response = await axios.get(`${wallet.url}/${wallet.name}`, { headers: { 'Authorization': `${wallet.apiKey}` } });
|
||||
const data: { walletBalances: WalletAddress[] } = response.data;
|
||||
const addresses = data.walletBalances;
|
||||
const newAddresses: Record<string, boolean> = {};
|
||||
// sync all current addresses
|
||||
for (const address of addresses) {
|
||||
await this.$syncWalletAddress(wallet, address);
|
||||
newAddresses[address.address] = true;
|
||||
}
|
||||
// remove old addresses
|
||||
for (const address of Object.keys(wallet.addresses)) {
|
||||
if (!newAddresses[address]) {
|
||||
delete wallet.addresses[address];
|
||||
}
|
||||
}
|
||||
wallet.lastPoll = Date.now();
|
||||
logger.debug(`Synced ${Object.keys(wallet.addresses).length} addresses for wallet ${wallet.name}`);
|
||||
} catch (e) {
|
||||
logger.err(`Error syncing wallet ${wallet.name}: ${(e instanceof Error ? e.message : e)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.syncing = false;
|
||||
}
|
||||
|
||||
// resync address transactions from esplora
|
||||
async $syncWalletAddress(wallet: Wallet, address: WalletAddress): Promise<void> {
|
||||
// fetch full transaction data if the address is new or still active
|
||||
const refreshTransactions = !wallet.addresses[address.address] || address.active;
|
||||
if (refreshTransactions) {
|
||||
try {
|
||||
const walletAddress: WalletAddress = {
|
||||
address: address.address,
|
||||
active: address.active,
|
||||
transactions: await bitcoinApi.$getAddressTransactionSummary(address.address),
|
||||
};
|
||||
logger.debug(`Synced ${walletAddress.transactions?.length || 0} transactions for wallet ${wallet.name} address ${address.address}`);
|
||||
wallet.addresses[address.address] = walletAddress;
|
||||
} catch (e) {
|
||||
logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check a new block for transactions that affect wallet address balances, and add relevant transactions to wallets
|
||||
processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, Record<string, IEsploraApi.AddressTxSummary[]>> {
|
||||
const walletTransactions: Record<string, Record<string, IEsploraApi.AddressTxSummary[]>> = {};
|
||||
for (const walletKey of Object.keys(this.wallets)) {
|
||||
const wallet = this.wallets[walletKey];
|
||||
walletTransactions[walletKey] = {};
|
||||
for (const tx of blockTxs) {
|
||||
const funded: Record<string, number> = {};
|
||||
const spent: Record<string, number> = {};
|
||||
for (const vin of tx.vin) {
|
||||
const address = vin.prevout?.scriptpubkey_address;
|
||||
if (address && wallet.addresses[address]) {
|
||||
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
const address = vout.scriptpubkey_address;
|
||||
if (address && wallet.addresses[address]) {
|
||||
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
|
||||
}
|
||||
}
|
||||
for (const address of Object.keys({ ...funded, ...spent })) {
|
||||
if (!walletTransactions[walletKey][address]) {
|
||||
walletTransactions[walletKey][address] = [];
|
||||
}
|
||||
walletTransactions[walletKey][address].push({
|
||||
txid: tx.txid,
|
||||
value: (funded[address] ?? 0) - (spent[address] ?? 0),
|
||||
height: block.height,
|
||||
time: block.timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return walletTransactions;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WalletApi();
|
||||
@@ -3,7 +3,7 @@ import * as WebSocket from 'ws';
|
||||
import {
|
||||
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
||||
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
||||
MempoolDelta, MempoolDeltaTxids
|
||||
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids
|
||||
} from '../mempool.interfaces';
|
||||
import blocks from './blocks';
|
||||
import memPool from './mempool';
|
||||
@@ -26,6 +26,7 @@ import mempool from './mempool';
|
||||
import statistics from './statistics/statistics';
|
||||
import accelerationRepository from '../repositories/AccelerationRepository';
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import walletApi from './services/wallets';
|
||||
|
||||
interface AddressTransactions {
|
||||
mempool: MempoolTransactionExtended[],
|
||||
@@ -305,6 +306,14 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-wallet']) {
|
||||
if (parsedMessage['track-wallet'] === 'stop') {
|
||||
client['track-wallet'] = null;
|
||||
} else {
|
||||
client['track-wallet'] = parsedMessage['track-wallet'];
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-asset']) {
|
||||
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
|
||||
client['track-asset'] = parsedMessage['track-asset'];
|
||||
@@ -823,7 +832,6 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
},
|
||||
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
||||
};
|
||||
@@ -865,7 +873,6 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
};
|
||||
if (!mempoolTx.cpfpChecked) {
|
||||
calculateMempoolTxCpfp(mempoolTx, newMempool);
|
||||
@@ -933,8 +940,6 @@ class WebsocketHandler {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
}
|
||||
|
||||
const blockTransactions = structuredClone(transactions);
|
||||
|
||||
this.printLogs();
|
||||
await statistics.runStatistics();
|
||||
|
||||
@@ -944,7 +949,7 @@ class WebsocketHandler {
|
||||
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
|
||||
|
||||
const accelerations = Object.values(mempool.getAccelerations());
|
||||
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
|
||||
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, transactions);
|
||||
|
||||
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
||||
@@ -964,7 +969,7 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (Common.indexingEnabled()) {
|
||||
const { unseen, censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(block.height, blockTransactions, projectedBlocks, auditMempool);
|
||||
const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||
|
||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
||||
@@ -986,11 +991,9 @@ class WebsocketHandler {
|
||||
});
|
||||
|
||||
BlocksAuditsRepository.$saveAudit({
|
||||
version: 1,
|
||||
time: block.timestamp,
|
||||
height: block.height,
|
||||
hash: block.id,
|
||||
unseenTxs: unseen,
|
||||
addedTxs: added,
|
||||
prioritizedTxs: prioritized,
|
||||
missingTxs: censored,
|
||||
@@ -1091,6 +1094,9 @@ class WebsocketHandler {
|
||||
replaced: replacedTransactions,
|
||||
};
|
||||
|
||||
// check for wallet transactions
|
||||
const walletTransactions = config.WALLETS.ENABLED ? walletApi.processBlock(block, transactions) : [];
|
||||
|
||||
const responseCache = { ...this.socketData };
|
||||
function getCachedResponse(key, data): string {
|
||||
if (!responseCache[key]) {
|
||||
@@ -1144,7 +1150,6 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
},
|
||||
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
||||
});
|
||||
@@ -1167,7 +1172,6 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1295,6 +1299,11 @@ class WebsocketHandler {
|
||||
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
|
||||
}
|
||||
|
||||
if (client['track-wallet']) {
|
||||
const trackedWallet = client['track-wallet'];
|
||||
response['wallet-transactions'] = getCachedResponse(`wallet-transactions-${trackedWallet}`, walletTransactions[trackedWallet] ?? {});
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
|
||||
@@ -160,6 +160,14 @@ interface IConfig {
|
||||
PAID: boolean;
|
||||
API_KEY: string;
|
||||
},
|
||||
WALLETS: {
|
||||
ENABLED: boolean;
|
||||
WALLETS: {
|
||||
url: string;
|
||||
name: string;
|
||||
apiKey: string;
|
||||
}[];
|
||||
}
|
||||
}
|
||||
|
||||
const defaults: IConfig = {
|
||||
@@ -193,7 +201,7 @@ const defaults: IConfig = {
|
||||
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
||||
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||
'AUDIT': false,
|
||||
'RUST_GBT': true,
|
||||
'RUST_GBT': false,
|
||||
'LIMIT_GBT': false,
|
||||
'CPFP_INDEXING': false,
|
||||
'MAX_BLOCKS_BULK_QUERY': 0,
|
||||
@@ -320,6 +328,10 @@ const defaults: IConfig = {
|
||||
'PAID': false,
|
||||
'API_KEY': '',
|
||||
},
|
||||
'WALLETS': {
|
||||
'ENABLED': false,
|
||||
'WALLETS': [],
|
||||
},
|
||||
};
|
||||
|
||||
class Config implements IConfig {
|
||||
@@ -341,6 +353,7 @@ class Config implements IConfig {
|
||||
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
|
||||
REDIS: IConfig['REDIS'];
|
||||
FIAT_PRICE: IConfig['FIAT_PRICE'];
|
||||
WALLETS: IConfig['WALLETS'];
|
||||
|
||||
constructor() {
|
||||
const configs = this.merge(configFromFile, defaults);
|
||||
@@ -362,6 +375,7 @@ class Config implements IConfig {
|
||||
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
|
||||
this.REDIS = configs.REDIS;
|
||||
this.FIAT_PRICE = configs.FIAT_PRICE;
|
||||
this.WALLETS = configs.WALLETS;
|
||||
}
|
||||
|
||||
merge = (...objects: object[]): IConfig => {
|
||||
|
||||
@@ -32,6 +32,7 @@ import pricesRoutes from './api/prices/prices.routes';
|
||||
import miningRoutes from './api/mining/mining-routes';
|
||||
import liquidRoutes from './api/liquid/liquid.routes';
|
||||
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
|
||||
import servicesRoutes from './api/services/services-routes';
|
||||
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
|
||||
import forensicsService from './tasks/lightning/forensics.service';
|
||||
import priceUpdater from './tasks/price-updater';
|
||||
@@ -46,6 +47,7 @@ import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
|
||||
import accelerationRoutes from './api/acceleration/acceleration.routes';
|
||||
import aboutRoutes from './api/about.routes';
|
||||
import mempoolBlocks from './api/mempool-blocks';
|
||||
import walletApi from './api/services/wallets';
|
||||
|
||||
class Server {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
@@ -236,6 +238,10 @@ class Server {
|
||||
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
|
||||
}
|
||||
indexer.$run();
|
||||
if (config.WALLETS.ENABLED) {
|
||||
// might take a while, so run in the background
|
||||
walletApi.$syncWallets();
|
||||
}
|
||||
if (config.FIAT_PRICE.ENABLED) {
|
||||
priceUpdater.$run();
|
||||
}
|
||||
@@ -333,6 +339,9 @@ class Server {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
accelerationRoutes.initRoutes(this.app);
|
||||
}
|
||||
if (config.WALLETS.ENABLED) {
|
||||
servicesRoutes.initRoutes(this.app);
|
||||
}
|
||||
if (!config.MEMPOOL.OFFICIAL) {
|
||||
aboutRoutes.initRoutes(this.app);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import config from './config';
|
||||
import auditReplicator from './replication/AuditReplication';
|
||||
import statisticsReplicator from './replication/StatisticsReplication';
|
||||
import AccelerationRepository from './repositories/AccelerationRepository';
|
||||
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
|
||||
|
||||
export interface CoreIndex {
|
||||
name: string;
|
||||
@@ -193,7 +192,6 @@ class Indexer {
|
||||
await auditReplicator.$sync();
|
||||
await statisticsReplicator.$sync();
|
||||
await AccelerationRepository.$indexPastAccelerations();
|
||||
await BlocksAuditsRepository.$migrateAuditsV0toV1();
|
||||
// do not wait for classify blocks to finish
|
||||
blocks.$classifyBlocks();
|
||||
} catch (e) {
|
||||
|
||||
@@ -29,11 +29,9 @@ export interface PoolStats extends PoolInfo {
|
||||
}
|
||||
|
||||
export interface BlockAudit {
|
||||
version: number,
|
||||
time: number,
|
||||
height: number,
|
||||
hash: string,
|
||||
unseenTxs: string[],
|
||||
missingTxs: string[],
|
||||
freshTxs: string[],
|
||||
sigopTxs: string[],
|
||||
@@ -128,7 +126,6 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
||||
acceleration?: boolean;
|
||||
acceleratedBy?: number[];
|
||||
acceleratedAt?: number;
|
||||
feeDelta?: number;
|
||||
replacement?: boolean;
|
||||
uid?: number;
|
||||
flags?: number;
|
||||
@@ -385,9 +382,8 @@ export interface CpfpCluster {
|
||||
}
|
||||
|
||||
export interface CpfpSummary {
|
||||
transactions: MempoolTransactionExtended[];
|
||||
transactions: TransactionExtended[];
|
||||
clusters: CpfpCluster[];
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface Statistic {
|
||||
@@ -453,7 +449,7 @@ export interface OptimizedStatistic {
|
||||
|
||||
export interface TxTrackingInfo {
|
||||
replacedBy?: string,
|
||||
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number },
|
||||
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number },
|
||||
cpfp?: {
|
||||
ancestors?: Ancestor[],
|
||||
bestDescendant?: Ancestor | null,
|
||||
@@ -466,7 +462,6 @@ export interface TxTrackingInfo {
|
||||
accelerated?: boolean,
|
||||
acceleratedBy?: number[],
|
||||
acceleratedAt?: number,
|
||||
feeDelta?: number,
|
||||
confirmed?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,11 @@ class AuditReplication {
|
||||
const missingAudits = await this.$getMissingAuditBlocks();
|
||||
|
||||
logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication');
|
||||
|
||||
|
||||
let totalSynced = 0;
|
||||
let totalMissed = 0;
|
||||
let loggerTimer = Date.now();
|
||||
// process missing audits in batches of BATCH_SIZE
|
||||
// process missing audits in batches of
|
||||
for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) {
|
||||
const slice = missingAudits.slice(i, i + BATCH_SIZE);
|
||||
const results = await Promise.all(slice.map(hash => this.$syncAudit(hash)));
|
||||
@@ -109,11 +109,9 @@ class AuditReplication {
|
||||
version: 1,
|
||||
});
|
||||
await blocksAuditsRepository.$saveAudit({
|
||||
version: auditSummary.version || 0,
|
||||
hash: blockHash,
|
||||
height: auditSummary.height,
|
||||
time: auditSummary.timestamp || auditSummary.time,
|
||||
unseenTxs: auditSummary.unseenTxs || [],
|
||||
missingTxs: auditSummary.missingTxs || [],
|
||||
addedTxs: auditSummary.addedTxs || [],
|
||||
prioritizedTxs: auditSummary.prioritizedTxs || [],
|
||||
|
||||
@@ -192,7 +192,6 @@ class AccelerationRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// modifies block transactions
|
||||
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
|
||||
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
|
||||
for (const tx of transactions) {
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
import blocks from '../api/blocks';
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||
import { BlockAudit, AuditScore, TransactionAudit, TransactionStripped } from '../mempool.interfaces';
|
||||
|
||||
interface MigrationAudit {
|
||||
version: number,
|
||||
height: number,
|
||||
id: string,
|
||||
timestamp: number,
|
||||
prioritizedTxs: string[],
|
||||
acceleratedTxs: string[],
|
||||
template: TransactionStripped[],
|
||||
transactions: TransactionStripped[],
|
||||
}
|
||||
import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces';
|
||||
|
||||
class BlocksAuditRepositories {
|
||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||
try {
|
||||
await DB.query(`INSERT INTO blocks_audits(version, time, height, hash, unseen_txs, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
|
||||
VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.version, audit.time, audit.height, audit.hash, JSON.stringify(audit.unseenTxs), JSON.stringify(audit.missingTxs),
|
||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
|
||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||
JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
|
||||
} catch (e: any) {
|
||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||
@@ -73,30 +62,24 @@ class BlocksAuditRepositories {
|
||||
public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(
|
||||
`SELECT
|
||||
blocks_audits.version,
|
||||
blocks_audits.height,
|
||||
blocks_audits.hash as id,
|
||||
UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
|
||||
template,
|
||||
unseen_txs as unseenTxs,
|
||||
missing_txs as missingTxs,
|
||||
added_txs as addedTxs,
|
||||
prioritized_txs as prioritizedTxs,
|
||||
fresh_txs as freshTxs,
|
||||
sigop_txs as sigopTxs,
|
||||
fullrbf_txs as fullrbfTxs,
|
||||
accelerated_txs as acceleratedTxs,
|
||||
match_rate as matchRate,
|
||||
expected_fees as expectedFees,
|
||||
expected_weight as expectedWeight
|
||||
`SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
|
||||
template,
|
||||
missing_txs as missingTxs,
|
||||
added_txs as addedTxs,
|
||||
prioritized_txs as prioritizedTxs,
|
||||
fresh_txs as freshTxs,
|
||||
sigop_txs as sigopTxs,
|
||||
fullrbf_txs as fullrbfTxs,
|
||||
accelerated_txs as acceleratedTxs,
|
||||
match_rate as matchRate,
|
||||
expected_fees as expectedFees,
|
||||
expected_weight as expectedWeight
|
||||
FROM blocks_audits
|
||||
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
||||
WHERE blocks_audits.hash = ?
|
||||
`, [hash]);
|
||||
|
||||
if (rows.length) {
|
||||
rows[0].unseenTxs = JSON.parse(rows[0].unseenTxs);
|
||||
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
||||
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
||||
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
|
||||
@@ -118,7 +101,7 @@ class BlocksAuditRepositories {
|
||||
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
|
||||
try {
|
||||
const blockAudit = await this.$getBlockAudit(hash);
|
||||
|
||||
|
||||
if (blockAudit) {
|
||||
const isAdded = blockAudit.addedTxs.includes(txid);
|
||||
const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
|
||||
@@ -132,17 +115,16 @@ class BlocksAuditRepositories {
|
||||
firstSeen = tx.time;
|
||||
}
|
||||
});
|
||||
const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
|
||||
|
||||
return {
|
||||
seen: wasSeen,
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
expected: isExpected,
|
||||
added: isAdded && (blockAudit.version === 0 || !wasSeen),
|
||||
added: isAdded,
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
firstSeen,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e: any) {
|
||||
@@ -204,96 +186,6 @@ class BlocksAuditRepositories {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [INDEXING] Migrate audits from v0 to v1
|
||||
*/
|
||||
public async $migrateAuditsV0toV1(): Promise<void> {
|
||||
try {
|
||||
let done = false;
|
||||
let processed = 0;
|
||||
let lastHeight;
|
||||
while (!done) {
|
||||
const [toMigrate]: MigrationAudit[][] = await DB.query(
|
||||
`SELECT
|
||||
blocks_audits.height as height,
|
||||
blocks_audits.hash as id,
|
||||
UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
|
||||
blocks_summaries.transactions as transactions,
|
||||
blocks_templates.template as template,
|
||||
blocks_audits.prioritized_txs as prioritizedTxs,
|
||||
blocks_audits.accelerated_txs as acceleratedTxs
|
||||
FROM blocks_audits
|
||||
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
|
||||
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
||||
WHERE blocks_audits.version = 0
|
||||
AND blocks_summaries.version = 2
|
||||
ORDER BY blocks_audits.height DESC
|
||||
LIMIT 100
|
||||
`) as any[];
|
||||
|
||||
if (toMigrate.length <= 0 || lastHeight === toMigrate[0].height) {
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
lastHeight = toMigrate[0].height;
|
||||
|
||||
logger.info(`migrating ${toMigrate.length} audits to version 1`);
|
||||
|
||||
for (const audit of toMigrate) {
|
||||
// unpack JSON-serialized transaction lists
|
||||
audit.transactions = JSON.parse((audit.transactions as any as string) || '[]');
|
||||
audit.template = JSON.parse((audit.template as any as string) || '[]');
|
||||
|
||||
// we know transactions in the template, or marked "prioritized" or "accelerated"
|
||||
// were seen in our mempool before the block was mined.
|
||||
const isSeen = new Set<string>();
|
||||
for (const tx of audit.template) {
|
||||
isSeen.add(tx.txid);
|
||||
}
|
||||
for (const txid of audit.prioritizedTxs) {
|
||||
isSeen.add(txid);
|
||||
}
|
||||
for (const txid of audit.acceleratedTxs) {
|
||||
isSeen.add(txid);
|
||||
}
|
||||
const unseenTxs = audit.transactions.slice(0).map(tx => tx.txid).filter(txid => !isSeen.has(txid));
|
||||
|
||||
// identify "prioritized" transactions
|
||||
const prioritizedTxs: string[] = [];
|
||||
let lastEffectiveRate = 0;
|
||||
// Iterate over the mined template from bottom to top (excluding the coinbase)
|
||||
// Transactions should appear in ascending order of mining priority.
|
||||
for (let i = audit.transactions.length - 1; i > 0; i--) {
|
||||
const blockTx = audit.transactions[i];
|
||||
// If a tx has a lower in-band effective fee rate than the previous tx,
|
||||
// it must have been prioritized out-of-band (in order to have a higher mining priority)
|
||||
// so exclude from the analysis.
|
||||
if ((blockTx.rate || 0) < lastEffectiveRate) {
|
||||
prioritizedTxs.push(blockTx.txid);
|
||||
} else {
|
||||
lastEffectiveRate = blockTx.rate || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Update audit in the database
|
||||
await DB.query(`
|
||||
UPDATE blocks_audits SET
|
||||
version = ?,
|
||||
unseen_txs = ?,
|
||||
prioritized_txs = ?
|
||||
WHERE hash = ?
|
||||
`, [1, JSON.stringify(unseenTxs), JSON.stringify(prioritizedTxs), audit.id]);
|
||||
}
|
||||
|
||||
processed += toMigrate.length;
|
||||
}
|
||||
|
||||
logger.info(`migrated ${processed} audits to version 1`);
|
||||
} catch (e: any) {
|
||||
logger.err(`Error while migrating audits from v0 to v1. Will try again later. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new BlocksAuditRepositories();
|
||||
|
||||
@@ -30,7 +30,7 @@ __MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false}
|
||||
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
|
||||
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
|
||||
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
|
||||
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true}
|
||||
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
|
||||
__MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
|
||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
||||
@@ -144,7 +144,7 @@ __REPLICATION_STATISTICS_START_TIME__=${REPLICATION_STATISTICS_START_TIME:=14819
|
||||
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
||||
|
||||
# MEMPOOL_SERVICES
|
||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
|
||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
|
||||
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||
|
||||
# REDIS
|
||||
|
||||
@@ -41,7 +41,7 @@ __TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__ACCELERATOR__=${ACCELERATOR:=false}
|
||||
__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true}
|
||||
__SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
|
||||
__SERVICES_API__=${SERVICES_API:=false}
|
||||
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
|
||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
||||
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
||||
|
||||
@@ -54,10 +54,6 @@
|
||||
"translation": "src/locale/messages.fr.xlf",
|
||||
"baseHref": "/fr/"
|
||||
},
|
||||
"hr": {
|
||||
"translation": "src/locale/messages.hr.xlf",
|
||||
"baseHref": "/hr/"
|
||||
},
|
||||
"ja": {
|
||||
"translation": "src/locale/messages.ja.xlf",
|
||||
"baseHref": "/ja/"
|
||||
|
||||
48
frontend/custom-bitb-config.json
Normal file
48
frontend/custom-bitb-config.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"theme": "wiz",
|
||||
"enterprise": "bitb",
|
||||
"branding": {
|
||||
"name": "bitb",
|
||||
"title": "BITB",
|
||||
"site_id": 20,
|
||||
"header_img": "/resources/bitblogo.svg",
|
||||
"footer_img": "/resources/bitblogo.svg"
|
||||
},
|
||||
"dashboard": {
|
||||
"widgets": [
|
||||
{
|
||||
"component": "fees",
|
||||
"mobileOrder": 4
|
||||
},
|
||||
{
|
||||
"component": "walletBalance",
|
||||
"mobileOrder": 1,
|
||||
"props": {
|
||||
"wallet": "BITB"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "goggles",
|
||||
"mobileOrder": 5
|
||||
},
|
||||
{
|
||||
"component": "wallet",
|
||||
"mobileOrder": 2,
|
||||
"props": {
|
||||
"wallet": "BITB",
|
||||
"period": "all"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "blocks"
|
||||
},
|
||||
{
|
||||
"component": "walletTransactions",
|
||||
"mobileOrder": 3,
|
||||
"props": {
|
||||
"wallet": "BITB"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -543,7 +543,16 @@ describe('Mainnet', () => {
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('.alert-replaced').should('be.visible');
|
||||
cy.get('.alert').should('be.visible');
|
||||
cy.get('.alert').invoke('css', 'width').then((alertWidth) => {
|
||||
cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth);
|
||||
});
|
||||
|
||||
cy.get('.btn-warning').then(getRectangle).then((rectA) => {
|
||||
cy.get('.alert').then(getRectangle).then((rectB) => {
|
||||
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows RBF transactions properly (desktop)', () => {
|
||||
|
||||
@@ -750,7 +750,7 @@
|
||||
},
|
||||
"backendInfo": {
|
||||
"hostname": "node205.tk7.mempool.space",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.0-dev",
|
||||
"gitCommit": "abbc8a134",
|
||||
"lightning": false
|
||||
},
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0-dev",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-frontend",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.0-dev",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@angular-devkit/build-angular": "^17.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0-dev",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
|
||||
@@ -9,7 +9,6 @@ import { StatusViewComponent } from './components/status-view/status-view.compon
|
||||
import { AddressGroupComponent } from './components/address-group/address-group.component';
|
||||
import { TrackerComponent } from './components/tracker/tracker.component';
|
||||
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
|
||||
import { TrackerGuard } from './route-guards';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
@@ -141,17 +140,16 @@ let routes: Routes = [
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
canMatch: [TrackerGuard],
|
||||
runGuardsAndResolvers: 'always',
|
||||
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'tracker',
|
||||
data: { networkSpecific: true },
|
||||
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
@@ -215,6 +213,10 @@ let routes: Routes = [
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
},
|
||||
];
|
||||
|
||||
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
@@ -299,16 +301,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (!window['isMempoolSpaceBuild']) {
|
||||
routes.push({
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
initialNavigation: 'enabledBlocking',
|
||||
|
||||
@@ -151,7 +151,7 @@ export const languages: Language[] = [
|
||||
{ code: 'fr', name: 'Français' }, // French
|
||||
// { code: 'gl', name: 'Galego' }, // Galician
|
||||
{ code: 'ko', name: '한국어' }, // Korean
|
||||
{ code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||
// { code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
||||
{ code: 'hi', name: 'हिन्दी' }, // Hindi
|
||||
{ code: 'ne', name: 'नेपाली' }, // Nepalese
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<span>Spiral</span>
|
||||
</a>
|
||||
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76" class="image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76">
|
||||
<defs>
|
||||
<style>
|
||||
.d {
|
||||
@@ -125,9 +125,7 @@
|
||||
<span>Blockstream</span>
|
||||
</a>
|
||||
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
||||
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68" class="image">
|
||||
<defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/>
|
||||
</svg>
|
||||
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
|
||||
<span>Unchained</span>
|
||||
</a>
|
||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||
@@ -152,7 +150,7 @@
|
||||
<span>Bull Bitcoin</span>
|
||||
</a>
|
||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
|
||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
||||
<g clip-path="url(#clip0_2_14)">
|
||||
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
|
||||
@@ -437,7 +435,7 @@
|
||||
Trademark Notice<br>
|
||||
</div>
|
||||
<p>
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
</p>
|
||||
<p>
|
||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
.image.not-rounded {
|
||||
border-radius: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
@@ -156,8 +158,9 @@
|
||||
margin: 40px 29px 10px;
|
||||
&.image.coldcard {
|
||||
border-radius: 0;
|
||||
height: auto;
|
||||
margin: 20px 29px 20px;
|
||||
width: auto;
|
||||
max-height: 50px;
|
||||
margin: 40px 29px 14px 29px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,29 +389,21 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
||||
@if (canPayWithCashapp || canPayWithApplePay) {
|
||||
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
|
||||
<p class="text-nowrap">—<span i18n="or">OR</span>—</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
||||
@if (canPayWithCashapp || canPayWithApplePay) {
|
||||
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
||||
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <app-fiat [value]="cost"></app-fiat> with</p>
|
||||
@if (canPayWithCashapp) {
|
||||
<img class="paymentMethod mx-2" style="width: 200px" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
|
||||
}
|
||||
@if (canPayWithApplePay) {
|
||||
@if (canPayWithCashapp) { <span class="mt-1 mb-1"></span> }
|
||||
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('applepay')">
|
||||
<img src="/resources/apple-pay.png" height=37>
|
||||
</div>
|
||||
}
|
||||
@if (canPayWithGooglePay) {
|
||||
@if (canPayWithCashapp || canPayWithApplePay) { <span class="mt-1 mb-1"></span> }
|
||||
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('googlepay')">
|
||||
<img src="/resources/google-pay.png" height=37>
|
||||
</div>
|
||||
@if (canPayWithCashapp) { <hr class="w-25 mt-2 mb-2"> }
|
||||
<img style="cursor: pointer;" src="/resources/apple-pay.svg" height=55 (click)="moveToStep('applepay')">
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -435,7 +427,7 @@
|
||||
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
|
||||
} @else if (step === 'cashapp' || step === 'applepay') {
|
||||
<!-- Show checkout page -->
|
||||
<div class="row mb-md-1 text-center" id="confirm-title">
|
||||
<div class="col-sm" id="confirm-payment-title">
|
||||
@@ -451,7 +443,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
|
||||
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay) {
|
||||
<div class="row text-center mt-1">
|
||||
<div class="col-sm">
|
||||
<div class="form-group w-100">
|
||||
@@ -471,13 +463,11 @@
|
||||
<div class="col-sm">
|
||||
<div class="form-group w-100">
|
||||
@if (step === 'applepay') {
|
||||
<div id="apple-pay-button" class="apple-pay-button apple-pay-button-black" style="height: 50px" [style]="loadingApplePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
<div id="apple-pay-button" class="apple-pay-button apple-pay-button-white" [style]="loadingApplePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
} @else if (step === 'cashapp') {
|
||||
<div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
} @else if (step === 'googlepay') {
|
||||
<div id="google-pay-button" class="d-inline-block" style="height: 50px" [style]="loadingGooglePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
<div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
}
|
||||
@if (loadingCashapp || loadingApplePay || loadingGooglePay) {
|
||||
@if (loadingCashapp || loadingApplePay) {
|
||||
<div display="d-flex flex-row justify-content-center">
|
||||
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
|
||||
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||
@@ -525,7 +515,7 @@
|
||||
<div class="col-sm">
|
||||
<div class="d-flex flex-row flex-column justify-content-center align-items-center">
|
||||
<span i18n="accelerator.confirming-acceleration-with-miners">Confirming your acceleration with our mining pool partners...</span>
|
||||
@if (timeSincePaid > 30000) {
|
||||
@if (timeSincePaid > 20000) {
|
||||
<span i18n="accelerator.confirming-acceleration-with-miners">...sorry, this is taking longer than expected...</span>
|
||||
}
|
||||
<div class="m-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
|
||||
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { md5, insecureRandomUUID } from '../../shared/common.utils';
|
||||
import { md5, nextRoundNumber, insecureRandomUUID } from '../../shared/common.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ETA, EtaService } from '../../services/eta.service';
|
||||
@@ -11,9 +10,8 @@ import { MiningStats } from '../../services/mining.service';
|
||||
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { isDevMode } from '@angular/core';
|
||||
|
||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
|
||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp';
|
||||
|
||||
export type AccelerationEstimate = {
|
||||
hasAccess: boolean;
|
||||
@@ -26,7 +24,7 @@ export type AccelerationEstimate = {
|
||||
mempoolBaseFee: number;
|
||||
vsizeFee: number;
|
||||
pools: number[];
|
||||
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
|
||||
availablePaymentMethods: {[method: string]: {min: number, max: number}};
|
||||
unavailable?: boolean;
|
||||
options: { // recommended bid options
|
||||
fee: number; // recommended userBid in sats
|
||||
@@ -49,7 +47,7 @@ export const MIN_BID_RATIO = 1;
|
||||
export const DEFAULT_BID_RATIO = 2;
|
||||
export const MAX_BID_RATIO = 4;
|
||||
|
||||
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success';
|
||||
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'processing' | 'paid' | 'success';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerate-checkout',
|
||||
@@ -64,7 +62,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
@Input() scrollEvent: boolean;
|
||||
@Input() cashappEnabled: boolean = true;
|
||||
@Input() applePayEnabled: boolean = false;
|
||||
@Input() googlePayEnabled: boolean = true;
|
||||
@Input() advancedEnabled: boolean = false;
|
||||
@Input() forceMobile: boolean = false;
|
||||
@Input() showDetails: boolean = false;
|
||||
@@ -83,22 +80,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
timePaid: number = 0; // time acceleration requested
|
||||
math = Math;
|
||||
isMobile: boolean = window.innerWidth <= 767.98;
|
||||
isProdDomain = ['mempool.space',
|
||||
'mempool-staging.va1.mempool.space',
|
||||
'mempool-staging.fmt.mempool.space',
|
||||
'mempool-staging.fra.mempool.space',
|
||||
'mempool-staging.tk7.mempool.space',
|
||||
'mempool-staging.sg1.mempool.space'
|
||||
].indexOf(document.location.hostname) > -1;
|
||||
|
||||
private _step: CheckoutStep = 'summary';
|
||||
simpleMode: boolean = true;
|
||||
paymentMethod: 'cashapp' | 'btcpay';
|
||||
timeoutTimer: any;
|
||||
|
||||
authSubscription$: Subscription;
|
||||
auth: IAuth | null = null;
|
||||
|
||||
// accelerator stuff
|
||||
square: { appId: string, locationId: string};
|
||||
accelerationUUID: string;
|
||||
accelerationSubscription: Subscription;
|
||||
difficultySubscription: Subscription;
|
||||
@@ -120,14 +112,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
// square
|
||||
loadingCashapp = false;
|
||||
loadingApplePay = false;
|
||||
loadingGooglePay = false;
|
||||
cashappError = false;
|
||||
cashappSubmit: any;
|
||||
payments: any;
|
||||
cashAppPay: any;
|
||||
applePay: any;
|
||||
googlePay: any;
|
||||
conversionsSubscription: Subscription;
|
||||
conversions: Record<string, number>;
|
||||
|
||||
conversions: any;
|
||||
|
||||
// btcpay
|
||||
loadingBtcpayInvoice = false;
|
||||
invoice = undefined;
|
||||
@@ -145,13 +137,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.accelerationUUID = insecureRandomUUID();
|
||||
|
||||
// Check if Apple Pay available
|
||||
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
|
||||
if (window['ApplePaySession']) {
|
||||
// @ts-ignore https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
|
||||
if (window.ApplePaySession) {
|
||||
this.applePayEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
ngOnInit() {
|
||||
this.authSubscription$ = this.authService.getAuth$().subscribe((auth) => {
|
||||
if (this.auth?.user?.userId !== auth?.user?.userId) {
|
||||
this.auth = auth;
|
||||
@@ -176,6 +168,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.moveToStep('summary');
|
||||
}
|
||||
|
||||
this.servicesApiService.setupSquare$().subscribe(ids => {
|
||||
this.square = {
|
||||
appId: ids.squareAppId,
|
||||
locationId: ids.squareLocationId
|
||||
};
|
||||
});
|
||||
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
@@ -183,7 +182,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
ngOnDestroy() {
|
||||
if (this.estimateSubscription) {
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
@@ -203,7 +202,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
moveToStep(step: CheckoutStep): void {
|
||||
moveToStep(step: CheckoutStep) {
|
||||
this._step = step;
|
||||
if (this.timeoutTimer) {
|
||||
clearTimeout(this.timeoutTimer);
|
||||
@@ -212,7 +211,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.fetchEstimate();
|
||||
}
|
||||
if (this._step === 'checkout') {
|
||||
this.insertSquare();
|
||||
this.enterpriseService.goal(8);
|
||||
}
|
||||
if (this._step === 'checkout' && this.canPayWithBitcoin) {
|
||||
@@ -222,14 +220,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.requestBTCPayInvoice();
|
||||
} else if (this._step === 'cashapp' && this.cashappEnabled) {
|
||||
this.loadingCashapp = true;
|
||||
this.insertSquare();
|
||||
this.setupSquare();
|
||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'applepay' && this.applePayEnabled) {
|
||||
this.loadingApplePay = true;
|
||||
this.setupSquare();
|
||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'googlepay' && this.googlePayEnabled) {
|
||||
this.loadingGooglePay = true;
|
||||
this.insertSquare();
|
||||
this.setupSquare();
|
||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'paid') {
|
||||
@@ -238,7 +234,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
if (this.step === 'paid') {
|
||||
this.accelerateError = 'internal_server_error';
|
||||
}
|
||||
}, 120000);
|
||||
}, 120000)
|
||||
}
|
||||
this.hasDetails.emit(this._step === 'quote');
|
||||
}
|
||||
@@ -256,7 +252,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.scrollToElement(id, position);
|
||||
}, timeout);
|
||||
}
|
||||
scrollToElement(id: string, position: ScrollLogicalPosition): void {
|
||||
scrollToElement(id: string, position: ScrollLogicalPosition) {
|
||||
const acceleratePreviewAnchor = document.getElementById(id);
|
||||
if (acceleratePreviewAnchor) {
|
||||
this.cd.markForCheck();
|
||||
@@ -271,7 +267,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Accelerator
|
||||
*/
|
||||
fetchEstimate(): void {
|
||||
fetchEstimate() {
|
||||
if (this.estimateSubscription) {
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
@@ -335,7 +331,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
}),
|
||||
|
||||
catchError(() => {
|
||||
catchError((response) => {
|
||||
this.estimate = undefined;
|
||||
this.quoteError = `cannot_accelerate_tx`;
|
||||
this.estimateSubscription.unsubscribe();
|
||||
@@ -406,70 +402,63 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
* Square
|
||||
*/
|
||||
insertSquare(): void {
|
||||
if (!this.isProdDomain && !isDevMode()) {
|
||||
return;
|
||||
}
|
||||
if (window['Square']) {
|
||||
//@ts-ignore
|
||||
if (window.Square) {
|
||||
return;
|
||||
}
|
||||
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
|
||||
if (this.isProdDomain) {
|
||||
statsUrl = '/square/v1/square.js';
|
||||
if (document.location.hostname === 'mempool-staging.fmt.mempool.space' ||
|
||||
document.location.hostname === 'mempool-staging.va1.mempool.space' ||
|
||||
document.location.hostname === 'mempool-staging.fra.mempool.space' ||
|
||||
document.location.hostname === 'mempool-staging.tk7.mempool.space' ||
|
||||
document.location.hostname === 'mempool.space') {
|
||||
statsUrl = 'https://web.squarecdn.com/v1/square.js';
|
||||
}
|
||||
|
||||
(function(): void {
|
||||
(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);
|
||||
})();
|
||||
}
|
||||
setupSquare(): void {
|
||||
if (!this.isProdDomain && !isDevMode()) {
|
||||
return;
|
||||
}
|
||||
const init = (): void => {
|
||||
setupSquare() {
|
||||
const init = () => {
|
||||
this.initSquare();
|
||||
};
|
||||
|
||||
if (!window['Square']) {
|
||||
console.debug('Square.js failed to load properly. Retrying.');
|
||||
setTimeout(this.setupSquare.bind(this), 100);
|
||||
//@ts-ignore
|
||||
if (!window.Square) {
|
||||
console.debug('Square.js failed to load properly. Retrying in 1 second.');
|
||||
setTimeout(init, 1000);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}
|
||||
async initSquare(): Promise<void> {
|
||||
try {
|
||||
this.servicesApiService.setupSquare$().subscribe({
|
||||
next: async (ids) => {
|
||||
this.payments = window['Square'].payments(ids.squareAppId, ids.squareLocationId);
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (this._step === 'cashapp' || urlParams.get('cash_request_id')) {
|
||||
await this.requestCashAppPayment();
|
||||
} else if (this._step === 'applepay') {
|
||||
await this.requestApplePayPayment();
|
||||
} else if (this._step === 'googlepay') {
|
||||
await this.requestGooglePayPayment();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
console.debug('Error loading Square Payments');
|
||||
this.accelerateError = 'cannot_setup_square';
|
||||
}
|
||||
});
|
||||
//@ts-ignore
|
||||
this.payments = window.Square.payments(this.square.appId, this.square.locationId)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (this._step === 'cashapp' || urlParams.get('cash_request_id')) {
|
||||
await this.requestCashAppPayment();
|
||||
} else if (this._step === 'applepay') {
|
||||
await this.requestApplePayPayment();
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Error loading Square Payments', e);
|
||||
this.accelerateError = 'cannot_setup_square';
|
||||
this.cashappError = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* APPLE PAY
|
||||
*/
|
||||
async requestApplePayPayment(): Promise<void> {
|
||||
async requestApplePayPayment() {
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
@@ -516,7 +505,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.applePay) {
|
||||
this.applePay.destroy();
|
||||
@@ -553,101 +541,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GOOGLE PAY
|
||||
*/
|
||||
async requestGooglePayPayment(): Promise<void> {
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
if (this.googlePay) {
|
||||
this.googlePay.destroy();
|
||||
}
|
||||
|
||||
const costUSD = this.cost / 100_000_000 * conversions.USD;
|
||||
const paymentRequest = this.payments.paymentRequest({
|
||||
countryCode: 'US',
|
||||
currencyCode: 'USD',
|
||||
total: {
|
||||
amount: costUSD.toFixed(2),
|
||||
label: 'Total'
|
||||
}
|
||||
});
|
||||
this.googlePay = await this.payments.googlePay(paymentRequest , {
|
||||
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
});
|
||||
|
||||
await this.googlePay.attach(`#google-pay-button`, {
|
||||
buttonType: 'pay',
|
||||
buttonSizeMode: 'fill',
|
||||
});
|
||||
this.loadingGooglePay = false;
|
||||
|
||||
document.getElementById('google-pay-button').addEventListener('click', async event => {
|
||||
event.preventDefault();
|
||||
const tokenResult = await this.googlePay.tokenize();
|
||||
if (tokenResult?.status === 'OK') {
|
||||
const card = tokenResult.details?.card;
|
||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||
console.error(`Cannot retreive payment card details`);
|
||||
this.accelerateError = 'apple_pay_no_card_details';
|
||||
return;
|
||||
}
|
||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||
this.servicesApiService.accelerateWithGooglePay$(
|
||||
this.tx.txid,
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.googlePay) {
|
||||
this.googlePay.destroy();
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.moveToStep('paid');
|
||||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
// Reset everything by reloading the page :D, can be improved
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||
if (tokenResult.errors) {
|
||||
errorMessage += ` and errors: ${JSON.stringify(
|
||||
tokenResult.errors,
|
||||
)}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* CASHAPP
|
||||
*/
|
||||
async requestCashAppPayment(): Promise<void> {
|
||||
async requestCashAppPayment() {
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
@@ -664,15 +565,19 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
amount: costUSD.toFixed(2),
|
||||
label: 'Total',
|
||||
pending: true,
|
||||
productUrl: `${redirectHostname}/tx/${this.tx.txid}`,
|
||||
}
|
||||
productUrl: `${redirectHostname}/tracker/${this.tx.txid}`,
|
||||
},
|
||||
button: { shape: 'semiround', size: 'small', theme: 'light'}
|
||||
});
|
||||
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
|
||||
redirectURL: `${redirectHostname}/tx/${this.tx.txid}`,
|
||||
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`
|
||||
redirectURL: `${redirectHostname}/tracker/${this.tx.txid}`,
|
||||
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
button: { shape: 'semiround', size: 'small', theme: 'light'}
|
||||
});
|
||||
|
||||
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'dark' });
|
||||
if (this.step === 'cashapp') {
|
||||
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' })
|
||||
}
|
||||
this.loadingCashapp = false;
|
||||
|
||||
this.cashAppPay.addEventListener('ontokenization', event => {
|
||||
@@ -721,7 +626,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
/**
|
||||
* BTCPay
|
||||
*/
|
||||
async requestBTCPayInvoice(): Promise<void> {
|
||||
async requestBTCPayInvoice() {
|
||||
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
|
||||
switchMap(response => {
|
||||
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
|
||||
@@ -751,61 +656,54 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
/**
|
||||
* UI events
|
||||
*/
|
||||
selectedOptionChanged(event): void {
|
||||
selectedOptionChanged(event) {
|
||||
this.selectedOption = event.target.id;
|
||||
}
|
||||
|
||||
get step(): CheckoutStep {
|
||||
get step() {
|
||||
return this._step;
|
||||
}
|
||||
|
||||
get paymentMethods(): PaymentMethod[] {
|
||||
return Object.keys(this.estimate?.availablePaymentMethods || {}) as PaymentMethod[];
|
||||
get paymentMethods() {
|
||||
return Object.keys(this.estimate?.availablePaymentMethods || {});
|
||||
}
|
||||
|
||||
get couldPayWithBitcoin(): boolean {
|
||||
get couldPayWithBitcoin() {
|
||||
return !!this.estimate?.availablePaymentMethods?.bitcoin;
|
||||
}
|
||||
|
||||
get couldPayWithCashapp(): boolean {
|
||||
get couldPayWithCashapp() {
|
||||
if (!this.cashappEnabled) {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.cashapp;
|
||||
}
|
||||
|
||||
get couldPayWithApplePay(): boolean {
|
||||
get couldPayWithApplePay() {
|
||||
if (!this.applePayEnabled) {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.applePay;
|
||||
}
|
||||
|
||||
get couldPayWithGooglePay(): boolean {
|
||||
if (!this.googlePayEnabled) {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.googlePay;
|
||||
}
|
||||
|
||||
get couldPayWithBalance(): boolean {
|
||||
get couldPayWithBalance() {
|
||||
if (!this.hasAccessToBalanceMode) {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.balance;
|
||||
}
|
||||
|
||||
get couldPay(): boolean {
|
||||
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay || this.couldPayWithGooglePay;
|
||||
get couldPay() {
|
||||
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay;
|
||||
}
|
||||
|
||||
get canPayWithBitcoin(): boolean {
|
||||
get canPayWithBitcoin() {
|
||||
const paymentMethod = this.estimate?.availablePaymentMethods?.bitcoin;
|
||||
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max;
|
||||
}
|
||||
|
||||
get canPayWithCashapp(): boolean {
|
||||
if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
|
||||
get canPayWithCashapp() {
|
||||
if (!this.cashappEnabled || !this.conversions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -820,8 +718,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
get canPayWithApplePay(): boolean {
|
||||
if (!this.applePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
|
||||
get canPayWithApplePay() {
|
||||
if (!this.applePayEnabled || !this.conversions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -836,23 +734,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
get canPayWithGooglePay(): boolean {
|
||||
if (!this.googlePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const paymentMethod = this.estimate?.availablePaymentMethods?.googlePay;
|
||||
if (paymentMethod) {
|
||||
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
|
||||
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get canPayWithBalance(): boolean {
|
||||
get canPayWithBalance() {
|
||||
if (!this.hasAccessToBalanceMode) {
|
||||
return false;
|
||||
}
|
||||
@@ -860,11 +742,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max && this.cost <= this.estimate?.userBalance;
|
||||
}
|
||||
|
||||
get canPay(): boolean {
|
||||
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay || this.canPayWithGooglePay;
|
||||
get canPay() {
|
||||
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay;
|
||||
}
|
||||
|
||||
get hasAccessToBalanceMode(): boolean {
|
||||
get hasAccessToBalanceMode() {
|
||||
return this.isLoggedIn() && this.estimate?.hasAccess;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<div
|
||||
#tooltip
|
||||
*ngIf="accelerationInfo && tooltipPosition !== null"
|
||||
class="acceleration-tooltip"
|
||||
[style.left]="tooltipPosition.x + 'px'"
|
||||
[style.top]="tooltipPosition.y + 'px'"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label" i18n="transaction.status|Transaction Status">Status</td>
|
||||
<td class="value">
|
||||
@if (accelerationInfo.status === 'seen') {
|
||||
<span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span>
|
||||
} @else if (accelerationInfo.status === 'accelerated') {
|
||||
<span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
|
||||
} @else if (accelerationInfo.status === 'mined') {
|
||||
<span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="accelerationInfo.fee">
|
||||
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
|
||||
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
|
||||
@if (accelerationInfo.status === 'accelerated') {
|
||||
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
} @else {
|
||||
<td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
}
|
||||
</tr>
|
||||
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
|
||||
@if (accelerationInfo.status === 'seen') {
|
||||
<td class="label" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||
} @else if (accelerationInfo.status === 'accelerated' || accelerationInfo.status === 'mined') {
|
||||
<td class="label" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
|
||||
@if (accelerationInfo.status === 'accelerated') {
|
||||
<td class="value oobFees"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.feeDelta || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||
} @else {
|
||||
<td class="value oobFees"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.bidBoost || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||
}
|
||||
}
|
||||
</tr>
|
||||
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
|
||||
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
|
||||
<td class="value" *ngIf="accelerationInfo.pools">
|
||||
<ng-container *ngFor="let pool of accelerationInfo.pools">
|
||||
<img *ngIf="accelerationInfo.poolsData[pool]"
|
||||
class="pool-logo"
|
||||
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
|
||||
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'"
|
||||
[alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||
</ng-container>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,39 +0,0 @@
|
||||
.acceleration-tooltip {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
|
||||
color: var(--tooltip-grey);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 10px 15px;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
|
||||
.badge.badge-accelerated {
|
||||
background-color: var(--tertiary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.oobFees {
|
||||
color: #905cf4;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-timeline-tooltip',
|
||||
templateUrl: './acceleration-timeline-tooltip.component.html',
|
||||
styleUrls: ['./acceleration-timeline-tooltip.component.scss'],
|
||||
})
|
||||
export class AccelerationTimelineTooltipComponent implements OnChanges {
|
||||
@Input() accelerationInfo: any;
|
||||
@Input() cursorPosition: { x: number, y: number };
|
||||
|
||||
tooltipPosition: any = null;
|
||||
|
||||
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
|
||||
let x = Math.max(10, changes.cursorPosition.currentValue.x - 50);
|
||||
let y = changes.cursorPosition.currentValue.y + 20;
|
||||
if (this.tooltipElement) {
|
||||
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
|
||||
if ((x + elementBounds.width) > (window.innerWidth - 10)) {
|
||||
x = Math.max(0, window.innerWidth - elementBounds.width - 10);
|
||||
}
|
||||
if (y + elementBounds.height > (window.innerHeight - 20)) {
|
||||
y = y - elementBounds.height - 20;
|
||||
}
|
||||
}
|
||||
this.tooltipPosition = { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
hasPoolsData(): boolean {
|
||||
return Object.keys(this.accelerationInfo.poolsData).length > 0;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="node" [id]="'confirmed'">
|
||||
<div class="acc-to-confirmed left go-faster"></div>
|
||||
<div class="shape-border waiting">
|
||||
<div class="shape"></div>
|
||||
<div class="shape animate"></div>
|
||||
</div>
|
||||
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@
|
||||
<div class="nodes">
|
||||
<div class="node" [id]="'first-seen'">
|
||||
<div class="seen-to-acc right"></div>
|
||||
<div class="shape-border hovering" (pointerover)="onHover($event, 'seen');" (pointerout)="onBlur($event);">
|
||||
<div class="shape-border">
|
||||
<div class="shape"></div>
|
||||
</div>
|
||||
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
|
||||
@@ -80,7 +80,7 @@
|
||||
} @else {
|
||||
<div class="seen-to-acc right"></div>
|
||||
}
|
||||
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
|
||||
<div class="shape-border">
|
||||
<div class="shape"></div>
|
||||
@if (!tx.status.confirmed) {
|
||||
<div class="connector down loading"></div>
|
||||
@@ -89,14 +89,14 @@
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
|
||||
}
|
||||
<div class="time" [class.no-margin]="!tx.status.confirmed" [class.offset-left]="!tx.status.confirmed">
|
||||
<div class="time offset-left" [class.no-margin]="!tx.status.confirmed">
|
||||
@if (!tx.status.confirmed) {
|
||||
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
|
||||
}
|
||||
@if (useAbsoluteTime) {
|
||||
<span>{{ acceleratedAt * 1000 | date }}</span>
|
||||
} @else {
|
||||
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed"></app-time>
|
||||
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="true"></app-time>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,10 +113,7 @@
|
||||
} @else {
|
||||
<div class="seen-to-acc left"></div>
|
||||
}
|
||||
<div class="shape-border"
|
||||
[ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}"
|
||||
(pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)"
|
||||
(pointerout)="onBlur($event);">
|
||||
<div class="shape-border" [class.waiting]="!tx.status.confirmed">
|
||||
<div class="shape"></div>
|
||||
</div>
|
||||
@if (tx.status.confirmed) {
|
||||
@@ -133,10 +130,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-acceleration-timeline-tooltip
|
||||
[accelerationInfo]="hoverInfo"
|
||||
[cursorPosition]="tooltipPosition"
|
||||
></app-acceleration-timeline-tooltip>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -152,16 +152,9 @@
|
||||
margin-bottom: -8px;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
transition: background-color 300ms, padding 300ms;
|
||||
|
||||
&.hovering {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: relative;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
|
||||
import { Component, Input, OnInit, OnChanges } from '@angular/core';
|
||||
import { ETA } from '../../services/eta.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface';
|
||||
import { MiningService } from '../../services/mining.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-timeline',
|
||||
@@ -12,7 +10,6 @@ import { MiningService } from '../../services/mining.service';
|
||||
export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
@Input() transactionTime: number;
|
||||
@Input() tx: Transaction;
|
||||
@Input() accelerationInfo: Acceleration;
|
||||
@Input() eta: ETA;
|
||||
// A mined transaction has standard ETA and accelerated ETA undefined
|
||||
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
|
||||
@@ -25,25 +22,13 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
useAbsoluteTime: boolean = false;
|
||||
interval: number;
|
||||
|
||||
tooltipPosition = null;
|
||||
hoverInfo: any = null;
|
||||
poolsData: { [id: number]: SinglePoolStats } = {};
|
||||
|
||||
constructor(
|
||||
private miningService: MiningService,
|
||||
) {}
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||
|
||||
this.miningService.getPools().subscribe(pools => {
|
||||
for (const pool of pools) {
|
||||
this.poolsData[pool.unique_id] = pool;
|
||||
}
|
||||
});
|
||||
|
||||
this.interval = window.setInterval(() => {
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||
@@ -67,42 +52,4 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
ngOnDestroy(): void {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
onHover(event, status: string): void {
|
||||
if (status === 'seen') {
|
||||
this.hoverInfo = {
|
||||
status,
|
||||
fee: this.tx.fee,
|
||||
weight: this.tx.weight
|
||||
};
|
||||
} else if (status === 'accelerated') {
|
||||
this.hoverInfo = {
|
||||
status,
|
||||
fee: this.accelerationInfo?.effectiveFee || this.tx.fee,
|
||||
weight: this.tx.weight,
|
||||
feeDelta: this.accelerationInfo?.feeDelta || this.tx.feeDelta,
|
||||
pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
|
||||
poolsData: this.poolsData
|
||||
};
|
||||
} else if (status === 'mined') {
|
||||
this.hoverInfo = {
|
||||
status,
|
||||
fee: this.accelerationInfo?.effectiveFee,
|
||||
weight: this.tx.weight,
|
||||
bidBoost: this.accelerationInfo?.bidBoost,
|
||||
minedByPoolUniqueId: this.accelerationInfo?.minedByPoolUniqueId,
|
||||
pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
|
||||
poolsData: this.poolsData
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onBlur(event): void {
|
||||
this.hoverInfo = null;
|
||||
}
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="acceleration-list">
|
||||
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
|
||||
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations">
|
||||
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed">
|
||||
<thead>
|
||||
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<ng-container *ngIf="pending">
|
||||
@@ -21,7 +21,7 @@
|
||||
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
|
||||
</ng-container>
|
||||
</thead>
|
||||
<tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let acceleration of accelerations; let i= index;">
|
||||
<td class="txid text-left">
|
||||
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
|
||||
@@ -62,9 +62,8 @@
|
||||
</td>
|
||||
<td class="status text-right">
|
||||
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
|
||||
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span>
|
||||
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span>
|
||||
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</ng-container><span *ngIf="acceleration.status === 'failed_provisional'"> ⌛</span></span>
|
||||
<span *ngIf="acceleration.status.includes('completed')" class="badge badge-success" i18n="">Completed <span *ngIf="acceleration.status === 'completed_provisional'">🔄</span></span>
|
||||
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger" i18n="accelerator.canceled">Failed <span *ngIf="acceleration.status === 'failed_provisional'">🔄</span></span>
|
||||
</td>
|
||||
<td class="date text-right" *ngIf="!this.widget">
|
||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||
@@ -73,47 +72,22 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
<ng-template #skeleton>
|
||||
@if (!pending) {
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="txid text-left">
|
||||
<span class="skeleton-loader" style="max-width: 200px"></span>
|
||||
</td>
|
||||
<td class="fee text-right">
|
||||
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||
</td>
|
||||
<td class="block text-right">
|
||||
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||
</td>
|
||||
<td class="pool text-right" *ngIf="!this.widget">
|
||||
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||
</td>
|
||||
<td class="status text-right">
|
||||
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||
</td>
|
||||
<td class="date text-right" *ngIf="!this.widget">
|
||||
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
} @else {
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="txid text-left">
|
||||
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||
</td>
|
||||
<td class="fee-rate text-right">
|
||||
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||
</td>
|
||||
<td class="bid text-right">
|
||||
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||
</td>
|
||||
<td class="time text-right">
|
||||
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
}
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="txid text-left">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="fee text-right">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="fee-delta text-right">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="status text-right">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -85,8 +85,8 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
|
||||
@@ -32,7 +32,6 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
dir: 'rtl' | 'ltr' = 'ltr';
|
||||
paramSubscription: Subscription;
|
||||
pools: { [id: number]: SinglePoolStats } = {};
|
||||
nonEmptyAccelerations: boolean = true;
|
||||
|
||||
constructor(
|
||||
private servicesApiService: ServicesApiServices,
|
||||
@@ -51,12 +50,6 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.miningService.getPools().subscribe(pools => {
|
||||
for (const pool of pools) {
|
||||
this.pools[pool.unique_id] = pool;
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.widget) {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
|
||||
@@ -89,6 +82,12 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
).subscribe(() => {
|
||||
this.pageChange(this.page);
|
||||
});
|
||||
|
||||
this.miningService.getMiningStats('1m').subscribe(stats => {
|
||||
for (const pool of stats.pools) {
|
||||
this.pools[pool.poolUniqueId] = pool;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
|
||||
@@ -116,7 +115,6 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
for (const acc of accelerations) {
|
||||
acc.boost = acc.boostCost != null ? acc.boostCost : acc.bidBoost;
|
||||
}
|
||||
this.nonEmptyAccelerations = accelerations.length > 0;
|
||||
if (this.widget) {
|
||||
return of(accelerations.slice(0, 6));
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@if (chartOnly) {
|
||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||
} @else {
|
||||
<table style="width: 100%;">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
|
||||
@@ -11,9 +11,9 @@
|
||||
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
|
||||
<div class="effective-fee-container">
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
|
||||
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
} @else {
|
||||
<app-fee-rate class="oobFees" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -61,8 +61,4 @@
|
||||
& > div, & > div > svg {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.oobFees {
|
||||
color: #905cf4;
|
||||
}
|
||||
@@ -67,17 +67,12 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
|
||||
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
|
||||
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
|
||||
// Find the first pool with at least 1% of the total network hashrate
|
||||
const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100);
|
||||
const numSignificantPools = acceleratingPools.length - firstSignificantPool;
|
||||
acceleratingPools.forEach((poolId, index) => {
|
||||
const pool = pools[poolId];
|
||||
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
|
||||
data.push(getDataItem(
|
||||
pool.lastEstimatedHashrate,
|
||||
index >= firstSignificantPool
|
||||
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
|
||||
: 'white',
|
||||
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * .08)),
|
||||
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
|
||||
true,
|
||||
) as PieSeriesOption);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<div class="sparkles" #sparkleAnchor>
|
||||
<div *ngFor="let sparkle of sparkles" class="sparkle" [style]="sparkle.style">
|
||||
<span class="inner-sparkle" [style]="sparkle.rotation">+</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,45 +0,0 @@
|
||||
.sparkles {
|
||||
position: absolute;
|
||||
top: var(--block-size);
|
||||
height: 50px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sparkle {
|
||||
position: absolute;
|
||||
color: rgba(152, 88, 255, 0.75);
|
||||
opacity: 0;
|
||||
transform: scale(0.8) rotate(0deg);
|
||||
animation: pop ease 2000ms forwards, sparkle ease 500ms infinite;
|
||||
}
|
||||
|
||||
.inner-sparkle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% {
|
||||
transform: scale(0.8) rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
transform: scale(1) rotate(72deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0) rotate(360deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sparkle {
|
||||
0% {
|
||||
color: rgba(152, 88, 255, 0.75);
|
||||
}
|
||||
50% {
|
||||
color: rgba(198, 162, 255, 0.75);
|
||||
}
|
||||
100% {
|
||||
color: rgba(152, 88, 255, 0.75);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-sparkles',
|
||||
templateUrl: './acceleration-sparkles.component.html',
|
||||
styleUrls: ['./acceleration-sparkles.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AccelerationSparklesComponent implements OnChanges {
|
||||
@Input() arrow: ElementRef<HTMLDivElement>;
|
||||
@Input() run: boolean = false;
|
||||
|
||||
@ViewChild('sparkleAnchor')
|
||||
sparkleAnchor: ElementRef<HTMLDivElement>;
|
||||
|
||||
constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
endTimeout: any;
|
||||
lastSparkle: number = 0;
|
||||
sparkleWidth: number = 0;
|
||||
sparkles: any[] = [];
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.run) {
|
||||
if (this.endTimeout) {
|
||||
clearTimeout(this.endTimeout);
|
||||
this.endTimeout = null;
|
||||
}
|
||||
if (this.run) {
|
||||
this.doSparkle();
|
||||
} else {
|
||||
this.endTimeout = setTimeout(() => {
|
||||
this.sparkles = [];
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doSparkle(): void {
|
||||
if (this.run) {
|
||||
const now = performance.now();
|
||||
if (now - this.lastSparkle > 20) {
|
||||
this.lastSparkle = now;
|
||||
if (this.arrow?.nativeElement && this.sparkleAnchor?.nativeElement) {
|
||||
const anchor = this.sparkleAnchor.nativeElement.getBoundingClientRect().right;
|
||||
const right = this.arrow.nativeElement.getBoundingClientRect().right;
|
||||
const dx = (anchor - right) + 30;
|
||||
const numSparkles = Math.ceil(Math.random() * 3);
|
||||
for (let i = 0; i < numSparkles; i++) {
|
||||
this.sparkles.push({
|
||||
style: {
|
||||
right: (dx + (Math.random() * 10)) + 'px',
|
||||
top: (15 + (Math.random() * 30)) + 'px',
|
||||
},
|
||||
rotation: {
|
||||
transform: `rotate(${Math.random() * 360}deg)`,
|
||||
}
|
||||
});
|
||||
}
|
||||
while (this.sparkles.length > 200) {
|
||||
this.sparkles.shift();
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
this.doSparkle();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ const periodSeconds = {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -83,7 +83,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.isLoading = true;
|
||||
if (!this.address || !this.stats) {
|
||||
if (!this.addressSummary$ && (!this.address || !this.stats)) {
|
||||
return;
|
||||
}
|
||||
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
|
||||
@@ -144,15 +144,16 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
prepareChartOptions(summary: AddressTxSummary[]) {
|
||||
if (!summary || !this.stats) {
|
||||
if (!summary) {
|
||||
return;
|
||||
}
|
||||
|
||||
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
|
||||
const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
|
||||
let runningTotal = total;
|
||||
const processData = summary.map(d => {
|
||||
const balance = total;
|
||||
const fiatBalance = total * d.price / 100_000_000;
|
||||
total -= d.value;
|
||||
const balance = runningTotal;
|
||||
const fiatBalance = runningTotal * d.price / 100_000_000;
|
||||
runningTotal -= d.value;
|
||||
return {
|
||||
time: d.time * 1000,
|
||||
balance,
|
||||
@@ -172,7 +173,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
this.fiatData = this.fiatData.filter(d => d[0] >= startFiat);
|
||||
}
|
||||
this.data.push(
|
||||
{value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }}
|
||||
{value: [now, total], symbol: 'none', tooltip: { show: false }}
|
||||
);
|
||||
|
||||
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount></td>
|
||||
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" [digitsInfo]="getAmountDigits(transaction.value)" [noFiat]="true"></app-amount></td>
|
||||
<td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td>
|
||||
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td>
|
||||
</tr>
|
||||
|
||||
@@ -43,7 +43,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
|
||||
|
||||
startAddressSubscription(): void {
|
||||
this.isLoading = true;
|
||||
if (!this.address || !this.addressInfo) {
|
||||
if (!this.addressSummary$ && (!this.address || !this.addressInfo)) {
|
||||
return;
|
||||
}
|
||||
this.transactions$ = (this.addressSummary$ || (this.isPubkey
|
||||
@@ -55,7 +55,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
|
||||
})
|
||||
)).pipe(
|
||||
map(summary => {
|
||||
return summary?.slice(0, 6);
|
||||
return summary?.filter(tx => Math.abs(tx.value) >= 1000000)?.slice(0, 6);
|
||||
}),
|
||||
switchMap(txs => {
|
||||
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe(
|
||||
@@ -68,6 +68,12 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
|
||||
))));
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
getAmountDigits(value: number): string {
|
||||
const decimals = Math.max(0, 4 - Math.ceil(Math.log10(Math.abs(value / 100_000_000))));
|
||||
return `1.${decimals}-${decimals}`;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
|
||||
<div class="card-text">
|
||||
{{ ((addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
|
||||
{{ ((total) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
|
||||
</div>
|
||||
<div class="symbol">
|
||||
<app-fiat [value]="(addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum)"></app-fiat>
|
||||
<app-fiat [value]="(total)"></app-fiat>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
|
||||
@@ -19,6 +19,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
|
||||
isLoading: boolean = true;
|
||||
error: any;
|
||||
|
||||
total: number = 0;
|
||||
delta7d: number = 0;
|
||||
delta30d: number = 0;
|
||||
|
||||
@@ -34,7 +35,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.isLoading = true;
|
||||
if (!this.address || !this.addressInfo) {
|
||||
if (!this.addressSummary$ && (!this.address || !this.addressInfo)) {
|
||||
return;
|
||||
}
|
||||
(this.addressSummary$ || (this.isPubkey
|
||||
@@ -57,6 +58,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
|
||||
calculateStats(summary: AddressTxSummary[]): void {
|
||||
let weekTotal = 0;
|
||||
let monthTotal = 0;
|
||||
this.total = this.addressInfo ? this.addressInfo.chain_stats.funded_txo_sum - this.addressInfo.chain_stats.spent_txo_sum : summary.reduce((acc, tx) => acc + tx.value, 0);
|
||||
|
||||
const weekAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (7 * 24 * 60 * 60 * 1000)).getTime()) / 1000;
|
||||
const monthAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (30 * 24 * 60 * 60 * 1000)).getTime()) / 1000;
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<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 float-right" 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>
|
||||
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
|
||||
</a>
|
||||
<div class="filter-bar">
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
float: right;
|
||||
|
||||
&:hover, &:active {
|
||||
text-decoration: none;
|
||||
|
||||
@@ -21,7 +21,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
<div class="graph-alignment" [class.grid-align]="!autofit" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'">
|
||||
<div class="block-overview-graph">
|
||||
<canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
|
||||
@if (!disableSpinner) {
|
||||
<div class="loader-wrapper" [class.hidden]="!isLoading && !unavailable">
|
||||
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
|
||||
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
|
||||
</div>
|
||||
}
|
||||
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
|
||||
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
|
||||
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
|
||||
</div>
|
||||
<app-block-overview-tooltip
|
||||
[tx]="selectedTx || hoverTx"
|
||||
[cursorPosition]="tooltipPosition"
|
||||
|
||||
@@ -18,7 +18,6 @@ const unmatchedAuditColors = {
|
||||
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
|
||||
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
|
||||
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
|
||||
added_prioritized: setOpacity(defaultAuditColors.added_prioritized, unmatchedOpacity),
|
||||
prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
|
||||
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
|
||||
};
|
||||
@@ -26,7 +25,6 @@ const unmatchedContrastAuditColors = {
|
||||
censored: setOpacity(contrastAuditColors.censored, unmatchedOpacity),
|
||||
missing: setOpacity(contrastAuditColors.missing, unmatchedOpacity),
|
||||
added: setOpacity(contrastAuditColors.added, unmatchedOpacity),
|
||||
added_prioritized: setOpacity(contrastAuditColors.added_prioritized, unmatchedOpacity),
|
||||
prioritized: setOpacity(contrastAuditColors.prioritized, unmatchedOpacity),
|
||||
accelerated: setOpacity(contrastAuditColors.accelerated, unmatchedOpacity),
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped {
|
||||
flags: number;
|
||||
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
||||
time?: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ export const defaultAuditColors = {
|
||||
censored: hexToColor('f344df'),
|
||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||
added: hexToColor('0099ff'),
|
||||
added_prioritized: darken(desaturate(hexToColor('0099ff'), 0.15), 0.85),
|
||||
prioritized: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||
accelerated: hexToColor('8f5ff6'),
|
||||
};
|
||||
@@ -102,7 +101,6 @@ export const contrastAuditColors = {
|
||||
censored: hexToColor('ffa8ff'),
|
||||
missing: darken(desaturate(hexToColor('ffa8ff'), 0.3), 0.7),
|
||||
added: hexToColor('00bb98'),
|
||||
added_prioritized: darken(desaturate(hexToColor('00bb98'), 0.15), 0.85),
|
||||
prioritized: darken(desaturate(hexToColor('00bb98'), 0.3), 0.7),
|
||||
accelerated: hexToColor('8f5ff6'),
|
||||
};
|
||||
@@ -138,8 +136,6 @@ export function defaultColorFunction(
|
||||
return auditColors.missing;
|
||||
case 'added':
|
||||
return auditColors.added;
|
||||
case 'added_prioritized':
|
||||
return auditColors.added_prioritized;
|
||||
case 'prioritized':
|
||||
return auditColors.prioritized;
|
||||
case 'selected':
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<tr *ngIf="hasEffectiveRate && effectiveRate != null">
|
||||
<td *ngIf="!this.acceleration" class="label" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td *ngIf="this.acceleration" class="label" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
|
||||
<td class="value" [class.oobFees]="this.acceleration">
|
||||
<td class="value">
|
||||
<app-fee-rate [fee]="effectiveRate"></app-fee-rate>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -75,10 +75,6 @@
|
||||
<span *ngSwitchCase="'freshcpfp'" class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span>
|
||||
<span *ngSwitchCase="'added'" class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
|
||||
<span *ngSwitchCase="'prioritized'" class="badge badge-warning" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
|
||||
<ng-container *ngSwitchCase="'added_prioritized'">
|
||||
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
|
||||
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
|
||||
</ng-container>
|
||||
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
|
||||
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
|
||||
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
|
||||
|
||||
@@ -27,9 +27,6 @@ th, td {
|
||||
width: 70%;
|
||||
text-align: end;
|
||||
}
|
||||
&.oobFees {
|
||||
color: #905cf4;
|
||||
}
|
||||
}
|
||||
|
||||
.badge.badge-accelerated {
|
||||
|
||||
@@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -521,7 +521,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
if (transactions && blockAudit) {
|
||||
const inTemplate = {};
|
||||
const inBlock = {};
|
||||
const isUnseen = {};
|
||||
const isAdded = {};
|
||||
const isPrioritized = {};
|
||||
const isCensored = {};
|
||||
@@ -544,9 +543,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
for (const tx of transactions) {
|
||||
inBlock[tx.txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.unseenTxs || []) {
|
||||
isUnseen[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.addedTxs) {
|
||||
isAdded[txid] = true;
|
||||
}
|
||||
@@ -596,27 +592,18 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
tx.status = 'accelerated';
|
||||
}
|
||||
}
|
||||
let anySeen = false;
|
||||
for (let index = transactions.length - 1; index >= 0; index--) {
|
||||
const tx = transactions[index];
|
||||
for (const [index, tx] of transactions.entries()) {
|
||||
tx.context = 'actual';
|
||||
if (index === 0) {
|
||||
tx.status = null;
|
||||
} else if (isPrioritized[tx.txid]) {
|
||||
if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) {
|
||||
tx.status = 'added_prioritized';
|
||||
} else {
|
||||
tx.status = 'prioritized';
|
||||
}
|
||||
} else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) {
|
||||
} else if (isAdded[tx.txid]) {
|
||||
tx.status = 'added';
|
||||
} else if (isPrioritized[tx.txid]) {
|
||||
tx.status = 'prioritized';
|
||||
} else if (inTemplate[tx.txid]) {
|
||||
anySeen = true;
|
||||
tx.status = 'found';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else if (isUnseen[tx.txid] && anySeen) {
|
||||
tx.status = 'added';
|
||||
} else {
|
||||
tx.status = 'selected';
|
||||
isSelected[tx.txid] = true;
|
||||
|
||||
@@ -257,6 +257,36 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@case ('walletBalance') {
|
||||
<div class="col card-wrapper" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="main-title" i18n="dashboard.treasury">Treasury</div>
|
||||
<app-balance-widget [addressSummary$]="walletSummary$"></app-balance-widget>
|
||||
</div>
|
||||
}
|
||||
@case ('wallet') {
|
||||
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body">
|
||||
<span class="title-link">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.balance-history">Balance History</h5>
|
||||
</span>
|
||||
<app-address-graph [addressSummary$]="walletSummary$" [period]="widget.props.period || 'all'" [widget]="true" [height]="graphHeight"></app-address-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@case ('walletTransactions') {
|
||||
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<span class="title-link">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
|
||||
</span>
|
||||
<app-address-transactions-widget [addressSummary$]="walletSummary$"></app-address-transactions-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@case ('twitter') {
|
||||
<div class="col" style="min-height:410px" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="card graph-card">
|
||||
|
||||
@@ -62,8 +62,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
|
||||
widgets;
|
||||
|
||||
addressSubscription: Subscription;
|
||||
walletSubscription: Subscription;
|
||||
blockTxSubscription: Subscription;
|
||||
addressSummary$: Observable<AddressTxSummary[]>;
|
||||
walletSummary$: Observable<AddressTxSummary[]>;
|
||||
address: Address;
|
||||
|
||||
goggleResolution = 82;
|
||||
@@ -107,6 +109,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
|
||||
this.websocketService.stopTrackingAddress();
|
||||
this.address = null;
|
||||
}
|
||||
if (this.walletSubscription) {
|
||||
this.walletSubscription.unsubscribe();
|
||||
this.websocketService.stopTrackingWallet();
|
||||
}
|
||||
this.destroy$.next(1);
|
||||
this.destroy$.complete();
|
||||
}
|
||||
@@ -260,6 +266,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
|
||||
});
|
||||
|
||||
this.startAddressSubscription();
|
||||
this.startWalletSubscription();
|
||||
}
|
||||
|
||||
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
|
||||
@@ -358,6 +365,51 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
|
||||
}
|
||||
}
|
||||
|
||||
startWalletSubscription(): void {
|
||||
if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.wallet)) {
|
||||
const walletName = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.wallet).props.wallet;
|
||||
this.websocketService.startTrackingWallet(walletName);
|
||||
|
||||
this.walletSummary$ = this.apiService.getWallet$(walletName).pipe(
|
||||
catchError(e => {
|
||||
return of(null);
|
||||
}),
|
||||
map((walletTransactions) => {
|
||||
const transactions = Object.values(walletTransactions).flatMap(wallet => wallet.transactions);
|
||||
return this.deduplicateWalletTransactions(transactions);
|
||||
}),
|
||||
switchMap(initial => this.stateService.walletTransactions$.pipe(
|
||||
startWith(null),
|
||||
scan((summary, walletTransactions) => {
|
||||
if (walletTransactions) {
|
||||
const transactions: AddressTxSummary[] = [...summary, ...Object.values(walletTransactions).flat()];
|
||||
return this.deduplicateWalletTransactions(transactions);
|
||||
}
|
||||
return summary;
|
||||
}, initial)
|
||||
)),
|
||||
share(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
|
||||
const transactions = new Map<string, AddressTxSummary>();
|
||||
for (const tx of walletTransactions) {
|
||||
if (transactions.has(tx.txid)) {
|
||||
transactions.get(tx.txid).value += tx.value;
|
||||
} else {
|
||||
transactions.set(tx.txid, tx);
|
||||
}
|
||||
}
|
||||
return Array.from(transactions.values()).sort((a, b) => {
|
||||
if (a.height === b.height) {
|
||||
return b.tx_position - a.tx_position;
|
||||
}
|
||||
return b.height - a.height;
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 992) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
.loadingGraphs.widget {
|
||||
top: 75%;
|
||||
|
||||
@@ -28,7 +28,7 @@ interface Hashrate {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -17,7 +17,7 @@ const OUTLIERS_MEDIAN_MULTIPLIER = 4;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
templateUrl: './lbtc-pegs-graph.component.html',
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet4'] || '/testnet4')" ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet4'] || '/testnet4')" ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a>
|
||||
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
||||
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
.sticky-loading {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
font-size: 14px;
|
||||
@media (width >= 992px) {
|
||||
left: 32px;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<img [src]="enterpriseInfo.img" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
|
||||
}
|
||||
@if (enterpriseInfo?.header_img) {
|
||||
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="60px" class="mr-3">
|
||||
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" [alt]="enterpriseInfo.title" height="60px" class="mr-3">
|
||||
} @else {
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo" style="width: 200px; height: 50px"></app-svg-images>
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 200px; height: 50px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<a class="navbar-brand d-none d-md-flex" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
@if (enterpriseInfo?.header_img) {
|
||||
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="48px" class="mr-3">
|
||||
<img [src]="enterpriseInfo?.header_img" [alt]="enterpriseInfo.title" height="48px" class="mr-3">
|
||||
} @else {
|
||||
<ng-template [ngIf]="subdomain && enterpriseInfo">
|
||||
<div class="subdomain_container">
|
||||
@@ -39,7 +39,7 @@
|
||||
<!-- Mobile -->
|
||||
<a class="navbar-brand d-flex d-md-none justify-content-center" [ngClass]="{'dual-logos': subdomain, 'mr-0': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
|
||||
@if (enterpriseInfo?.header_img) {
|
||||
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="42px">
|
||||
<img [src]="enterpriseInfo?.header_img" [alt]="enterpriseInfo.title" height="42px">
|
||||
} @else {
|
||||
<ng-template [ngIf]="subdomain && enterpriseInfo">
|
||||
<div class="subdomain_container">
|
||||
@@ -49,7 +49,7 @@
|
||||
</ng-template>
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
@if (enterpriseInfo?.header_img) {
|
||||
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px">
|
||||
<img [src]="enterpriseInfo?.header_img" [alt]="enterpriseInfo.title" height="36px">
|
||||
} @else {
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images>
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
@@ -70,7 +70,7 @@
|
||||
<a ngbDropdownItem *ngIf="env.MAINNET_ENABLED" class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a>
|
||||
<a ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet4" [class.active]="network.val === 'testnet4'" [routerLink]="networkPaths['testnet4'] || '/testnet4'"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4</a>
|
||||
<a ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet4" [class.active]="network.val === 'testnet4'" [routerLink]="networkPaths['testnet4'] || '/testnet4'"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||
</div>
|
||||
|
||||
@@ -51,8 +51,7 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<app-acceleration-sparkles [style]="{ position: 'absolute', right: 0}" [arrow]="arrowElement" [run]="acceleratingArrow"></app-acceleration-sparkles>
|
||||
<div *ngIf="arrowVisible" #arrowUp id="arrow-up" [ngStyle]="{'right': rightPosition + (blockWidth * 0.3) + containerOffset + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div>
|
||||
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + (blockWidth * 0.3) + containerOffset + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
|
||||
import { Subscription, Observable, of, combineLatest } from 'rxjs';
|
||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
@@ -77,9 +77,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
maxArrowPosition = 0;
|
||||
rightPosition = 0;
|
||||
transition = 'background 2s, right 2s, transform 1s';
|
||||
@ViewChild('arrowUp')
|
||||
arrowElement: ElementRef<HTMLDivElement>;
|
||||
acceleratingArrow: boolean = false;
|
||||
|
||||
markIndex: number;
|
||||
txPosition: MempoolPosition;
|
||||
@@ -204,7 +201,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
this.markBlocksSubscription = this.stateService.markBlock$
|
||||
.subscribe((state) => {
|
||||
const oldTxPosition = this.txPosition;
|
||||
this.markIndex = undefined;
|
||||
this.txPosition = undefined;
|
||||
this.txFeePerVSize = undefined;
|
||||
@@ -213,12 +209,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
if (state.mempoolPosition) {
|
||||
this.txPosition = state.mempoolPosition;
|
||||
if (this.txPosition.accelerated && !oldTxPosition?.accelerated) {
|
||||
this.acceleratingArrow = true;
|
||||
setTimeout(() => {
|
||||
this.acceleratingArrow = false;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
if (state.txFeePerVSize) {
|
||||
this.txFeePerVSize = state.txFeePerVSize;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/grap
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.pool-distribution {
|
||||
|
||||
@@ -167,7 +167,7 @@ div.scrollable {
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
top: 475px;
|
||||
@media (max-width: 992px) {
|
||||
top: 600px;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<br><br>
|
||||
|
||||
<h2>Privacy Policy</h2>
|
||||
<h6>Updated: July 31, 2024</h6>
|
||||
<h6>Updated: July 10, 2024</h6>
|
||||
|
||||
<br><br>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<br>
|
||||
|
||||
<h4>USING THIS WEBSITE</h4>
|
||||
<h4>General</h4>
|
||||
|
||||
<p *ngIf="officialMempoolSpace">Out of respect for the Bitcoin community, this Website does not use any third-party analytics, third-party trackers, or third-party cookies, and we do not share any private user data with third-parties. Additionally, to mitigate the risk of surveillance by malicious third-parties, we self-host this Website on our own hardware and network infrastructure, so there are no "hosting companies" or "cloud providers" involved with the operation of this Website.</p>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li>We use basic webserver logging (nginx) for sysadmin purposes, which collects your IP address along with the requests you make. These logs are deleted after 10 days, and we do not share this data with any third-party.</li>
|
||||
<li>We use basic webserver logging (nginx) for sysadmin purposes, which collects your IP address along with the requests you make. These logs are deleted after 10 days, and we do not share this data with any third-party. To conceal your IP address from our webserver logs, we recommend that you use Tor Browser with our Tor v3 hidden service onion hostname.</li>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
<p *ngIf="officialMempoolSpace">If you use Mempool Accelerator™ your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
|
||||
|
||||
<p *ngIf="!officialMempoolSpace">When using Mempool Accelerator™ the mempool.space privacy policy will apply: <a href="https://mempool.space/privacy-policy">https://mempool.space/privacy-policy</a>.</p>
|
||||
<p *ngIf="!officialMempoolSpace">If you click the accelerate button on a transaction you will load acceleration pricing information from Mempool. If you make an acceleration request, the TXID and your maximum bid will be sent to Mempool who will store and share this information with their mining pool partners, and publicly display accelerated transaction details on mempool.space and via Mempool's APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -74,38 +74,22 @@
|
||||
<br>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
|
||||
|
||||
<h4>PAYMENTS AND DONATIONS</h4>
|
||||
|
||||
<p>If you make any payment to Mempool or donation to The Mempool Open Source Project®, we may collect the following:</p>
|
||||
<p>If you make any payment to Mempool or donation to The Mempool Open Source Project®, we may collect the following:</p>
|
||||
|
||||
<ul>
|
||||
<ul>
|
||||
|
||||
<li>Your e-mail address and/or country; we may use this information to manage your user account, for billing purposes, or to update you about our services. We will not share this with any third-party, except as necessary for our fiat payment processor.</li>
|
||||
<li>Your e-mail address and/or country; we may use this information to manage your user account, for billing purposes, or to update you about our services. We will not share this with any third-party, except as necessary for our fiat payment processor.</li>
|
||||
|
||||
<li>If you make a payment using Bitcoin, we will process your payment using our self-hosted BTCPay Server instance. We will not share your payment details with any third-party. For payments made over the Lightning network, we may utilize third party LSPs / lightning liquidity providers.</li>
|
||||
<li>If you make a payment using Bitcoin, we will process your payment using our self-hosted BTCPay Server instance. We will not share your payment details with any third-party. For payments made over the Lightning network, we may utilize third party LSPs / lightning liquidity providers.</li>
|
||||
|
||||
<li>If you make a payment using Fiat we will collect your payment details. We will share your payment details with our fiat payment processor Square (Block, Inc.) - Please see "Information we collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy.</li>
|
||||
<li>If you make a payment using Fiat we will collect your payment details. We will share your payment details with our fiat payment processor Square (Block, Inc.),. - Please see "Information we collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy.</li>
|
||||
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
<br>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
|
||||
<h4>DATA RETENTION AND ACCOUNT INACTIVITY</h4>
|
||||
|
||||
<p>We aim to retain your data only as long as necessary:</p>
|
||||
<ul>
|
||||
<li>An account is considered inactive if all of the following conditions are met: a) No login activity within the past 6 months, b) No active subscriptions associated with the account, c) No Mempool Accelerator™ Pro account credit</li>
|
||||
<li>If an account meets the criteria for inactivity as defined above, we will automatically delete the associated account data after a period of 6 months of continuous inactivity, except in the case of payment disputes or account irregularities.</li>
|
||||
</ul>
|
||||
|
||||
</ng-container>
|
||||
<br>
|
||||
|
||||
<p>EOF</p>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="nav-header">
|
||||
@if (enterpriseInfo?.header_img) {
|
||||
<a class="d-flex" [routerLink]="['/' | relativeUrl]">
|
||||
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="42px">
|
||||
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" [alt]="enterpriseInfo.title" height="42px">
|
||||
</a>
|
||||
} @else if (enterpriseInfo?.img || enterpriseInfo?.imageMd5) {
|
||||
<a [routerLink]="['/' | relativeUrl]">
|
||||
@@ -46,7 +46,7 @@
|
||||
@if (replaced) {
|
||||
<div class="alert-replaced" role="alert">
|
||||
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
|
||||
<app-truncate [text]="latestReplacement" [lastChars]="12" [link]="['/tx/' | relativeUrl, latestReplacement]" [queryParams]="{mode: 'status'}"></app-truncate>
|
||||
<app-truncate [text]="latestReplacement" [lastChars]="12" [link]="['/tracker/' | relativeUrl, latestReplacement]"></app-truncate>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="tracker-bar">
|
||||
@@ -61,7 +61,7 @@
|
||||
@if (transactionTime > 0) {
|
||||
<i><app-time kind="since" [time]="transactionTime" [fastRender]="true" [showTooltip]="true"></app-time></i>
|
||||
} @else {
|
||||
<span class="skeleton-loader" style="max-width: 200px;"></span>
|
||||
<span class="skeleton-loader" style="max-width: 50%;"></span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,7 +78,7 @@
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-template #etaSkeleton>
|
||||
<span class="skeleton-loader" style="max-width: 200px;"></span>
|
||||
<span class="skeleton-loader"></span>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,7 +118,7 @@
|
||||
</div>
|
||||
<span class="explainer"> </span>
|
||||
} @else {
|
||||
@if (tx && !tx.status?.confirmed && showAccelerationSummary) {
|
||||
@if (!tx.status?.confirmed && showAccelerationSummary) {
|
||||
<ng-container *ngIf="(ETA$ | async) as eta;">
|
||||
<app-accelerate-checkout
|
||||
*ngIf="(da$ | async) as da;"
|
||||
@@ -135,7 +135,7 @@
|
||||
></app-accelerate-checkout>
|
||||
</ng-container>
|
||||
}
|
||||
<div class="status-panel d-flex flex-column h-100 w-100 justify-content-center align-items-center" [class.small-status]="tx && !tx.status?.confirmed && showAccelerationSummary">
|
||||
<div class="status-panel d-flex flex-column h-100 w-100 justify-content-center align-items-center" [class.small-status]="!tx.status?.confirmed && showAccelerationSummary">
|
||||
@if (tx?.acceleration && !tx.status?.confirmed) {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'wand-magic-sparkles']" [fixedWidth]="true"></fa-icon>
|
||||
@@ -185,11 +185,7 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="footer-link"
|
||||
[routerLink]="['/tx' | relativeUrl, tx?.txid || txId]"
|
||||
[queryParams]="{ mode: 'details' }"
|
||||
queryParamsHandling="merge"
|
||||
>
|
||||
<div class="footer-link" [routerLink]="['/tx' | relativeUrl, tx?.txid]">
|
||||
<span><ng-container i18n="accelerator.show-more-details">See more details</ng-container> <fa-icon [icon]="['fas', 'arrow-alt-circle-right']"></fa-icon></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
position: relative;
|
||||
background: var(--nav-bg);
|
||||
box-shadow: 0 -5px 15px #000;
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user