Compare commits
242 Commits
v3.0.0-alp
...
v3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
617789926b | ||
|
|
001c596d14 | ||
|
|
f0af1703da | ||
|
|
5452d7f524 | ||
|
|
ff9e2456b9 | ||
|
|
4e581347c8 | ||
|
|
820777236e | ||
|
|
beeb5eb08c | ||
|
|
b78aca0282 | ||
|
|
9572f2d554 | ||
|
|
ef13596b59 | ||
|
|
80da024bbb | ||
|
|
f75f85f914 | ||
|
|
b3ac107b0b | ||
|
|
f8cedaa7a3 | ||
|
|
72bb92dd8b | ||
|
|
e3c4e219f3 | ||
|
|
aa3fa4478a | ||
|
|
26c03eee88 | ||
|
|
db10ab9aae | ||
|
|
2ee7b9531a | ||
|
|
5f6af83944 | ||
|
|
8d2204a53f | ||
|
|
96bec279a9 | ||
|
|
5178ae43f6 | ||
|
|
ca26154426 | ||
|
|
021f0b32a1 | ||
|
|
b8cfeb579b | ||
|
|
fc5b99f93f | ||
|
|
ce4b0ed0f3 | ||
|
|
a31729b8b8 | ||
|
|
79e494150c | ||
|
|
b1a43abc0e | ||
|
|
3e50a3c9e7 | ||
|
|
132d6204c3 | ||
|
|
77c6ad5576 | ||
|
|
4d35845c18 | ||
|
|
3d8a4a85f7 | ||
|
|
1545347a45 | ||
|
|
9facf28ba5 | ||
|
|
d6eb98561b | ||
|
|
0f688e8347 | ||
|
|
7f53741a7b | ||
|
|
d5672691e1 | ||
|
|
e6049c707b | ||
|
|
91e74e769c | ||
|
|
7f252f06b7 | ||
|
|
1b9d3f669d | ||
|
|
ef0ba9a77a | ||
|
|
15f10736e2 | ||
|
|
924399df46 | ||
|
|
cddff129b3 | ||
|
|
7c90e8ae06 | ||
|
|
34d996c7cb | ||
|
|
641a2ae3ae | ||
|
|
11a849ef28 | ||
|
|
7b0347e846 | ||
|
|
bb1352ed58 | ||
|
|
fa6456b92c | ||
|
|
bfea19238b | ||
|
|
36b91cfdfd | ||
|
|
7b56212064 | ||
|
|
d4be3c2c4c | ||
|
|
3a9f06f651 | ||
|
|
e652eb339d | ||
|
|
96435c329f | ||
|
|
1c69613d65 | ||
|
|
3707763e30 | ||
|
|
b6ce8229f0 | ||
|
|
b62ae9b6f6 | ||
|
|
f61ace2f92 | ||
|
|
2b572f2494 | ||
|
|
c0e4c1efe1 | ||
|
|
8078caaa89 | ||
|
|
5eb117165f | ||
|
|
a2dcf0d545 | ||
|
|
212d58f917 | ||
|
|
d1eec80afb | ||
|
|
05c6709926 | ||
|
|
f1a48db9ee | ||
|
|
76ce43d289 | ||
|
|
51f5b728f3 | ||
|
|
8fa1863aff | ||
|
|
e3d1d9c1c0 | ||
|
|
f2f8d91e10 | ||
|
|
11ef090846 | ||
|
|
5cacd2635e | ||
|
|
1b4780c25b | ||
|
|
5ea44f2e7d | ||
|
|
261c794817 | ||
|
|
de9fae5cd7 | ||
|
|
439c52af30 | ||
|
|
4dbcd6ca18 | ||
|
|
2921c94520 | ||
|
|
ab4a258be3 | ||
|
|
21b0d50947 | ||
|
|
233ec112c2 | ||
|
|
6f0a5c9b44 | ||
|
|
2bc243b115 | ||
|
|
154f8e65a7 | ||
|
|
cc9855aa65 | ||
|
|
7b45d922bc | ||
|
|
9488ca50a3 | ||
|
|
9f559248cc | ||
|
|
067aac4d06 | ||
|
|
37eb17cc22 | ||
|
|
c7382a1c6c | ||
|
|
f782693b26 | ||
|
|
908988d06e | ||
|
|
a0c21e4120 | ||
|
|
bd96cbd701 | ||
|
|
9cccdcf70f | ||
|
|
2489fc4902 | ||
|
|
e378df4158 | ||
|
|
fa5f758875 | ||
|
|
a46f15e7ec | ||
|
|
46ecd2a51f | ||
|
|
c1aaf2f61a | ||
|
|
d2b57c8d4f | ||
|
|
f27a5700fa | ||
|
|
17522cca6e | ||
|
|
3f2581886d | ||
|
|
c5beee3a40 | ||
|
|
94e223e8da | ||
|
|
416e1bb4cb | ||
|
|
564dc08509 | ||
|
|
b9334c93f5 | ||
|
|
67761230e3 | ||
|
|
7cc01af631 | ||
|
|
96e2e6060b | ||
|
|
0723778e7c | ||
|
|
39d7e4ac3e | ||
|
|
1869368a49 | ||
|
|
b83f19f186 | ||
|
|
b248aad6d9 | ||
|
|
87c9f920c1 | ||
|
|
ea0e339d60 | ||
|
|
0a0b5e52fe | ||
|
|
5314f35974 | ||
|
|
87d2f6cf90 | ||
|
|
4266bdcbb6 | ||
|
|
73e68534f1 | ||
|
|
ee5b383a46 | ||
|
|
fa4b445dca | ||
|
|
06eaadd517 | ||
|
|
ebdc1dbf6d | ||
|
|
184903670a | ||
|
|
878561244a | ||
|
|
a0e9b33199 | ||
|
|
0a4513c8fb | ||
|
|
77b9277da5 | ||
|
|
009fac183f | ||
|
|
83a6df4d04 | ||
|
|
474d384b0c | ||
|
|
5870782abf | ||
|
|
fb335f62db | ||
|
|
346ef0028d | ||
|
|
916cae2a8f | ||
|
|
550e667a2f | ||
|
|
eb4ebf6786 | ||
|
|
7515348997 | ||
|
|
41441bb958 | ||
|
|
98c49918f7 | ||
|
|
5855e09663 | ||
|
|
558b876d47 | ||
|
|
fb63af5070 | ||
|
|
d02a2ccf65 | ||
|
|
e3bb812203 | ||
|
|
e3af4ea61c | ||
|
|
7d10b32861 | ||
|
|
adea897e93 | ||
|
|
17ec50dba0 | ||
|
|
53a36d042f | ||
|
|
7531a53b2e | ||
|
|
d59bc085e5 | ||
|
|
5d37e08c64 | ||
|
|
b5d89b83fa | ||
|
|
1245673575 | ||
|
|
62a72755c7 | ||
|
|
5b8ec8925a | ||
|
|
262866c15b | ||
|
|
aec7eb57c2 | ||
|
|
8d3b4733f5 | ||
|
|
3e4debdf7a | ||
|
|
b719b76999 | ||
|
|
6adbda5185 | ||
|
|
01311d0ba1 | ||
|
|
6081daacef | ||
|
|
54c9970386 | ||
|
|
1e4a599055 | ||
|
|
b051861d61 | ||
|
|
3c7deafffd | ||
|
|
845123fc63 | ||
|
|
d3e3650cac | ||
|
|
008cc385da | ||
|
|
817a6bef6e | ||
|
|
8dd8fe5fb1 | ||
|
|
c734a81f08 | ||
|
|
a0f4c260ab | ||
|
|
000a989055 | ||
|
|
90331e2c1b | ||
|
|
78ac0137b3 | ||
|
|
3d9133c47e | ||
|
|
481859bc8f | ||
|
|
811feec145 | ||
|
|
b3e59c06e9 | ||
|
|
67d44e3d6f | ||
|
|
ca4b1943a8 | ||
|
|
df0f244bd1 | ||
|
|
fe1ad86885 | ||
|
|
aee2454a98 | ||
|
|
5c814d9c22 | ||
|
|
2c5d7fbc9f | ||
|
|
570f7841ce | ||
|
|
117c066425 | ||
|
|
96f9f66e7f | ||
|
|
ce2742ff9c | ||
|
|
4547a2757c | ||
|
|
4d44ee55fc | ||
|
|
29875e0095 | ||
|
|
bc498733fc | ||
|
|
dc09e75783 | ||
|
|
544261eafe | ||
|
|
58c0c060d5 | ||
|
|
af7a962a0b | ||
|
|
7b3cc6372b | ||
|
|
b49a6c4cac | ||
|
|
b0db348605 | ||
|
|
b1aa4f50bd | ||
|
|
301f1821ae | ||
|
|
b9a053387f | ||
|
|
82c271267a | ||
|
|
4ef4e5b98a | ||
|
|
a7be59df3e | ||
|
|
82360f5525 | ||
|
|
8762ccaa09 | ||
|
|
3f7a24fb52 | ||
|
|
09b09710e4 | ||
|
|
08d3beed72 | ||
|
|
920f225e6c | ||
|
|
8c07e3c31a | ||
|
|
84ba721407 |
12
LICENSE
12
LICENSE
@@ -1,5 +1,5 @@
|
||||
The Mempool Open Source Project®
|
||||
Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders
|
||||
Copyright (c) 2019-2024 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,10 +12,12 @@ 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 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.
|
||||
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.
|
||||
|
||||
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": false,
|
||||
"RUST_GBT": true,
|
||||
"LIMIT_GBT": false,
|
||||
"CPFP_INDEXING": false,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||
|
||||
734
backend/package-lock.json
generated
734
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.0-dev",
|
||||
"version": "3.0.1",
|
||||
"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.24.0",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@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.10.0",
|
||||
"mysql2": "~3.11.0",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"redis": "^4.6.6",
|
||||
"redis": "^4.7.0",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@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', async (req, res) => {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username/:md5', 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,20 +6,22 @@ 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(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 } {
|
||||
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 } {
|
||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||
return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
||||
return { unseen: [], 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 prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool
|
||||
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 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;
|
||||
@@ -32,6 +34,7 @@ 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
|
||||
@@ -113,11 +116,16 @@ class Audit {
|
||||
} else {
|
||||
if (rbfCache.has(tx.txid)) {
|
||||
rbf.push(tx.txid);
|
||||
} else if (!isDisplaced[tx.txid]) {
|
||||
if (!mempool[tx.txid] && !rbfCache.getReplacedBy(tx.txid)) {
|
||||
unseen.push(tx.txid);
|
||||
}
|
||||
} else {
|
||||
if (mempool[tx.txid]) {
|
||||
prioritized.push(tx.txid);
|
||||
if (isDisplaced[tx.txid]) {
|
||||
added.push(tx.txid);
|
||||
}
|
||||
} else {
|
||||
added.push(tx.txid);
|
||||
unseen.push(tx.txid);
|
||||
}
|
||||
}
|
||||
overflowWeight += tx.weight;
|
||||
@@ -125,6 +133,24 @@ 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;
|
||||
@@ -165,6 +191,7 @@ class Audit {
|
||||
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
|
||||
|
||||
return {
|
||||
unseen,
|
||||
censored: Object.keys(isCensored),
|
||||
added,
|
||||
prioritized,
|
||||
|
||||
@@ -165,6 +165,7 @@ class BitcoinRoutes {
|
||||
acceleration: tx.acceleration,
|
||||
acceleratedBy: tx.acceleratedBy || undefined,
|
||||
acceleratedAt: tx.acceleratedAt || undefined,
|
||||
feeDelta: tx.feeDelta || undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ 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[] = [];
|
||||
@@ -439,7 +440,7 @@ class Blocks {
|
||||
|
||||
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendMempoolTransaction(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
|
||||
@@ -904,7 +905,12 @@ class Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, Object.values(mempool.getAccelerations()).map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
||||
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 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}`);
|
||||
@@ -927,12 +933,12 @@ class Blocks {
|
||||
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
||||
this.blocks.push(newBlock);
|
||||
this.updateTimerProgress(timer, `reindexed block`);
|
||||
let cpfpSummary;
|
||||
let newCpfpSummary;
|
||||
if (config.MEMPOOL.CPFP_INDEXING) {
|
||||
cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
|
||||
newCpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
|
||||
this.updateTimerProgress(timer, `reindexed block cpfp`);
|
||||
}
|
||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height);
|
||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true, newCpfpSummary, newBlock.height);
|
||||
this.updateTimerProgress(timer, `reindexed block summary`);
|
||||
}
|
||||
await mining.$indexDifficultyAdjustments();
|
||||
@@ -981,7 +987,7 @@ class Blocks {
|
||||
|
||||
// start async callbacks
|
||||
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
|
||||
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
|
||||
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, cpfpSummary.transactions));
|
||||
|
||||
if (block.height % 2016 === 0) {
|
||||
if (Common.indexingEnabled()) {
|
||||
@@ -1178,7 +1184,7 @@ class Blocks {
|
||||
};
|
||||
}),
|
||||
};
|
||||
summaryVersion = 1;
|
||||
summaryVersion = cpfpSummary.version;
|
||||
} else {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
@@ -1397,11 +1403,11 @@ class Blocks {
|
||||
return this.currentBlockHeight;
|
||||
}
|
||||
|
||||
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
|
||||
public async $indexCPFP(hash: string, height: number, txs?: MempoolTransactionExtended[]): Promise<CpfpSummary | null> {
|
||||
let transactions = txs;
|
||||
if (!transactions) {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
|
||||
}
|
||||
if (!transactions) {
|
||||
const block = await bitcoinClient.getBlock(hash, 2);
|
||||
@@ -1413,7 +1419,7 @@ class Blocks {
|
||||
}
|
||||
|
||||
if (transactions?.length != null) {
|
||||
const summary = calculateFastBlockCpfp(height, transactions as TransactionExtended[]);
|
||||
const summary = calculateFastBlockCpfp(height, transactions);
|
||||
|
||||
await this.$saveCpfp(hash, height, summary);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { Request } from 'express';
|
||||
import { CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
|
||||
import { 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: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
|
||||
export function calculateFastBlockCpfp(height: number, transactions: MempoolTransactionExtended[], 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,6 +93,7 @@ export function calculateFastBlockCpfp(height: number, transactions: Transaction
|
||||
return {
|
||||
transactions,
|
||||
clusters,
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -159,6 +160,7 @@ 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 = 80;
|
||||
private static currentVersion = 81;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -653,9 +653,11 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
|
||||
|
||||
await this.$executeQuery('TRUNCATE hashrates');
|
||||
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
||||
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||
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.updateToSchemaVersion(75);
|
||||
}
|
||||
@@ -691,6 +693,13 @@ 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,6 +453,7 @@ 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;
|
||||
@@ -460,6 +461,7 @@ 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 {
|
||||
|
||||
@@ -396,10 +396,6 @@ class Mempool {
|
||||
}
|
||||
|
||||
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const changed: string[] = [];
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import bitcoinClient from '../bitcoin/bitcoin-client';
|
||||
import mining from "./mining";
|
||||
import PricesRepository from '../../repositories/PricesRepository';
|
||||
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
||||
import accelerationApi from '../services/acceleration';
|
||||
|
||||
class MiningRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
@@ -41,6 +42,8 @@ class MiningRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/total', this.$getAccelerationTotals)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations', this.$getActiveAccelerations)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'acceleration/request/:txid', this.$requestAcceleration)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -445,6 +448,33 @@ class MiningRoutes {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getActiveAccelerations(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(accelerationApi.accelerations || []);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $requestAcceleration(req: Request, res: Response): Promise<void> {
|
||||
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();
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MiningRoutes();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import config from '../../config';
|
||||
import logger from '../../logger';
|
||||
import { BlockExtended, PoolTag } from '../../mempool.interfaces';
|
||||
import { BlockExtended } from '../../mempool.interfaces';
|
||||
import axios from 'axios';
|
||||
|
||||
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
|
||||
|
||||
export interface Acceleration {
|
||||
txid: string,
|
||||
added: number,
|
||||
@@ -35,18 +37,91 @@ export interface AccelerationHistory {
|
||||
};
|
||||
|
||||
class AccelerationApi {
|
||||
public async $fetchAccelerations(): Promise<Acceleration[] | null> {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
try {
|
||||
const response = await axios.get(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`, { responseType: 'json', timeout: 10000 });
|
||||
return response.data as Acceleration[];
|
||||
} catch (e) {
|
||||
logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e));
|
||||
return null;
|
||||
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;
|
||||
private forcePoll = false;
|
||||
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
|
||||
|
||||
public get accelerations(): Acceleration[] | null {
|
||||
return this._accelerations;
|
||||
}
|
||||
|
||||
public countMyAccelerationsWithStatus(filter: MyAccelerationStatus): number {
|
||||
return Object.values(this.myAccelerations).reduce((count, {status}) => { return count + (status === filter ? 1 : 0); }, 0);
|
||||
}
|
||||
|
||||
public accelerationRequested(txid: string): void {
|
||||
if (this.onDemandPollingEnabled) {
|
||||
this.myAccelerations[txid] = { status: 'requested', added: Date.now() };
|
||||
}
|
||||
}
|
||||
|
||||
public accelerationConfirmed(): void {
|
||||
this.forcePoll = true;
|
||||
}
|
||||
|
||||
private async $fetchAccelerations(): Promise<Acceleration[] | null> {
|
||||
try {
|
||||
const response = await axios.get(this.apiPath, { responseType: 'json', timeout: 10000 });
|
||||
return response?.data || [];
|
||||
} catch (e) {
|
||||
logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async $updateAccelerations(): Promise<Acceleration[] | null> {
|
||||
if (!this.onDemandPollingEnabled) {
|
||||
const accelerations = await this.$fetchAccelerations();
|
||||
if (accelerations) {
|
||||
this._accelerations = accelerations;
|
||||
return this._accelerations;
|
||||
}
|
||||
} else {
|
||||
return [];
|
||||
return this.$updateAccelerationsOnDemand();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
|
||||
const shouldUpdate = this.forcePoll
|
||||
|| this.countMyAccelerationsWithStatus('requested') > 0
|
||||
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
|
||||
|
||||
// update accelerations if necessary
|
||||
if (shouldUpdate) {
|
||||
const accelerations = await this.$fetchAccelerations();
|
||||
this.lastPoll = Date.now();
|
||||
this.forcePoll = false;
|
||||
if (accelerations) {
|
||||
const latestAccelerations: Record<string, Acceleration> = {};
|
||||
// set relevant accelerations to 'accelerating'
|
||||
for (const acc of accelerations) {
|
||||
if (this.myAccelerations[acc.txid]) {
|
||||
latestAccelerations[acc.txid] = acc;
|
||||
this.myAccelerations[acc.txid] = { status: 'accelerating', added: Date.now(), acceleration: acc };
|
||||
}
|
||||
}
|
||||
// txs that are no longer accelerating are either confirmed or canceled, so mark for expiry
|
||||
for (const [txid, { status, acceleration }] of Object.entries(this.myAccelerations)) {
|
||||
if (status === 'accelerating' && !latestAccelerations[txid]) {
|
||||
this.myAccelerations[txid] = { status: 'done', added: Date.now(), acceleration };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clear expired accelerations (confirmed / failed / not accepted) after 10 minutes
|
||||
for (const [txid, { status, added }] of Object.entries(this.myAccelerations)) {
|
||||
if (['requested', 'done'].includes(status) && added < (Date.now() - (1000 * 60 * 10))) {
|
||||
delete this.myAccelerations[txid];
|
||||
}
|
||||
}
|
||||
|
||||
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
|
||||
return this._accelerations;
|
||||
}
|
||||
|
||||
public async $fetchAccelerationHistory(page?: number, status?: string): Promise<AccelerationHistory[] | null> {
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as WebSocket from 'ws';
|
||||
import {
|
||||
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
||||
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
||||
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids
|
||||
MempoolDelta, MempoolDeltaTxids
|
||||
} from '../mempool.interfaces';
|
||||
import blocks from './blocks';
|
||||
import memPool from './mempool';
|
||||
@@ -538,9 +538,9 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, true);
|
||||
} else {
|
||||
await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, true);
|
||||
}
|
||||
|
||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
@@ -823,6 +823,7 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
},
|
||||
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
||||
};
|
||||
@@ -864,6 +865,7 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
};
|
||||
if (!mempoolTx.cpfpChecked) {
|
||||
calculateMempoolTxCpfp(mempoolTx, newMempool);
|
||||
@@ -931,6 +933,8 @@ class WebsocketHandler {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
}
|
||||
|
||||
const blockTransactions = structuredClone(transactions);
|
||||
|
||||
this.printLogs();
|
||||
await statistics.runStatistics();
|
||||
|
||||
@@ -940,7 +944,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, transactions);
|
||||
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
|
||||
|
||||
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
||||
@@ -949,22 +953,18 @@ class WebsocketHandler {
|
||||
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||
let projectedBlocks;
|
||||
const auditMempool = _memPool;
|
||||
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
|
||||
const isAccelerated = accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
|
||||
|
||||
if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) {
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
const added = memPool.limitGBT ? (candidates?.added || []) : [];
|
||||
const removed = memPool.limitGBT ? (candidates?.removed || []) : [];
|
||||
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id);
|
||||
} else {
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id);
|
||||
}
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
const added = memPool.limitGBT ? (candidates?.added || []) : [];
|
||||
const removed = memPool.limitGBT ? (candidates?.removed || []) : [];
|
||||
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id);
|
||||
} else {
|
||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id);
|
||||
}
|
||||
|
||||
if (Common.indexingEnabled()) {
|
||||
const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||
const { unseen, censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(block.height, blockTransactions, projectedBlocks, auditMempool);
|
||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||
|
||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
||||
@@ -986,9 +986,11 @@ class WebsocketHandler {
|
||||
});
|
||||
|
||||
BlocksAuditsRepository.$saveAudit({
|
||||
version: 1,
|
||||
time: block.timestamp,
|
||||
height: block.height,
|
||||
hash: block.id,
|
||||
unseenTxs: unseen,
|
||||
addedTxs: added,
|
||||
prioritizedTxs: prioritized,
|
||||
missingTxs: censored,
|
||||
@@ -1040,7 +1042,7 @@ class WebsocketHandler {
|
||||
const removed = memPool.limitGBT ? (candidates?.removed || []) : transactions;
|
||||
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, _memPool, added, removed, candidates, true);
|
||||
} else {
|
||||
await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, true);
|
||||
}
|
||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||
@@ -1142,6 +1144,7 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
},
|
||||
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
||||
});
|
||||
@@ -1164,6 +1167,7 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +193,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': false,
|
||||
'RUST_GBT': true,
|
||||
'LIMIT_GBT': false,
|
||||
'CPFP_INDEXING': false,
|
||||
'MAX_BLOCKS_BULK_QUERY': 0,
|
||||
|
||||
@@ -229,7 +229,7 @@ class Server {
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
||||
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
||||
const newAccelerations = await accelerationApi.$fetchAccelerations();
|
||||
const newAccelerations = await accelerationApi.$updateAccelerations();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||
if (numHandledBlocks === 0) {
|
||||
|
||||
@@ -10,6 +10,7 @@ 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;
|
||||
@@ -192,6 +193,7 @@ 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,9 +29,11 @@ export interface PoolStats extends PoolInfo {
|
||||
}
|
||||
|
||||
export interface BlockAudit {
|
||||
version: number,
|
||||
time: number,
|
||||
height: number,
|
||||
hash: string,
|
||||
unseenTxs: string[],
|
||||
missingTxs: string[],
|
||||
freshTxs: string[],
|
||||
sigopTxs: string[],
|
||||
@@ -126,6 +128,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
||||
acceleration?: boolean;
|
||||
acceleratedBy?: number[];
|
||||
acceleratedAt?: number;
|
||||
feeDelta?: number;
|
||||
replacement?: boolean;
|
||||
uid?: number;
|
||||
flags?: number;
|
||||
@@ -382,8 +385,9 @@ export interface CpfpCluster {
|
||||
}
|
||||
|
||||
export interface CpfpSummary {
|
||||
transactions: TransactionExtended[];
|
||||
transactions: MempoolTransactionExtended[];
|
||||
clusters: CpfpCluster[];
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface Statistic {
|
||||
@@ -449,7 +453,7 @@ export interface OptimizedStatistic {
|
||||
|
||||
export interface TxTrackingInfo {
|
||||
replacedBy?: string,
|
||||
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number },
|
||||
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number },
|
||||
cpfp?: {
|
||||
ancestors?: Ancestor[],
|
||||
bestDescendant?: Ancestor | null,
|
||||
@@ -462,6 +466,7 @@ 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
|
||||
// process missing audits in batches of BATCH_SIZE
|
||||
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,9 +109,11 @@ 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,6 +192,7 @@ 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) {
|
||||
@@ -213,6 +214,15 @@ class AccelerationRepository {
|
||||
this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, successfulAccelerations);
|
||||
}
|
||||
}
|
||||
let anyConfirmed = false;
|
||||
for (const acc of accelerations) {
|
||||
if (blockTxs[acc.txid]) {
|
||||
anyConfirmed = true;
|
||||
}
|
||||
}
|
||||
if (anyConfirmed) {
|
||||
accelerationApi.accelerationConfirmed();
|
||||
}
|
||||
const lastSyncedHeight = await this.$getLastSyncedHeight();
|
||||
// if we've missed any blocks, let the indexer catch up from the last synced height on the next run
|
||||
if (block.height === lastSyncedHeight + 1) {
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import blocks from '../api/blocks';
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces';
|
||||
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[],
|
||||
}
|
||||
|
||||
class BlocksAuditRepositories {
|
||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||
try {
|
||||
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),
|
||||
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),
|
||||
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
|
||||
@@ -62,24 +73,30 @@ class BlocksAuditRepositories {
|
||||
public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(
|
||||
`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
|
||||
`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
|
||||
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);
|
||||
@@ -101,7 +118,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);
|
||||
@@ -115,16 +132,17 @@ class BlocksAuditRepositories {
|
||||
firstSeen = tx.time;
|
||||
}
|
||||
});
|
||||
const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
|
||||
|
||||
return {
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
seen: wasSeen,
|
||||
expected: isExpected,
|
||||
added: isAdded,
|
||||
added: isAdded && (blockAudit.version === 0 || !wasSeen),
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
firstSeen,
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (e: any) {
|
||||
@@ -186,6 +204,96 @@ 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:=false}
|
||||
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true}
|
||||
__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:=""}
|
||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
|
||||
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||
|
||||
# REDIS
|
||||
|
||||
@@ -40,7 +40,8 @@ __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__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}
|
||||
__SERVICES_API__=${SERVICES_API:=false}
|
||||
__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true}
|
||||
__SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
|
||||
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
|
||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
||||
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
||||
@@ -70,6 +71,7 @@ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __ACCELERATOR__
|
||||
export __ACCELERATOR_BUTTON__
|
||||
export __SERVICES_API__
|
||||
export __PUBLIC_ACCELERATIONS__
|
||||
export __HISTORICAL_PRICE__
|
||||
|
||||
@@ -54,6 +54,10 @@
|
||||
"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/"
|
||||
|
||||
@@ -543,16 +543,7 @@ describe('Mainnet', () => {
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
cy.get('.alert-replaced').should('be.visible');
|
||||
});
|
||||
|
||||
it('shows RBF transactions properly (desktop)', () => {
|
||||
|
||||
@@ -750,7 +750,7 @@
|
||||
},
|
||||
"backendInfo": {
|
||||
"hostname": "node205.tk7.mempool.space",
|
||||
"version": "3.0.0-dev",
|
||||
"version": "3.0.0",
|
||||
"gitCommit": "abbc8a134",
|
||||
"lightning": false
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"HISTORICAL_PRICE": true,
|
||||
"ADDITIONAL_CURRENCIES": false,
|
||||
"ACCELERATOR": false,
|
||||
"ACCELERATOR_BUTTON": true,
|
||||
"PUBLIC_ACCELERATIONS": false,
|
||||
"SERVICES_API": "https://mempool.space/api/v1/services"
|
||||
}
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "3.0.0-dev",
|
||||
"version": "3.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-frontend",
|
||||
"version": "3.0.0-dev",
|
||||
"version": "3.0.0",
|
||||
"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.0-dev",
|
||||
"version": "3.0.1",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
@@ -140,16 +141,17 @@ 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: [],
|
||||
@@ -213,10 +215,6 @@ let routes: Routes = [
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
},
|
||||
];
|
||||
|
||||
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
@@ -301,13 +299,16 @@ 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">
|
||||
<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">
|
||||
<defs>
|
||||
<style>
|
||||
.d {
|
||||
@@ -125,7 +125,9 @@
|
||||
<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"><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" 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>
|
||||
<span>Unchained</span>
|
||||
</a>
|
||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||
@@ -150,7 +152,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">
|
||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
|
||||
<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)"/>
|
||||
@@ -435,7 +437,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 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.
|
||||
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.
|
||||
</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,8 +13,6 @@
|
||||
|
||||
.image.not-rounded {
|
||||
border-radius: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
@@ -158,9 +156,8 @@
|
||||
margin: 40px 29px 10px;
|
||||
&.image.coldcard {
|
||||
border-radius: 0;
|
||||
width: auto;
|
||||
max-height: 50px;
|
||||
margin: 40px 29px 14px 29px;
|
||||
height: auto;
|
||||
margin: 20px 29px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,16 +389,30 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (canPayWithCashapp) {
|
||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
||||
<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) {
|
||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
||||
<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>
|
||||
<img class="paymentMethod mx-2" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
|
||||
@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>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -421,9 +435,9 @@
|
||||
<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') {
|
||||
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
|
||||
<!-- Show checkout page -->
|
||||
<div class="row mb-md-1 text-center">
|
||||
<div class="row mb-md-1 text-center" id="confirm-title">
|
||||
<div class="col-sm" id="confirm-payment-title">
|
||||
<h1 style="font-size: larger;"><ng-content select="[slot='checkout-title']"></ng-content><span class="default-slot" i18n="accelerator.confirm-your-payment">Confirm your payment</span></h1>
|
||||
</div>
|
||||
@@ -437,7 +451,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!loadingCashapp) {
|
||||
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
|
||||
<div class="row text-center mt-1">
|
||||
<div class="col-sm">
|
||||
<div class="form-group w-100">
|
||||
@@ -456,8 +470,14 @@
|
||||
<div class="row text-center mt-1">
|
||||
<div class="col-sm">
|
||||
<div class="form-group w-100">
|
||||
<div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
@if (loadingCashapp) {
|
||||
@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>
|
||||
} @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>
|
||||
}
|
||||
@if (loadingCashapp || loadingApplePay || loadingGooglePay) {
|
||||
<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>
|
||||
@@ -505,7 +525,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 > 20000) {
|
||||
@if (timeSincePaid > 30000) {
|
||||
<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>
|
||||
@@ -549,7 +569,7 @@
|
||||
<button type="button" *ngIf="advancedEnabled" class="btn btn-sm btn-outline-info btn-small-height ml-2" (click)="moveToStep('quote')" i18n="accelerator.customize">customize</button>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #accelerateTo let-x i18n="accelerator.accelerate-to-x">Accelerate to ~{{ x | number : '1.0-0' }} sat/vB</ng-template>
|
||||
<ng-template id="accelerate-to" #accelerateTo let-x i18n="accelerator.accelerate-to-x">Accelerate to ~{{ x | number : '1.0-0' }} sat/vB</ng-template>
|
||||
|
||||
<ng-template #accelerateButton>
|
||||
<div class="position-relative">
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
.paymentMethod {
|
||||
padding: 10px;
|
||||
background-color: var(--secondary);
|
||||
border-radius: 15px;
|
||||
border: 2px solid var(--bg);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -202,4 +201,19 @@
|
||||
|
||||
.btn-error-wrapper {
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.apple-pay-button {
|
||||
display: inline-block;
|
||||
-webkit-appearance: -apple-pay-button;
|
||||
-apple-pay-button-type: plain; /* Use any supported button type. */
|
||||
}
|
||||
.apple-pay-button-black {
|
||||
-apple-pay-button-style: black;
|
||||
}
|
||||
.apple-pay-button-white {
|
||||
-apple-pay-button-style: white;
|
||||
}
|
||||
.apple-pay-button-white-with-line {
|
||||
-apple-pay-button-style: white-outline;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
/* 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 { nextRoundNumber, insecureRandomUUID } from '../../shared/common.utils';
|
||||
import { md5, insecureRandomUUID } from '../../shared/common.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ETA, EtaService } from '../../services/eta.service';
|
||||
@@ -9,8 +10,10 @@ import { Transaction } from '../../interfaces/electrs.interface';
|
||||
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';
|
||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
|
||||
|
||||
export type AccelerationEstimate = {
|
||||
hasAccess: boolean;
|
||||
@@ -23,7 +26,7 @@ export type AccelerationEstimate = {
|
||||
mempoolBaseFee: number;
|
||||
vsizeFee: number;
|
||||
pools: number[];
|
||||
availablePaymentMethods: {[method: string]: {min: number, max: number}};
|
||||
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
|
||||
unavailable?: boolean;
|
||||
options: { // recommended bid options
|
||||
fee: number; // recommended userBid in sats
|
||||
@@ -46,7 +49,7 @@ export const MIN_BID_RATIO = 1;
|
||||
export const DEFAULT_BID_RATIO = 2;
|
||||
export const MAX_BID_RATIO = 4;
|
||||
|
||||
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'processing' | 'paid' | 'success';
|
||||
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerate-checkout',
|
||||
@@ -60,6 +63,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
@Input() eta: ETA;
|
||||
@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;
|
||||
@@ -78,17 +83,22 @@ 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;
|
||||
@@ -109,20 +119,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
|
||||
// square
|
||||
loadingCashapp = false;
|
||||
cashappError = false;
|
||||
cashappSubmit: any;
|
||||
loadingApplePay = false;
|
||||
loadingGooglePay = false;
|
||||
payments: any;
|
||||
cashAppPay: any;
|
||||
cashAppSubscription: Subscription;
|
||||
applePay: any;
|
||||
googlePay: any;
|
||||
conversionsSubscription: Subscription;
|
||||
conversions: any;
|
||||
|
||||
conversions: Record<string, number>;
|
||||
|
||||
// btcpay
|
||||
loadingBtcpayInvoice = false;
|
||||
invoice = undefined;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private etaService: EtaService,
|
||||
private audioService: AudioService,
|
||||
@@ -131,9 +143,15 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
private enterpriseService: EnterpriseService,
|
||||
) {
|
||||
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']) {
|
||||
this.applePayEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
this.authSubscription$ = this.authService.getAuth$().subscribe((auth) => {
|
||||
if (this.auth?.user?.userId !== auth?.user?.userId) {
|
||||
this.auth = auth;
|
||||
@@ -158,13 +176,6 @@ 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;
|
||||
@@ -172,7 +183,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
ngOnDestroy(): void {
|
||||
if (this.estimateSubscription) {
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
@@ -192,7 +203,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
moveToStep(step: CheckoutStep) {
|
||||
moveToStep(step: CheckoutStep): void {
|
||||
this._step = step;
|
||||
if (this.timeoutTimer) {
|
||||
clearTimeout(this.timeoutTimer);
|
||||
@@ -201,6 +212,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.fetchEstimate();
|
||||
}
|
||||
if (this._step === 'checkout') {
|
||||
this.insertSquare();
|
||||
this.enterpriseService.goal(8);
|
||||
}
|
||||
if (this._step === 'checkout' && this.canPayWithBitcoin) {
|
||||
@@ -210,15 +222,23 @@ 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.setupSquare();
|
||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'paid') {
|
||||
this.timePaid = Date.now();
|
||||
this.timeoutTimer = setTimeout(() => {
|
||||
if (this.step === 'paid') {
|
||||
this.accelerateError = 'internal_server_error';
|
||||
}
|
||||
}, 120000)
|
||||
}, 120000);
|
||||
}
|
||||
this.hasDetails.emit(this._step === 'quote');
|
||||
}
|
||||
@@ -229,14 +249,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to element id with or without setTimeout
|
||||
*/
|
||||
* Scroll to element id with or without setTimeout
|
||||
*/
|
||||
scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void {
|
||||
setTimeout(() => {
|
||||
this.scrollToElement(id, position);
|
||||
}, timeout);
|
||||
}
|
||||
scrollToElement(id: string, position: ScrollLogicalPosition) {
|
||||
scrollToElement(id: string, position: ScrollLogicalPosition): void {
|
||||
const acceleratePreviewAnchor = document.getElementById(id);
|
||||
if (acceleratePreviewAnchor) {
|
||||
this.cd.markForCheck();
|
||||
@@ -251,7 +271,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Accelerator
|
||||
*/
|
||||
fetchEstimate() {
|
||||
fetchEstimate(): void {
|
||||
if (this.estimateSubscription) {
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
@@ -315,7 +335,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
}),
|
||||
|
||||
catchError((response) => {
|
||||
catchError(() => {
|
||||
this.estimate = undefined;
|
||||
this.quoteError = `cannot_accelerate_tx`;
|
||||
this.estimateSubscription.unsubscribe();
|
||||
@@ -370,10 +390,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
this.showSuccess = true;
|
||||
this.estimateSubscription.unsubscribe();
|
||||
this.moveToStep('paid')
|
||||
this.moveToStep('paid');
|
||||
},
|
||||
error: (response) => {
|
||||
this.accelerateError = response.error;
|
||||
@@ -385,57 +406,248 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
* Square
|
||||
*/
|
||||
insertSquare(): void {
|
||||
//@ts-ignore
|
||||
if (window.Square) {
|
||||
if (!this.isProdDomain && !isDevMode()) {
|
||||
return;
|
||||
}
|
||||
if (window['Square']) {
|
||||
return;
|
||||
}
|
||||
let statsUrl = 'https://sandbox.web.squarecdn.com/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';
|
||||
if (this.isProdDomain) {
|
||||
statsUrl = '/square/v1/square.js';
|
||||
}
|
||||
|
||||
(function() {
|
||||
(function(): void {
|
||||
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() {
|
||||
const init = () => {
|
||||
setupSquare(): void {
|
||||
if (!this.isProdDomain && !isDevMode()) {
|
||||
return;
|
||||
}
|
||||
const init = (): void => {
|
||||
this.initSquare();
|
||||
};
|
||||
|
||||
//@ts-ignore
|
||||
if (!window.Square) {
|
||||
console.debug('Square.js failed to load properly. Retrying in 1 second.');
|
||||
setTimeout(init, 1000);
|
||||
if (!window['Square']) {
|
||||
console.debug('Square.js failed to load properly. Retrying.');
|
||||
setTimeout(this.setupSquare.bind(this), 100);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}
|
||||
async initSquare(): Promise<void> {
|
||||
try {
|
||||
//@ts-ignore
|
||||
this.payments = window.Square.payments(this.square.appId, this.square.locationId)
|
||||
await this.requestCashAppPayment();
|
||||
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';
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.debug('Error loading Square Payments', e);
|
||||
this.cashappError = true;
|
||||
return;
|
||||
this.accelerateError = 'cannot_setup_square';
|
||||
}
|
||||
}
|
||||
async requestCashAppPayment() {
|
||||
if (this.cashAppSubscription) {
|
||||
this.cashAppSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* APPLE PAY
|
||||
*/
|
||||
async requestApplePayPayment(): Promise<void> {
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
if (this.applePay) {
|
||||
this.applePay.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',
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
this.applePay = await this.payments.applePay(paymentRequest);
|
||||
const applePayButton = document.getElementById('apple-pay-button');
|
||||
if (!applePayButton) {
|
||||
console.error(`Unable to find apple pay button id='apple-pay-button'`);
|
||||
// Try again
|
||||
setTimeout(this.requestApplePayPayment.bind(this), 500);
|
||||
return;
|
||||
}
|
||||
this.loadingApplePay = false;
|
||||
applePayButton.addEventListener('click', async event => {
|
||||
event.preventDefault();
|
||||
const tokenResult = await this.applePay.tokenize();
|
||||
if (tokenResult?.status === 'OK') {
|
||||
const card = tokenResult.details?.card;
|
||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||
console.error(`Cannot retreive payment card details`);
|
||||
this.accelerateError = 'apple_pay_no_card_details';
|
||||
return;
|
||||
}
|
||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||
this.servicesApiService.accelerateWithApplePay$(
|
||||
this.tx.txid,
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.applePay) {
|
||||
this.applePay.destroy();
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.moveToStep('paid');
|
||||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.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);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
@@ -449,41 +661,37 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
countryCode: 'US',
|
||||
currencyCode: 'USD',
|
||||
total: {
|
||||
amount: costUSD.toString(),
|
||||
amount: costUSD.toFixed(2),
|
||||
label: 'Total',
|
||||
pending: true,
|
||||
productUrl: `${redirectHostname}/tracker/${this.tx.txid}`,
|
||||
},
|
||||
button: { shape: 'semiround', size: 'small', theme: 'light'}
|
||||
productUrl: `${redirectHostname}/tx/${this.tx.txid}`,
|
||||
}
|
||||
});
|
||||
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
|
||||
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'}
|
||||
redirectURL: `${redirectHostname}/tx/${this.tx.txid}`,
|
||||
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`
|
||||
});
|
||||
|
||||
if (this.step === 'cashapp') {
|
||||
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' })
|
||||
}
|
||||
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'dark' });
|
||||
this.loadingCashapp = false;
|
||||
|
||||
const that = this;
|
||||
this.cashAppPay.addEventListener('ontokenization', function (event) {
|
||||
this.cashAppPay.addEventListener('ontokenization', event => {
|
||||
const { tokenResult, error } = event.detail;
|
||||
if (error) {
|
||||
this.accelerateError = error;
|
||||
} else if (tokenResult.status === 'OK') {
|
||||
that.servicesApiService.accelerateWithCashApp$(
|
||||
that.tx.txid,
|
||||
this.servicesApiService.accelerateWithCashApp$(
|
||||
this.tx.txid,
|
||||
tokenResult.token,
|
||||
tokenResult.details.cashAppPay.cashtag,
|
||||
tokenResult.details.cashAppPay.referenceId,
|
||||
that.accelerationUUID
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
that.audioService.playSound('ascend-chime-cartoon');
|
||||
if (that.cashAppPay) {
|
||||
that.cashAppPay.destroy();
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.cashAppPay) {
|
||||
this.cashAppPay.destroy();
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.moveToStep('paid');
|
||||
@@ -494,7 +702,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
that.accelerateError = response.error;
|
||||
this.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
// Reset everything by reloading the page :D, can be improved
|
||||
@@ -513,7 +721,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
/**
|
||||
* BTCPay
|
||||
*/
|
||||
async requestBTCPayInvoice() {
|
||||
async requestBTCPayInvoice(): Promise<void> {
|
||||
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
|
||||
switchMap(response => {
|
||||
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
|
||||
@@ -530,9 +738,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
bitcoinPaymentCompleted(): void {
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
this.estimateSubscription.unsubscribe();
|
||||
this.moveToStep('paid')
|
||||
this.moveToStep('paid');
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
@@ -542,47 +751,61 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
/**
|
||||
* UI events
|
||||
*/
|
||||
selectedOptionChanged(event) {
|
||||
selectedOptionChanged(event): void {
|
||||
this.selectedOption = event.target.id;
|
||||
}
|
||||
|
||||
get step() {
|
||||
get step(): CheckoutStep {
|
||||
return this._step;
|
||||
}
|
||||
|
||||
get paymentMethods() {
|
||||
return Object.keys(this.estimate?.availablePaymentMethods || {});
|
||||
get paymentMethods(): PaymentMethod[] {
|
||||
return Object.keys(this.estimate?.availablePaymentMethods || {}) as PaymentMethod[];
|
||||
}
|
||||
|
||||
get couldPayWithBitcoin() {
|
||||
get couldPayWithBitcoin(): boolean {
|
||||
return !!this.estimate?.availablePaymentMethods?.bitcoin;
|
||||
}
|
||||
|
||||
get couldPayWithCashapp() {
|
||||
get couldPayWithCashapp(): boolean {
|
||||
if (!this.cashappEnabled) {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.cashapp;
|
||||
}
|
||||
|
||||
get couldPayWithBalance() {
|
||||
get couldPayWithApplePay(): boolean {
|
||||
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 {
|
||||
if (!this.hasAccessToBalanceMode) {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.balance;
|
||||
}
|
||||
|
||||
get couldPay() {
|
||||
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp;
|
||||
get couldPay(): boolean {
|
||||
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay || this.couldPayWithGooglePay;
|
||||
}
|
||||
|
||||
get canPayWithBitcoin() {
|
||||
get canPayWithBitcoin(): boolean {
|
||||
const paymentMethod = this.estimate?.availablePaymentMethods?.bitcoin;
|
||||
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max;
|
||||
}
|
||||
|
||||
get canPayWithCashapp() {
|
||||
if (!this.cashappEnabled || !this.conversions) {
|
||||
get canPayWithCashapp(): boolean {
|
||||
if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -593,11 +816,43 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get canPayWithBalance() {
|
||||
get canPayWithApplePay(): boolean {
|
||||
if (!this.applePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const paymentMethod = this.estimate?.availablePaymentMethods?.applePay;
|
||||
if (paymentMethod) {
|
||||
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
|
||||
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!this.hasAccessToBalanceMode) {
|
||||
return false;
|
||||
}
|
||||
@@ -605,11 +860,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max && this.cost <= this.estimate?.userBalance;
|
||||
}
|
||||
|
||||
get canPay() {
|
||||
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp;
|
||||
get canPay(): boolean {
|
||||
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay || this.canPayWithGooglePay;
|
||||
}
|
||||
|
||||
get hasAccessToBalanceMode() {
|
||||
get hasAccessToBalanceMode(): boolean {
|
||||
return this.isLoggedIn() && this.estimate?.hasAccess;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<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>
|
||||
@@ -0,0 +1,39 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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 animate"></div>
|
||||
<div class="shape"></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">
|
||||
<div class="shape-border hovering" (pointerover)="onHover($event, 'seen');" (pointerout)="onBlur($event);">
|
||||
<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">
|
||||
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
|
||||
<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 offset-left" [class.no-margin]="!tx.status.confirmed">
|
||||
<div class="time" [class.no-margin]="!tx.status.confirmed" [class.offset-left]="!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]="true"></app-time>
|
||||
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed"></app-time>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,7 +113,10 @@
|
||||
} @else {
|
||||
<div class="seen-to-acc left"></div>
|
||||
}
|
||||
<div class="shape-border" [class.waiting]="!tx.status.confirmed">
|
||||
<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"></div>
|
||||
</div>
|
||||
@if (tx.status.confirmed) {
|
||||
@@ -130,4 +133,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-acceleration-timeline-tooltip
|
||||
[accelerationInfo]="hoverInfo"
|
||||
[cursorPosition]="tooltipPosition"
|
||||
></app-acceleration-timeline-tooltip>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -152,9 +152,16 @@
|
||||
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,6 +1,8 @@
|
||||
import { Component, Input, OnInit, OnChanges } from '@angular/core';
|
||||
import { Component, Input, OnInit, OnChanges, HostListener } 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',
|
||||
@@ -10,6 +12,7 @@ import { Transaction } from '../../interfaces/electrs.interface';
|
||||
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)
|
||||
@@ -22,13 +25,25 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
useAbsoluteTime: boolean = false;
|
||||
interval: number;
|
||||
|
||||
constructor() {}
|
||||
tooltipPosition = null;
|
||||
hoverInfo: any = null;
|
||||
poolsData: { [id: number]: SinglePoolStats } = {};
|
||||
|
||||
constructor(
|
||||
private miningService: MiningService,
|
||||
) {}
|
||||
|
||||
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;
|
||||
@@ -52,4 +67,42 @@ 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: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations">
|
||||
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed">
|
||||
<div class="acceleration-list">
|
||||
<table *ngIf="nonEmptyAccelerations; 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">
|
||||
@@ -16,11 +16,12 @@
|
||||
<ng-container *ngIf="!pending">
|
||||
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
|
||||
<th class="block text-right" i18n="shared.block-title">Block</th>
|
||||
<th class="pool text-right" i18n="mining.pool-name" *ngIf="!this.widget">Pool</th>
|
||||
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
|
||||
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
|
||||
</ng-container>
|
||||
</thead>
|
||||
<tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tbody *ngIf="accelerationList$ | async as 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]">
|
||||
@@ -49,10 +50,21 @@
|
||||
<a *ngIf="acceleration.blockHeight" [routerLink]="['/block' | relativeUrl, acceleration.blockHeight]">{{ acceleration.blockHeight }}</a>
|
||||
<span *ngIf="!acceleration.blockHeight">~</span>
|
||||
</td>
|
||||
<td class="pool text-right" *ngIf="!this.widget">
|
||||
@if (acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]) {
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pools[acceleration.minedByPoolUniqueId].slug]" class="badge" style="color: #FFF;padding:0;">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + pools[acceleration.minedByPoolUniqueId].slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pools[acceleration.minedByPoolUniqueId].name + ' mining pool'">
|
||||
{{ pools[acceleration.minedByPoolUniqueId].name }}
|
||||
</a>
|
||||
} @else {
|
||||
~
|
||||
}
|
||||
</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')" 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>
|
||||
<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>
|
||||
</td>
|
||||
<td class="date text-right" *ngIf="!this.widget">
|
||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||
@@ -61,22 +73,47 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
<ng-template #skeleton>
|
||||
<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>
|
||||
@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>
|
||||
}
|
||||
</ng-template>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.container-xl.legacy {
|
||||
max-width: 1140px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
.container-xl.widget-container {
|
||||
min-height: 335px;
|
||||
@@ -72,9 +72,25 @@ tr, td, th {
|
||||
|
||||
.block {
|
||||
width: 15%;
|
||||
@media (max-width: 900px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.pool {
|
||||
width: 15%;
|
||||
|
||||
@media (max-width: 700px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, Subscription, catchError, filter, of, switchMap, tap, throttleTime } from 'rxjs';
|
||||
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
|
||||
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MiningService } from '../../../services/mining.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerations-list',
|
||||
@@ -30,11 +31,14 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
keyNavigationSubscription: Subscription;
|
||||
dir: 'rtl' | 'ltr' = 'ltr';
|
||||
paramSubscription: Subscription;
|
||||
pools: { [id: number]: SinglePoolStats } = {};
|
||||
nonEmptyAccelerations: boolean = true;
|
||||
|
||||
constructor(
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private websocketService: WebsocketService,
|
||||
public stateService: StateService,
|
||||
private miningService: MiningService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private seoService: SeoService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -47,6 +51,12 @@ 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`);
|
||||
@@ -106,6 +116,7 @@ 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>
|
||||
<table style="width: 100%;">
|
||||
<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 [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
} @else {
|
||||
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
<app-fee-rate class="oobFees" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -61,4 +61,8 @@
|
||||
& > div, & > div > svg {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.oobFees {
|
||||
color: #905cf4;
|
||||
}
|
||||
@@ -67,12 +67,17 @@ 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,
|
||||
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * .08)),
|
||||
index >= firstSignificantPool
|
||||
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
|
||||
: 'white',
|
||||
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
|
||||
true,
|
||||
) as PieSeriesOption);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<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>
|
||||
@@ -0,0 +1,45 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #pendingBalanceRow>
|
||||
<td i18n="address.unconfirmed-balance" class="font-italic">Unconfirmed balance</td>
|
||||
<td i18n="accelerator.pending-state" class="font-italic">Pending</td>
|
||||
<td *ngIf="mempoolStats.funded_txo_sum !== undefined; else confidentialTd" class="font-italic wrap-cell"><app-amount [satoshis]="mempoolStats.balance" [noFiat]="true" [addPlus]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolStats.balance"></app-fiat></span></td>
|
||||
</ng-template>
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #pendingUtxoRow>
|
||||
<td i18n="address.unconfirmed-utxos" class="font-italic">Unconfirmed UTXOs</td>
|
||||
<td i18n="address.pending-utxos" class="font-italic">Pending UTXOs</td>
|
||||
<td class="font-italic wrap-cell">{{ mempoolStats.utxos > 0 ? '+' : ''}}{{ mempoolStats.utxos }}</td>
|
||||
</ng-template>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
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: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.any-mode]="filterMode === 'or'" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
|
||||
<a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles™ tooltip" ngbTooltip="select filter categories to highlight matching transactions">
|
||||
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
|
||||
<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">
|
||||
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
|
||||
</a>
|
||||
<div class="filter-bar">
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
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: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
<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>
|
||||
<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>
|
||||
@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>
|
||||
}
|
||||
<app-block-overview-tooltip
|
||||
[tx]="selectedTx || hoverTx"
|
||||
[cursorPosition]="tooltipPosition"
|
||||
|
||||
@@ -18,6 +18,7 @@ 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),
|
||||
};
|
||||
@@ -25,6 +26,7 @@ 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' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ 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'),
|
||||
};
|
||||
@@ -101,6 +102,7 @@ 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'),
|
||||
};
|
||||
@@ -136,6 +138,8 @@ 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">
|
||||
<td class="value" [class.oobFees]="this.acceleration">
|
||||
<app-fee-rate [fee]="effectiveRate"></app-fee-rate>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -75,6 +75,10 @@
|
||||
<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,6 +27,9 @@ 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: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -521,6 +521,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
if (transactions && blockAudit) {
|
||||
const inTemplate = {};
|
||||
const inBlock = {};
|
||||
const isUnseen = {};
|
||||
const isAdded = {};
|
||||
const isPrioritized = {};
|
||||
const isCensored = {};
|
||||
@@ -543,6 +544,9 @@ 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;
|
||||
}
|
||||
@@ -592,18 +596,27 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
tx.status = 'accelerated';
|
||||
}
|
||||
}
|
||||
for (const [index, tx] of transactions.entries()) {
|
||||
let anySeen = false;
|
||||
for (let index = transactions.length - 1; index >= 0; index--) {
|
||||
const tx = transactions[index];
|
||||
tx.context = 'actual';
|
||||
if (index === 0) {
|
||||
tx.status = null;
|
||||
} else if (isAdded[tx.txid]) {
|
||||
tx.status = 'added';
|
||||
} else if (isPrioritized[tx.txid]) {
|
||||
tx.status = 'prioritized';
|
||||
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])) {
|
||||
tx.status = 'added';
|
||||
} 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;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
.loadingGraphs.widget {
|
||||
top: 75%;
|
||||
|
||||
@@ -28,7 +28,7 @@ interface Hashrate {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -17,7 +17,7 @@ const OUTLIERS_MEDIAN_MULTIPLIER = 4;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
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 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></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>
|
||||
<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: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
.sticky-loading {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
font-size: 14px;
|
||||
@media (width >= 992px) {
|
||||
left: 32px;
|
||||
|
||||
@@ -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 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></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 [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,7 +51,8 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + (blockWidth * 0.3) + containerOffset + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></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>
|
||||
</ng-container>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Subscription, Observable, of, combineLatest } from 'rxjs';
|
||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
@@ -77,6 +77,9 @@ 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;
|
||||
@@ -201,6 +204,7 @@ 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;
|
||||
@@ -209,6 +213,12 @@ 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: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.pool-distribution {
|
||||
|
||||
@@ -167,7 +167,7 @@ div.scrollable {
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
top: 475px;
|
||||
@media (max-width: 992px) {
|
||||
top: 600px;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<br><br>
|
||||
|
||||
<h2>Privacy Policy</h2>
|
||||
<h6>Updated: July 10, 2024</h6>
|
||||
<h6>Updated: July 31, 2024</h6>
|
||||
|
||||
<br><br>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<br>
|
||||
|
||||
<h4>General</h4>
|
||||
<h4>USING THIS WEBSITE</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. 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>
|
||||
<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>
|
||||
|
||||
<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">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>
|
||||
<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>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -74,22 +74,38 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<p>EOF</p>
|
||||
|
||||
|
||||
@@ -303,7 +303,6 @@ export class SearchFormComponent implements OnInit {
|
||||
(error) => { console.log(error); this.isSearching = false; }
|
||||
);
|
||||
} else {
|
||||
this.searchResults.searchButtonClick();
|
||||
this.isSearching = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
this.minScrollWidth = 40 + (8 * this.blockWidth) + (this.pageWidth * 2);
|
||||
|
||||
if (firstVisibleBlock != null) {
|
||||
this.scrollToBlock(firstVisibleBlock, offset);
|
||||
this.scrollToBlock(firstVisibleBlock, offset + (this.isMobile ? this.blockWidth : 0));
|
||||
} else {
|
||||
this.updatePages();
|
||||
}
|
||||
|
||||
@@ -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]="['/tracker/' | relativeUrl, latestReplacement]"></app-truncate>
|
||||
<app-truncate [text]="latestReplacement" [lastChars]="12" [link]="['/tx/' | relativeUrl, latestReplacement]" [queryParams]="{mode: 'status'}"></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: 50%;"></span>
|
||||
<span class="skeleton-loader" style="max-width: 200px;"></span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,7 +78,7 @@
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-template #etaSkeleton>
|
||||
<span class="skeleton-loader"></span>
|
||||
<span class="skeleton-loader" style="max-width: 200px;"></span>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,7 +118,7 @@
|
||||
</div>
|
||||
<span class="explainer"> </span>
|
||||
} @else {
|
||||
@if (!tx.status?.confirmed && showAccelerationSummary) {
|
||||
@if (tx && !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.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 && !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,7 +185,11 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="footer-link" [routerLink]="['/tx' | relativeUrl, tx?.txid]">
|
||||
<div class="footer-link"
|
||||
[routerLink]="['/tx' | relativeUrl, tx?.txid || txId]"
|
||||
[queryParams]="{ mode: 'details' }"
|
||||
queryParamsHandling="merge"
|
||||
>
|
||||
<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: 100;
|
||||
z-index: 99;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ import { TrackerStage } from './tracker-bar.component';
|
||||
import { MiningService, MiningStats } from '../../services/mining.service';
|
||||
import { ETA, EtaService } from '../../services/eta.service';
|
||||
import { getTransactionFlags, getUnacceleratedFeeRate } from '../../shared/transaction.utils';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
|
||||
interface Pool {
|
||||
id: number;
|
||||
@@ -140,6 +142,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||
private priceService: PriceService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private miningService: MiningService,
|
||||
private router: Router,
|
||||
private cd: ChangeDetectorRef,
|
||||
private zone: NgZone,
|
||||
@Inject(ZONE_SERVICE) private zoneService: any,
|
||||
@@ -290,7 +293,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
).subscribe((accelerationHistory) => {
|
||||
for (const acceleration of accelerationHistory) {
|
||||
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
|
||||
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
|
||||
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
||||
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||
acceleration.boost = boostCost;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div *ngIf="officialMempoolSpace">
|
||||
<h2>Trademark Policy and Guidelines</h2>
|
||||
<h5>The Mempool Open Source Project ®</h5>
|
||||
<h6>Updated: July 3, 2024</h6>
|
||||
<h6>Updated: August 19, 2024</h6>
|
||||
<br>
|
||||
|
||||
<div class="text-left">
|
||||
@@ -100,11 +100,26 @@
|
||||
<p>The Mempool Accelerator Logo</p>
|
||||
<br><br>
|
||||
|
||||
<img src="/resources/mempool-research.png" style="width: 500px; max-width: 80%">
|
||||
<br><br>
|
||||
<p>The mempool research Logo</p>
|
||||
<br><br>
|
||||
|
||||
<app-svg-images name="goggles" height="96px"></app-svg-images>
|
||||
<br><br>
|
||||
<p>The Mempool Goggles Logo</p>
|
||||
<br><br>
|
||||
|
||||
<img src="/resources/mempool-transaction.png" style="width: 500px; max-width: 80%">
|
||||
<br><br>
|
||||
<p>The mempool transaction Logo</p>
|
||||
<br><br>
|
||||
|
||||
<img src="/resources/mempool-block-visualization.png" style="width: 500px; max-width: 80%">
|
||||
<br><br>
|
||||
<p>The mempool block visualization Logo</p>
|
||||
<br><br>
|
||||
|
||||
<img src="/resources/mempool-blocks-2-3-logo.jpeg" style="width: 500px; max-width: 80%">
|
||||
<br><br>
|
||||
<p>The mempool Blocks Logo</p>
|
||||
|
||||
@@ -62,6 +62,8 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<span id="accelerate"></span>
|
||||
|
||||
<ng-template [ngIf]="!isLoadingTx && !error">
|
||||
|
||||
<!-- CPFP Details -->
|
||||
@@ -167,7 +169,7 @@
|
||||
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [eta]="(ETA$ | async)" [standardETA]="(standardETA$ | async)?.time"></app-acceleration-timeline>
|
||||
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [accelerationInfo]="accelerationInfo" [eta]="(ETA$ | async)" [standardETA]="(standardETA$ | async)?.time"></app-acceleration-timeline>
|
||||
<br>
|
||||
</ng-container>
|
||||
|
||||
@@ -550,19 +552,19 @@
|
||||
<td>
|
||||
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
|
||||
@if (eta.blocks >= 7) {
|
||||
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
|
||||
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
|
||||
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
|
||||
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
|
||||
<a class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
<a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
}
|
||||
</span>
|
||||
} @else if (network === 'liquid' || network === 'liquidtestnet') {
|
||||
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
} @else {
|
||||
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
|
||||
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
|
||||
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
|
||||
<a class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
<a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
}
|
||||
</span>
|
||||
<span class="eta justify-content-end">
|
||||
@@ -604,7 +606,12 @@
|
||||
@if (!isLoadingTx) {
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span></td>
|
||||
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
|
||||
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
}
|
||||
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
} @else {
|
||||
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
|
||||
@@ -640,9 +647,9 @@
|
||||
<td>
|
||||
<div class="effective-fee-container">
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize || tx.acceleration)) {
|
||||
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
<app-fee-rate [class.oobFees]="isAcceleration" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
} @else {
|
||||
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
<app-fee-rate [class.oobFees]="isAcceleration" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
}
|
||||
|
||||
@if (tx?.status?.confirmed && !tx.acceleration && !accelerationInfo && tx.fee && tx.effectiveFeePerVsize) {
|
||||
|
||||
@@ -297,7 +297,6 @@
|
||||
}
|
||||
|
||||
.etaDeepMempool {
|
||||
display: flex !important;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
@@ -333,3 +332,7 @@
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.oobFees {
|
||||
color: #905cf4;
|
||||
}
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
tap,
|
||||
map,
|
||||
retry,
|
||||
startWith
|
||||
startWith,
|
||||
repeat,
|
||||
take
|
||||
} from 'rxjs/operators';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { of, merge, Subscription, Observable, Subject, from, throwError, combineLatest, BehaviorSubject } from 'rxjs';
|
||||
@@ -76,6 +78,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
transactionTime = -1;
|
||||
subscription: Subscription;
|
||||
fetchCpfpSubscription: Subscription;
|
||||
transactionTimesSubscription: Subscription;
|
||||
fetchRbfSubscription: Subscription;
|
||||
fetchCachedTxSubscription: Subscription;
|
||||
fetchAccelerationSubscription: Subscription;
|
||||
@@ -88,6 +91,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
blocksSubscription: Subscription;
|
||||
miningSubscription: Subscription;
|
||||
auditSubscription: Subscription;
|
||||
txConfirmedSubscription: Subscription;
|
||||
currencyChangeSubscription: Subscription;
|
||||
fragmentParams: URLSearchParams;
|
||||
rbfTransaction: undefined | Transaction;
|
||||
@@ -106,6 +110,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
showCpfpDetails = false;
|
||||
miningStats: MiningStats;
|
||||
fetchCpfp$ = new Subject<string>();
|
||||
transactionTimes$ = new Subject<string>();
|
||||
fetchRbfHistory$ = new Subject<string>();
|
||||
fetchCachedTx$ = new Subject<string>();
|
||||
fetchAcceleration$ = new Subject<number>();
|
||||
@@ -141,7 +146,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
taprootEnabled: boolean;
|
||||
hasEffectiveFeeRate: boolean;
|
||||
accelerateCtaType: 'alert' | 'button' = 'button';
|
||||
acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR && this.stateService.network === '';
|
||||
acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR_BUTTON && this.stateService.network === '';
|
||||
eligibleForAcceleration: boolean = false;
|
||||
forceAccelerationSummary = false;
|
||||
hideAccelerationSummary = false;
|
||||
@@ -195,7 +200,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.stateService.networkChanged$.subscribe(
|
||||
(network) => {
|
||||
this.network = network;
|
||||
this.acceleratorAvailable = this.stateService.env.ACCELERATOR && this.stateService.network === '';
|
||||
this.acceleratorAvailable = this.stateService.env.ACCELERATOR_BUTTON && this.stateService.network === '';
|
||||
}
|
||||
);
|
||||
|
||||
@@ -225,6 +230,25 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.latestBlock = blocks[0];
|
||||
});
|
||||
|
||||
this.transactionTimesSubscription = this.transactionTimes$.pipe(
|
||||
tap(() => {
|
||||
this.isLoadingFirstSeen = true;
|
||||
}),
|
||||
switchMap((txid) => this.apiService.getTransactionTimes$([txid]).pipe(
|
||||
retry({ count: 2, delay: 2000 }),
|
||||
// Try again until we either get a valid response, or the transaction is confirmed
|
||||
repeat({ delay: 2000 }),
|
||||
filter((transactionTimes) => transactionTimes?.length && transactionTimes[0] > 0 && !this.tx.status?.confirmed),
|
||||
take(1),
|
||||
)),
|
||||
)
|
||||
.subscribe((transactionTimes) => {
|
||||
this.isLoadingFirstSeen = false;
|
||||
if (transactionTimes?.length && transactionTimes[0]) {
|
||||
this.transactionTime = transactionTimes[0];
|
||||
}
|
||||
});
|
||||
|
||||
this.fetchCpfpSubscription = this.fetchCpfp$
|
||||
.pipe(
|
||||
switchMap((txId) =>
|
||||
@@ -334,12 +358,18 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}),
|
||||
).subscribe((accelerationHistory) => {
|
||||
for (const acceleration of accelerationHistory) {
|
||||
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
|
||||
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
||||
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||
acceleration.boost = boostCost;
|
||||
this.tx.acceleratedAt = acceleration.added;
|
||||
this.accelerationInfo = acceleration;
|
||||
if (acceleration.txid === this.txId) {
|
||||
if (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') {
|
||||
if (acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
|
||||
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
||||
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||
acceleration.boost = boostCost;
|
||||
this.tx.acceleratedAt = acceleration.added;
|
||||
this.accelerationInfo = acceleration;
|
||||
} else {
|
||||
this.tx.feeDelta = undefined;
|
||||
}
|
||||
}
|
||||
this.waitingForAccelerationInfo = false;
|
||||
this.setIsAccelerated();
|
||||
}
|
||||
@@ -387,10 +417,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const isConflict = audit.fullrbfTxs.includes(txid);
|
||||
const isExpected = audit.template.some(tx => tx.txid === txid);
|
||||
const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
|
||||
const wasSeen = audit.version === 1 ? !audit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
|
||||
return {
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
seen: wasSeen,
|
||||
expected: isExpected,
|
||||
added: isAdded,
|
||||
added: isAdded && (audit.version === 0 || !wasSeen),
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
@@ -437,9 +468,23 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (txPosition.position.acceleratedBy) {
|
||||
txPosition.cpfp.acceleratedBy = txPosition.position.acceleratedBy;
|
||||
}
|
||||
if (txPosition.position.acceleratedAt) {
|
||||
txPosition.cpfp.acceleratedAt = txPosition.position.acceleratedAt;
|
||||
}
|
||||
if (txPosition.position.feeDelta) {
|
||||
txPosition.cpfp.feeDelta = txPosition.position.feeDelta;
|
||||
}
|
||||
this.setCpfpInfo(txPosition.cpfp);
|
||||
} else if ((this.tx?.acceleration && txPosition.position.acceleratedBy)) {
|
||||
this.tx.acceleratedBy = txPosition.position.acceleratedBy;
|
||||
} else if ((this.tx?.acceleration)) {
|
||||
if (txPosition.position.acceleratedBy) {
|
||||
this.tx.acceleratedBy = txPosition.position.acceleratedBy;
|
||||
}
|
||||
if (txPosition.position.acceleratedAt) {
|
||||
this.tx.acceleratedAt = txPosition.position.acceleratedAt;
|
||||
}
|
||||
if (txPosition.position.feeDelta) {
|
||||
this.tx.feeDelta = txPosition.position.feeDelta;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.stateService.network === '') {
|
||||
@@ -494,6 +539,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (window.innerWidth <= 767.98) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
|
||||
queryParamsHandling: 'merge',
|
||||
preserveFragment: true,
|
||||
queryParams: { mode: 'details' },
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
this.seoService.setTitle(
|
||||
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
||||
);
|
||||
@@ -572,7 +625,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (tx.firstSeen) {
|
||||
this.transactionTime = tx.firstSeen;
|
||||
} else {
|
||||
this.getTransactionTime();
|
||||
this.transactionTimes$.next(tx.txid);
|
||||
}
|
||||
} else {
|
||||
this.fetchAcceleration$.next(tx.status.block_height);
|
||||
@@ -598,8 +651,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
ancestors: tx.ancestors,
|
||||
bestDescendant: tx.bestDescendant,
|
||||
});
|
||||
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant);
|
||||
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) >= 0.1));
|
||||
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant || tx.descendants);
|
||||
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && tx.effectiveFeePerVsize !== (this.tx.fee / (this.tx.weight / 4)) && tx.effectiveFeePerVsize !== (tx.fee / Math.ceil(tx.weight / 4)));
|
||||
} else {
|
||||
this.fetchCpfp$.next(this.tx.txid);
|
||||
}
|
||||
@@ -625,7 +678,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
);
|
||||
|
||||
this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
|
||||
this.txConfirmedSubscription = this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
|
||||
if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
|
||||
if (this.tx.acceleration) {
|
||||
this.waitingForAccelerationInfo = true;
|
||||
@@ -729,7 +782,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.accelerationPositions,
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
@@ -763,28 +816,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
getTransactionTime() {
|
||||
this.isLoadingFirstSeen = true;
|
||||
this.apiService
|
||||
.getTransactionTimes$([this.tx.txid])
|
||||
.pipe(
|
||||
retry({ count: 2, delay: 2000 }),
|
||||
catchError(() => {
|
||||
this.isLoadingFirstSeen = false;
|
||||
return throwError(() => new Error(''));
|
||||
})
|
||||
)
|
||||
.subscribe((transactionTimes) => {
|
||||
if (transactionTimes?.length && transactionTimes[0]) {
|
||||
this.transactionTime = transactionTimes[0];
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.getTransactionTime();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setCpfpInfo(cpfpInfo: CpfpInfo): void {
|
||||
if (!cpfpInfo || !this.tx) {
|
||||
this.cpfpInfo = null;
|
||||
@@ -814,6 +845,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.tx.acceleration = cpfpInfo.acceleration;
|
||||
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
||||
this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
|
||||
this.tx.feeDelta = cpfpInfo.feeDelta;
|
||||
this.setIsAccelerated(firstCpfp);
|
||||
}
|
||||
|
||||
@@ -828,8 +860,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.sigops = this.cpfpInfo.sigops;
|
||||
this.adjustedVsize = this.cpfpInfo.adjustedVsize;
|
||||
}
|
||||
this.hasCpfp =!!(this.cpfpInfo && (this.cpfpInfo.bestDescendant || this.cpfpInfo.descendants?.length || this.cpfpInfo.ancestors?.length));
|
||||
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
|
||||
this.hasCpfp =!!(this.cpfpInfo && relatives.length);
|
||||
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && this.tx.effectiveFeePerVsize !== (this.tx.fee / (this.tx.weight / 4)) && this.tx.effectiveFeePerVsize !== (this.tx.fee / Math.ceil(this.tx.weight / 4)));
|
||||
}
|
||||
|
||||
setIsAccelerated(initialState: boolean = false) {
|
||||
@@ -841,7 +873,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
if (this.isAcceleration) {
|
||||
// this immediately returns cached stats if we fetched them recently
|
||||
this.miningService.getMiningStats('1w').subscribe(stats => {
|
||||
this.miningService.getMiningStats('1m').subscribe(stats => {
|
||||
this.miningStats = stats;
|
||||
this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable
|
||||
});
|
||||
@@ -1057,6 +1089,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
this.fetchCpfpSubscription.unsubscribe();
|
||||
this.transactionTimesSubscription.unsubscribe();
|
||||
this.fetchRbfSubscription.unsubscribe();
|
||||
this.fetchCachedTxSubscription.unsubscribe();
|
||||
this.fetchAccelerationSubscription.unsubscribe();
|
||||
@@ -1070,6 +1103,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.blocksSubscription.unsubscribe();
|
||||
this.miningSubscription?.unsubscribe();
|
||||
this.auditSubscription?.unsubscribe();
|
||||
this.txConfirmedSubscription?.unsubscribe();
|
||||
this.currencyChangeSubscription?.unsubscribe();
|
||||
this.leaveTransaction();
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<span *ngSwitchCase="'output'" i18n="transaction.output">Output</span>
|
||||
<span *ngSwitchCase="'fee'" i18n="transaction.fee|Transaction fee">Fee</span>
|
||||
</ng-container>
|
||||
<span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span>
|
||||
<span *ngIf="line.type !== 'fee'"> #{{ line.index }}</span>
|
||||
<ng-container [ngSwitch]="line.type">
|
||||
<span *ngSwitchCase="'input'">
|
||||
<ng-container *ngIf="line.status?.block_height">
|
||||
@@ -73,7 +73,7 @@
|
||||
<app-truncate [text]="line.txid"></app-truncate>
|
||||
</p>
|
||||
<ng-container [ngSwitch]="line.type">
|
||||
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span> #{{ line.vout + 1 }}
|
||||
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span> #{{ line.vout }}
|
||||
<ng-container *ngIf="line.status?.block_height">
|
||||
<ng-container *ngIf="line.blockHeight; else noBlockHeight">
|
||||
<ng-container *ngTemplateOutlet="nBlocksEarlier; context:{n: line.blockHeight - line?.status?.block_height, connector: true}"></ng-container>
|
||||
@@ -83,7 +83,7 @@
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</p>
|
||||
<p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span> #{{ line.vin + 1 }}
|
||||
<p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span> #{{ line.vin }}
|
||||
<ng-container *ngIf="line.blockHeight">
|
||||
<ng-container *ngIf="line?.status?.block_height; else noBlockHeight">
|
||||
<ng-container *ngTemplateOutlet="nBlocksLater; context:{n: line?.status?.block_height - line.blockHeight, connector: true}"></ng-container>
|
||||
|
||||
@@ -8993,7 +8993,7 @@ export const restApiDocsData = [
|
||||
fragment: "accelerator-estimate",
|
||||
title: "POST Calculate Estimated Costs",
|
||||
description: {
|
||||
default: "<p>Returns estimated costs to accelerate a transaction. Optionally set the <code>api_key</code> header to get customized estimation.</p>"
|
||||
default: "<p>Returns estimated costs to accelerate a transaction. Optionally set the <code>X-Mempool-Auth</code> header to get customized estimation.</p>"
|
||||
},
|
||||
urlString: "/v1/services/accelerator/estimate",
|
||||
showConditions: [""],
|
||||
@@ -9009,7 +9009,7 @@ export const restApiDocsData = [
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: ["txInput=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29"],
|
||||
headers: "api_key: stacksats",
|
||||
headers: "X-Mempool-Auth: stacksats",
|
||||
response: `{
|
||||
"txSummary": {
|
||||
"txid": "ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29",
|
||||
@@ -9240,7 +9240,7 @@ export const restApiDocsData = [
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: [],
|
||||
headers: "api_key: stacksats",
|
||||
headers: "X-Mempool-Auth: stacksats",
|
||||
response: `[
|
||||
{
|
||||
"type": "Bitcoin",
|
||||
@@ -9288,7 +9288,7 @@ export const restApiDocsData = [
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: [],
|
||||
headers: "api_key: stacksats",
|
||||
headers: "X-Mempool-Auth: stacksats",
|
||||
response: `{
|
||||
"balance": 99900000,
|
||||
"hold": 101829,
|
||||
@@ -9322,7 +9322,7 @@ export const restApiDocsData = [
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: ["txInput=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29&userBid=21000000"],
|
||||
headers: "api_key: stacksats",
|
||||
headers: "X-Mempool-Auth: stacksats",
|
||||
response: `HTTP/1.1 200 OK`,
|
||||
},
|
||||
}
|
||||
@@ -9352,7 +9352,7 @@ export const restApiDocsData = [
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: [],
|
||||
headers: "api_key: stacksats",
|
||||
headers: "X-Mempool-Auth: stacksats",
|
||||
response: `[
|
||||
{
|
||||
"id": 89,
|
||||
|
||||
@@ -17,11 +17,13 @@ export interface Transaction {
|
||||
feePerVsize?: number;
|
||||
effectiveFeePerVsize?: number;
|
||||
ancestors?: Ancestor[];
|
||||
descendants?: Ancestor[];
|
||||
bestDescendant?: BestDescendant | null;
|
||||
cpfpChecked?: boolean;
|
||||
acceleration?: boolean;
|
||||
acceleratedBy?: number[];
|
||||
acceleratedAt?: number;
|
||||
feeDelta?: number;
|
||||
deleteAfter?: number;
|
||||
_unblinded?: any;
|
||||
_deduced?: boolean;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user