Compare commits
260 Commits
mononaut/p
...
orangesurf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c08c206c8b | ||
|
|
51a28b2e01 | ||
|
|
966adf5963 | ||
|
|
ae7b17c4fd | ||
|
|
5b4bc9fe19 | ||
|
|
89eb7ec90b | ||
|
|
a646c5f22f | ||
|
|
c345f2164b | ||
|
|
9791ee018d | ||
|
|
7c83b85e3e | ||
|
|
63942af720 | ||
|
|
4da5910e0b | ||
|
|
025788e78c | ||
|
|
88a9524064 | ||
|
|
c216e849e6 | ||
|
|
c86768edc9 | ||
|
|
4283d30ce0 | ||
|
|
e2eeaebfc7 | ||
|
|
90d87aaaca | ||
|
|
b2e9275ad9 | ||
|
|
181e816c1a | ||
|
|
758ca4e93a | ||
|
|
a33f915c7a | ||
|
|
75c6d006ff | ||
|
|
1fe983a299 | ||
|
|
cd8e3e2604 | ||
|
|
dfc4309d9e | ||
|
|
0a70273456 | ||
|
|
62c9a88235 | ||
|
|
851e07b50b | ||
|
|
e1bbec074b | ||
|
|
9fcafeeeb0 | ||
|
|
e986cfd30b | ||
|
|
2584a1f2b0 | ||
|
|
1c92394563 | ||
|
|
36398ca57a | ||
|
|
6b978f9262 | ||
|
|
12cf130c00 | ||
|
|
ba933a81c3 | ||
|
|
7de7081d67 | ||
|
|
6b933c202f | ||
|
|
f1525b7df5 | ||
|
|
62c4af1a0c | ||
|
|
4ac98bc483 | ||
|
|
ed07daebca | ||
|
|
f8bbf7783c | ||
|
|
fac2fab16a | ||
|
|
76de4e34d8 | ||
|
|
6109f25aa9 | ||
|
|
86303175e9 | ||
|
|
a670131f40 | ||
|
|
c5ce3167f3 | ||
|
|
9913254a5c | ||
|
|
fbb4ba39c2 | ||
|
|
22bf0fe928 | ||
|
|
3b30765070 | ||
|
|
63fde7d0b6 | ||
|
|
cf338970bf | ||
|
|
08e046ea2a | ||
|
|
5697710c23 | ||
|
|
b9e050e24b | ||
|
|
ff819673e5 | ||
|
|
2e2801a4ab | ||
|
|
f52b17ca7a | ||
|
|
3d25235705 | ||
|
|
2cbc6783a4 | ||
|
|
406bf5d3b7 | ||
|
|
f5bf883de9 | ||
|
|
c06cade1ca | ||
|
|
c23a872887 | ||
|
|
cd5a51098b | ||
|
|
0b449347c1 | ||
|
|
d3f8876818 | ||
|
|
d2da81e039 | ||
|
|
66d88abdc5 | ||
|
|
be2b9a9c2e | ||
|
|
63ba273dbe | ||
|
|
8ae4c75c1a | ||
|
|
1cece1037a | ||
|
|
478873302c | ||
|
|
3cff01b21e | ||
|
|
f67848b043 | ||
|
|
f0c88ff6cc | ||
|
|
da5adc3e4e | ||
|
|
bebd2ea028 | ||
|
|
e7e25e1632 | ||
|
|
c4130fd5b9 | ||
|
|
dcad18b297 | ||
|
|
d380aad98c | ||
|
|
85c9c79699 | ||
|
|
107e0be59f | ||
|
|
e654170d0b | ||
|
|
652100f774 | ||
|
|
0f04f751e1 | ||
|
|
d3055dab54 | ||
|
|
643402c046 | ||
|
|
e95d5a7982 | ||
|
|
3103ef15e5 | ||
|
|
5d5e9e8219 | ||
|
|
99fd4500e4 | ||
|
|
69081ed647 | ||
|
|
c8bcd4f04f | ||
|
|
7f4fd83ad2 | ||
|
|
70722dfc9c | ||
|
|
407b4c53a6 | ||
|
|
c11551de7b | ||
|
|
2d5964b81e | ||
|
|
98e3c7b9cf | ||
|
|
2de57a8074 | ||
|
|
8badacf123 | ||
|
|
40b387a1e0 | ||
|
|
e5d2788736 | ||
|
|
f57436f511 | ||
|
|
75cc844676 | ||
|
|
5d05dd7089 | ||
|
|
35e108aa1c | ||
|
|
3f06b38767 | ||
|
|
9844c3d275 | ||
|
|
04eeb19bb9 | ||
|
|
671540af78 | ||
|
|
1a732f18fc | ||
|
|
729fb3bb9d | ||
|
|
fc56f273d4 | ||
|
|
642be969a3 | ||
|
|
d69cdacd5e | ||
|
|
ec8fc53dcb | ||
|
|
108d1762d6 | ||
|
|
879039ca8c | ||
|
|
047d1463e7 | ||
|
|
8fc60fa086 | ||
|
|
bce68ee37f | ||
|
|
2f25c128c1 | ||
|
|
a76a600d3f | ||
|
|
afdb419beb | ||
|
|
8bd2aa3dd3 | ||
|
|
a3f2c42b8e | ||
|
|
dbcd900056 | ||
|
|
5a6d6fae41 | ||
|
|
6bb666ba5e | ||
|
|
8dc80eadf3 | ||
|
|
505532f812 | ||
|
|
89eb02dad0 | ||
|
|
94a7b710c5 | ||
|
|
1e01f88c15 | ||
|
|
d471616a24 | ||
|
|
9b8f70a0ae | ||
|
|
ab079e9372 | ||
|
|
b77a16233b | ||
|
|
894075493b | ||
|
|
954512cd8e | ||
|
|
deaf6ad6a5 | ||
|
|
98443b48ba | ||
|
|
a7dc8793c2 | ||
|
|
84b8e5d472 | ||
|
|
1eb425897e | ||
|
|
e768072799 | ||
|
|
206706180f | ||
|
|
0e3b3a0e00 | ||
|
|
28733c1e97 | ||
|
|
7e74a26de2 | ||
|
|
6d920e0ed3 | ||
|
|
88e5f8a6af | ||
|
|
ae82ac8368 | ||
|
|
15cba58144 | ||
|
|
2c820f1cc0 | ||
|
|
4070492584 | ||
|
|
82a43e25e0 | ||
|
|
5e45d8f3bc | ||
|
|
e5709235f3 | ||
|
|
9d0bfdfa88 | ||
|
|
59c513f2a5 | ||
|
|
76a07315f3 | ||
|
|
5723f167df | ||
|
|
6ac328c979 | ||
|
|
85e52d24c3 | ||
|
|
7717c15666 | ||
|
|
61d7fd490a | ||
|
|
ccf952983b | ||
|
|
602d7ce207 | ||
|
|
64a51803c4 | ||
|
|
a63e68e9e3 | ||
|
|
d4d17fa167 | ||
|
|
4cd8d70de5 | ||
|
|
dc26c6f105 | ||
|
|
365954f5b4 | ||
|
|
ff38073280 | ||
|
|
eeebfde33c | ||
|
|
fa12233667 | ||
|
|
19477c4ee3 | ||
|
|
e4b56bac88 | ||
|
|
d390fa8671 | ||
|
|
f9f9c62608 | ||
|
|
d9966143c1 | ||
|
|
57ab82ae7a | ||
|
|
756f6d8abe | ||
|
|
f0840a51d9 | ||
|
|
eac8f8c2c6 | ||
|
|
20f61fc6a0 | ||
|
|
6454892d48 | ||
|
|
35d7c55c1d | ||
|
|
86fe6a802b | ||
|
|
d4568b631d | ||
|
|
2d30c0b588 | ||
|
|
1aea3fcac5 | ||
|
|
5cfd599018 | ||
|
|
d8f2462ff0 | ||
|
|
85091e1f3a | ||
|
|
3a8d19062f | ||
|
|
6913946079 | ||
|
|
fdd18317f9 | ||
|
|
a7c64c0df3 | ||
|
|
f2fb2f98f1 | ||
|
|
7aad664112 | ||
|
|
fa040ca19f | ||
|
|
00887bc24b | ||
|
|
ab8b557e73 | ||
|
|
5c0a59d2f6 | ||
|
|
29cbdf6cd5 | ||
|
|
08b68ef8ba | ||
|
|
1ae34e069c | ||
|
|
5bad829afc | ||
|
|
562cd5683a | ||
|
|
cbf2395009 | ||
|
|
c393483590 | ||
|
|
cbe1ec4e72 | ||
|
|
c6a92083a8 | ||
|
|
8c4b488251 | ||
|
|
3639dcc92a | ||
|
|
a9a3623539 | ||
|
|
27966ad8ec | ||
|
|
5d1ebc6d31 | ||
|
|
a68c2a2be6 | ||
|
|
2bc3352785 | ||
|
|
50460d4025 | ||
|
|
7d3f82eca0 | ||
|
|
20026f974f | ||
|
|
3d964fcdfa | ||
|
|
9e8b2957d0 | ||
|
|
c04e92a686 | ||
|
|
2dd6dd9233 | ||
|
|
e9b72776ed | ||
|
|
23df1f012c | ||
|
|
14a41b3108 | ||
|
|
81d1a809d2 | ||
|
|
a000438277 | ||
|
|
e3fffd8fb2 | ||
|
|
5dfd1a495e | ||
|
|
a463fc289f | ||
|
|
a8e6d9b4b9 | ||
|
|
9ed7e80c44 | ||
|
|
596d55e413 | ||
|
|
d630e7217a | ||
|
|
0e1a9d8619 | ||
|
|
d09668aaa6 | ||
|
|
93aa08b60d | ||
|
|
4664e55513 | ||
|
|
1dd66e6695 | ||
|
|
6199216c54 | ||
|
|
b988a4c526 | ||
|
|
89be841e64 |
@@ -52,6 +52,7 @@
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "http://127.0.0.1:3000",
|
||||
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
|
||||
"BATCH_QUERY_BASE_SIZE": 1000,
|
||||
"RETRY_UNIX_SOCKET_AFTER": 30000,
|
||||
"REQUEST_TIMEOUT": 10000,
|
||||
"FALLBACK_TIMEOUT": 5000,
|
||||
@@ -132,6 +133,11 @@
|
||||
"BISQ_URL": "https://bisq.markets/api",
|
||||
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": false,
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
|
||||
"BATCH_QUERY_BASE_SIZE": 5000
|
||||
},
|
||||
"REPLICATION": {
|
||||
"ENABLED": false,
|
||||
"AUDIT": false,
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "__ESPLORA_REST_API_URL__",
|
||||
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
|
||||
"BATCH_QUERY_BASE_SIZE": 1000,
|
||||
"RETRY_UNIX_SOCKET_AFTER": 888,
|
||||
"REQUEST_TIMEOUT": 10000,
|
||||
"FALLBACK_TIMEOUT": 5000,
|
||||
@@ -140,6 +141,7 @@
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": false,
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock"
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
|
||||
"BATCH_QUERY_BASE_SIZE": 5000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ describe('Mempool Backend Config', () => {
|
||||
expect(config.ESPLORA).toStrictEqual({
|
||||
REST_API_URL: 'http://127.0.0.1:3000',
|
||||
UNIX_SOCKET_PATH: null,
|
||||
BATCH_QUERY_BASE_SIZE: 1000,
|
||||
RETRY_UNIX_SOCKET_AFTER: 30000,
|
||||
REQUEST_TIMEOUT: 10000,
|
||||
FALLBACK_TIMEOUT: 5000,
|
||||
@@ -144,7 +145,8 @@ describe('Mempool Backend Config', () => {
|
||||
|
||||
expect(config.REDIS).toStrictEqual({
|
||||
ENABLED: false,
|
||||
UNIX_SOCKET_PATH: ''
|
||||
UNIX_SOCKET_PATH: '',
|
||||
BATCH_QUERY_BASE_SIZE: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ class Audit {
|
||||
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
|
||||
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 0, similarity: 1 };
|
||||
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
||||
}
|
||||
|
||||
const matches: string[] = []; // present in both mined block and template
|
||||
@@ -144,7 +144,12 @@ class Audit {
|
||||
|
||||
const numCensored = Object.keys(isCensored).length;
|
||||
const numMatches = matches.length - 1; // adjust for coinbase tx
|
||||
const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
|
||||
let score = 0;
|
||||
if (numMatches <= 0 && numCensored <= 0) {
|
||||
score = 1;
|
||||
} else if (numMatches > 0) {
|
||||
score = (numMatches / (numMatches + numCensored));
|
||||
}
|
||||
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface AbstractBitcoinApi {
|
||||
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
||||
$getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
|
||||
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
|
||||
$getAllMempoolTransactions(lastTxid: string);
|
||||
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number);
|
||||
$getTransactionHex(txId: string): Promise<string>;
|
||||
$getBlockHeightTip(): Promise<number>;
|
||||
$getBlockHashTip(): Promise<string>;
|
||||
|
||||
@@ -77,7 +77,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.');
|
||||
}
|
||||
|
||||
$getAllMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
|
||||
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
|
||||
throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.');
|
||||
|
||||
}
|
||||
|
||||
@@ -573,7 +573,9 @@ class BitcoinRoutes {
|
||||
}
|
||||
|
||||
try {
|
||||
const addressData = await bitcoinApi.$getScriptHash(req.params.scripthash);
|
||||
// electrum expects scripthashes in little-endian
|
||||
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
|
||||
const addressData = await bitcoinApi.$getScriptHash(electrumScripthash);
|
||||
res.json(addressData);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
@@ -590,11 +592,13 @@ class BitcoinRoutes {
|
||||
}
|
||||
|
||||
try {
|
||||
// electrum expects scripthashes in little-endian
|
||||
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
|
||||
let lastTxId: string = '';
|
||||
if (req.query.after_txid && typeof req.query.after_txid === 'string') {
|
||||
lastTxId = req.query.after_txid;
|
||||
}
|
||||
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.scripthash, lastTxId);
|
||||
const transactions = await bitcoinApi.$getScriptHashTransactions(electrumScripthash, lastTxId);
|
||||
res.json(transactions);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
|
||||
@@ -8,8 +8,9 @@ import logger from '../../logger';
|
||||
interface FailoverHost {
|
||||
host: string,
|
||||
rtts: number[],
|
||||
rtt: number
|
||||
rtt: number,
|
||||
failures: number,
|
||||
latestHeight?: number,
|
||||
socket?: boolean,
|
||||
outOfSync?: boolean,
|
||||
unreachable?: boolean,
|
||||
@@ -92,6 +93,7 @@ class FailoverRouter {
|
||||
host.rtts.unshift(rtt);
|
||||
host.rtts.slice(0, 5);
|
||||
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
|
||||
host.latestHeight = height;
|
||||
if (height == null || isNaN(height) || (maxHeight - height > 2)) {
|
||||
host.outOfSync = true;
|
||||
} else {
|
||||
@@ -99,22 +101,23 @@ class FailoverRouter {
|
||||
}
|
||||
host.unreachable = false;
|
||||
} else {
|
||||
host.outOfSync = true;
|
||||
host.unreachable = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.sortHosts();
|
||||
|
||||
logger.debug(`Tomahawk ranking: ${this.hosts.map(host => '\navg rtt ' + Math.round(host.rtt).toString().padStart(5, ' ') + ' | reachable? ' + (!host.unreachable || false).toString().padStart(5, ' ') + ' | in sync? ' + (!host.outOfSync || false).toString().padStart(5, ' ') + ` | ${host.host}`).join('')}`);
|
||||
logger.debug(`Tomahawk ranking:\n${this.hosts.map((host, index) => this.formatRanking(index, host, this.activeHost, maxHeight)).join('\n')}`);
|
||||
|
||||
// switch if the current host is out of sync or significantly slower than the next best alternative
|
||||
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) {
|
||||
if (this.activeHost.unreachable) {
|
||||
logger.warn(`Unable to reach ${this.activeHost.host}, failing over to next best alternative`);
|
||||
logger.warn(`🚨🚨🚨 Unable to reach ${this.activeHost.host}, failing over to next best alternative 🚨🚨🚨`);
|
||||
} else if (this.activeHost.outOfSync) {
|
||||
logger.warn(`${this.activeHost.host} has fallen behind, failing over to next best alternative`);
|
||||
logger.warn(`🚨🚨🚨 ${this.activeHost.host} has fallen behind, failing over to next best alternative 🚨🚨🚨`);
|
||||
} else {
|
||||
logger.debug(`${this.activeHost.host} is no longer the best esplora host`);
|
||||
logger.debug(`🛠️ ${this.activeHost.host} is no longer the best esplora host 🛠️`);
|
||||
}
|
||||
this.electHost();
|
||||
}
|
||||
@@ -122,6 +125,11 @@ class FailoverRouter {
|
||||
this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval);
|
||||
}
|
||||
|
||||
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
|
||||
const heightStatus = host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅');
|
||||
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : ' - '} ${host.unreachable ? '🔥' : '✅'} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
|
||||
}
|
||||
|
||||
// sort hosts by connection quality, and update default fallback
|
||||
private sortHosts(): void {
|
||||
// sort by connection quality
|
||||
@@ -156,7 +164,7 @@ class FailoverRouter {
|
||||
private addFailure(host: FailoverHost): FailoverHost {
|
||||
host.failures++;
|
||||
if (host.failures > 5 && this.multihost) {
|
||||
logger.warn(`Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative`);
|
||||
logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`);
|
||||
this.electHost();
|
||||
return this.activeHost;
|
||||
} else {
|
||||
@@ -225,8 +233,8 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/mempool/txs', txids, 'json');
|
||||
}
|
||||
|
||||
async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
|
||||
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
|
||||
async $getAllMempoolTransactions(lastSeenTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
|
||||
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''), 'json', max_txs ? { max_txs } : null);
|
||||
}
|
||||
|
||||
$getTransactionHex(txId: string): Promise<string> {
|
||||
|
||||
@@ -761,8 +761,13 @@ class Blocks {
|
||||
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
|
||||
|
||||
if (!fastForwarded) {
|
||||
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
|
||||
let lastestPriceId;
|
||||
try {
|
||||
lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
|
||||
} catch (e) {
|
||||
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
|
||||
await blocksRepository.$saveBlockPrices([{
|
||||
height: blockExtended.height,
|
||||
@@ -771,9 +776,7 @@ class Blocks {
|
||||
this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`);
|
||||
} else {
|
||||
logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
|
||||
setTimeout(() => {
|
||||
indexer.runSingleTask('blocksPrices');
|
||||
}, 10000);
|
||||
indexer.scheduleSingleTask('blocksPrices', 10000);
|
||||
}
|
||||
|
||||
// Save blocks summary for visualization if it's enabled
|
||||
|
||||
@@ -80,7 +80,13 @@ class ChannelsApi {
|
||||
|
||||
public async $searchChannelsById(search: string): Promise<any[]> {
|
||||
try {
|
||||
const searchStripped = search.replace(/[^0-9x]/g, '') + '%';
|
||||
// restrict search to valid id/short_id prefix formats
|
||||
let searchStripped = search.match(/[0-9]+[0-9x]*/)?.[0] || '';
|
||||
if (!searchStripped.length) {
|
||||
return [];
|
||||
}
|
||||
// add wildcard to search by prefix
|
||||
searchStripped += '%';
|
||||
const query = `SELECT id, short_id, capacity, status FROM channels WHERE id LIKE ? OR short_id LIKE ? LIMIT 10`;
|
||||
const [rows]: any = await DB.query(query, [searchStripped, searchStripped]);
|
||||
return rows;
|
||||
|
||||
@@ -15,11 +15,12 @@ class FeeApi {
|
||||
constructor() { }
|
||||
|
||||
defaultFee = Common.isLiquid() ? 0.1 : 1;
|
||||
minimumIncrement = Common.isLiquid() ? 0.1 : 1;
|
||||
|
||||
public getRecommendedFee(): RecommendedFees {
|
||||
const pBlocks = projectedBlocks.getMempoolBlocks();
|
||||
const mPool = mempool.getMempoolInfo();
|
||||
const minimumFee = Math.ceil(mPool.mempoolminfee * 100000);
|
||||
const minimumFee = this.roundUpToNearest(mPool.mempoolminfee * 100000, this.minimumIncrement);
|
||||
const defaultMinFee = Math.max(minimumFee, this.defaultFee);
|
||||
|
||||
if (!pBlocks.length) {
|
||||
@@ -58,7 +59,11 @@ class FeeApi {
|
||||
const multiplier = (pBlock.blockVSize - 500000) / 500000;
|
||||
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
|
||||
}
|
||||
return Math.ceil(useFee);
|
||||
return this.roundUpToNearest(useFee, this.minimumIncrement);
|
||||
}
|
||||
|
||||
private roundUpToNearest(value: number, nearest: number): number {
|
||||
return Math.ceil(value / nearest) * nearest;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,9 +44,13 @@ export enum FeatureBits {
|
||||
KeysendOptional = 55,
|
||||
ScriptEnforcedLeaseRequired = 2022,
|
||||
ScriptEnforcedLeaseOptional = 2023,
|
||||
SimpleTaprootChannelsRequiredFinal = 80,
|
||||
SimpleTaprootChannelsOptionalFinal = 81,
|
||||
SimpleTaprootChannelsRequiredStaging = 180,
|
||||
SimpleTaprootChannelsOptionalStaging = 181,
|
||||
MaxBolt11Feature = 5114,
|
||||
};
|
||||
|
||||
|
||||
export const FeaturesMap = new Map<FeatureBits, string>([
|
||||
[FeatureBits.DataLossProtectRequired, 'data-loss-protect'],
|
||||
[FeatureBits.DataLossProtectOptional, 'data-loss-protect'],
|
||||
@@ -85,6 +89,10 @@ export const FeaturesMap = new Map<FeatureBits, string>([
|
||||
[FeatureBits.ZeroConfOptional, 'zero-conf'],
|
||||
[FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'],
|
||||
[FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'],
|
||||
[FeatureBits.SimpleTaprootChannelsRequiredFinal, 'taproot-channels'],
|
||||
[FeatureBits.SimpleTaprootChannelsOptionalFinal, 'taproot-channels'],
|
||||
[FeatureBits.SimpleTaprootChannelsRequiredStaging, 'taproot-channels-staging'],
|
||||
[FeatureBits.SimpleTaprootChannelsOptionalStaging, 'taproot-channels-staging'],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ class Mempool {
|
||||
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
||||
private spendMap = new Map<string, MempoolTransactionExtended>();
|
||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
|
||||
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
|
||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
||||
@@ -126,7 +126,7 @@ class Mempool {
|
||||
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
|
||||
while (!done) {
|
||||
try {
|
||||
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid);
|
||||
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid, config.ESPLORA.BATCH_QUERY_BASE_SIZE);
|
||||
if (result) {
|
||||
for (const tx of result) {
|
||||
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
|
||||
@@ -235,7 +235,7 @@ class Mempool {
|
||||
|
||||
if (!loaded) {
|
||||
const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]);
|
||||
const sliceLength = 10000;
|
||||
const sliceLength = config.ESPLORA.BATCH_QUERY_BASE_SIZE;
|
||||
for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) {
|
||||
const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false);
|
||||
|
||||
@@ -15,6 +15,13 @@ import bitcoinApi from '../bitcoin/bitcoin-api-factory';
|
||||
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||
import database from '../../database';
|
||||
|
||||
interface DifficultyBlock {
|
||||
timestamp: number,
|
||||
height: number,
|
||||
bits: number,
|
||||
difficulty: number,
|
||||
}
|
||||
|
||||
class Mining {
|
||||
private blocksPriceIndexingRunning = false;
|
||||
public lastHashrateIndexingDate: number | null = null;
|
||||
@@ -421,6 +428,7 @@ class Mining {
|
||||
indexedHeights[height] = true;
|
||||
}
|
||||
|
||||
// gets {time, height, difficulty, bits} of blocks in ascending order of height
|
||||
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
|
||||
let currentDifficulty = genesisBlock.difficulty;
|
||||
@@ -436,41 +444,45 @@ class Mining {
|
||||
});
|
||||
}
|
||||
|
||||
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
|
||||
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
|
||||
currentBits = oldestConsecutiveBlock.bits;
|
||||
currentDifficulty = oldestConsecutiveBlock.difficulty;
|
||||
if (!blocks?.length) {
|
||||
// no blocks in database yet
|
||||
return;
|
||||
}
|
||||
|
||||
const oldestConsecutiveBlock = this.getOldestConsecutiveBlock(blocks);
|
||||
|
||||
currentBits = oldestConsecutiveBlock.bits;
|
||||
currentDifficulty = oldestConsecutiveBlock.difficulty;
|
||||
|
||||
let totalBlockChecked = 0;
|
||||
let timer = new Date().getTime() / 1000;
|
||||
|
||||
for (const block of blocks) {
|
||||
// skip until the first block after the oldest consecutive block
|
||||
if (block.height <= oldestConsecutiveBlock.height) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// difficulty has changed between two consecutive blocks!
|
||||
if (block.bits !== currentBits) {
|
||||
if (indexedHeights[block.height] === true) { // Already indexed
|
||||
if (block.height >= oldestConsecutiveBlock.height) {
|
||||
currentDifficulty = block.difficulty;
|
||||
currentBits = block.bits;
|
||||
}
|
||||
continue;
|
||||
// skip if already indexed
|
||||
if (indexedHeights[block.height] !== true) {
|
||||
let adjustment = block.difficulty / currentDifficulty;
|
||||
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
|
||||
|
||||
await DifficultyAdjustmentsRepository.$saveAdjustments({
|
||||
time: block.time,
|
||||
height: block.height,
|
||||
difficulty: block.difficulty,
|
||||
adjustment: adjustment,
|
||||
});
|
||||
|
||||
totalIndexed++;
|
||||
}
|
||||
|
||||
let adjustment = block.difficulty / currentDifficulty;
|
||||
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
|
||||
|
||||
await DifficultyAdjustmentsRepository.$saveAdjustments({
|
||||
time: block.time,
|
||||
height: block.height,
|
||||
difficulty: block.difficulty,
|
||||
adjustment: adjustment,
|
||||
});
|
||||
|
||||
totalIndexed++;
|
||||
if (block.height >= oldestConsecutiveBlock.height) {
|
||||
currentDifficulty = block.difficulty;
|
||||
currentBits = block.bits;
|
||||
}
|
||||
}
|
||||
// update the current difficulty
|
||||
currentDifficulty = block.difficulty;
|
||||
currentBits = block.bits;
|
||||
}
|
||||
|
||||
totalBlockChecked++;
|
||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||
@@ -633,6 +645,17 @@ class Mining {
|
||||
default: return 86400 * scale;
|
||||
}
|
||||
}
|
||||
|
||||
// Finds the oldest block in a consecutive chain back from the tip
|
||||
// assumes `blocks` is sorted in ascending height order
|
||||
private getOldestConsecutiveBlock(blocks: DifficultyBlock[]): DifficultyBlock {
|
||||
for (let i = blocks.length - 1; i > 0; i--) {
|
||||
if ((blocks[i].height - blocks[i - 1].height) > 1) {
|
||||
return blocks[i];
|
||||
}
|
||||
}
|
||||
return blocks[0];
|
||||
}
|
||||
}
|
||||
|
||||
export default new Mining();
|
||||
|
||||
@@ -480,14 +480,15 @@ class RbfCache {
|
||||
};
|
||||
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const sliceLength = 250;
|
||||
let processedCount = 0;
|
||||
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 40);
|
||||
for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) {
|
||||
const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
processedCount += slice.length;
|
||||
try {
|
||||
const txs = await bitcoinApi.$getRawTransactions(slice);
|
||||
logger.debug(`fetched ${slice.length} cached rbf transactions`);
|
||||
processTxs(txs);
|
||||
logger.debug(`processed ${slice.length} cached rbf transactions`);
|
||||
logger.debug(`fetched and processed ${processedCount} of ${txids.length} cached rbf transactions (${(processedCount / txids.length * 100).toFixed(2)}%)`);
|
||||
} catch (err) {
|
||||
logger.err(`failed to fetch or process ${slice.length} cached rbf transactions`);
|
||||
}
|
||||
|
||||
@@ -122,8 +122,9 @@ class RedisCache {
|
||||
async $removeTransactions(transactions: string[]) {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) {
|
||||
const slice = transactions.slice(i * 10000, (i + 1) * 10000);
|
||||
const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE;
|
||||
for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) {
|
||||
const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
|
||||
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
|
||||
}
|
||||
|
||||
@@ -94,9 +94,13 @@ class WebsocketHandler {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.wss.on('connection', (client: WebSocket) => {
|
||||
this.wss.on('connection', (client: WebSocket, req) => {
|
||||
this.numConnected++;
|
||||
client.on('error', logger.info);
|
||||
client['remoteAddress'] = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown';
|
||||
client.on('error', (e) => {
|
||||
logger.info(`websocket client error from ${client['remoteAddress']}: ` + (e instanceof Error ? e.message : e));
|
||||
client.close();
|
||||
});
|
||||
client.on('close', () => {
|
||||
this.numDisconnected++;
|
||||
});
|
||||
@@ -282,7 +286,8 @@ class WebsocketHandler {
|
||||
client.send(serializedResponse);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e));
|
||||
logger.debug(`Error parsing websocket message from ${client['remoteAddress']}: ` + (e instanceof Error ? e.message : e));
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,6 +43,7 @@ interface IConfig {
|
||||
ESPLORA: {
|
||||
REST_API_URL: string;
|
||||
UNIX_SOCKET_PATH: string | void | null;
|
||||
BATCH_QUERY_BASE_SIZE: number;
|
||||
RETRY_UNIX_SOCKET_AFTER: number;
|
||||
REQUEST_TIMEOUT: number;
|
||||
FALLBACK_TIMEOUT: number;
|
||||
@@ -151,6 +152,7 @@ interface IConfig {
|
||||
REDIS: {
|
||||
ENABLED: boolean;
|
||||
UNIX_SOCKET_PATH: string;
|
||||
BATCH_QUERY_BASE_SIZE: number;
|
||||
},
|
||||
}
|
||||
|
||||
@@ -195,6 +197,7 @@ const defaults: IConfig = {
|
||||
'ESPLORA': {
|
||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||
'UNIX_SOCKET_PATH': null,
|
||||
'BATCH_QUERY_BASE_SIZE': 1000,
|
||||
'RETRY_UNIX_SOCKET_AFTER': 30000,
|
||||
'REQUEST_TIMEOUT': 10000,
|
||||
'FALLBACK_TIMEOUT': 5000,
|
||||
@@ -303,6 +306,7 @@ const defaults: IConfig = {
|
||||
'REDIS': {
|
||||
'ENABLED': false,
|
||||
'UNIX_SOCKET_PATH': '',
|
||||
'BATCH_QUERY_BASE_SIZE': 5000,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@ import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import config from './config';
|
||||
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
|
||||
import { LogLevel } from './logger';
|
||||
import logger from './logger';
|
||||
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
class DB {
|
||||
constructor() {
|
||||
@@ -32,7 +34,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
|
||||
}
|
||||
|
||||
public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
|
||||
OkPacket[] | ResultSetHeader>(query, params?, connection?: PoolConnection): Promise<[T, FieldPacket[]]>
|
||||
OkPacket[] | ResultSetHeader>(query, params?, errorLogLevel: LogLevel | 'silent' = 'debug', connection?: PoolConnection): Promise<[T, FieldPacket[]]>
|
||||
{
|
||||
this.checkDBFlag();
|
||||
let hardTimeout;
|
||||
@@ -54,19 +56,38 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
|
||||
}).then(result => {
|
||||
resolve(result);
|
||||
}).catch(error => {
|
||||
if (errorLogLevel !== 'silent') {
|
||||
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
|
||||
}
|
||||
reject(error);
|
||||
}).finally(() => {
|
||||
clearTimeout(timer);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const pool = await this.getPool();
|
||||
return pool.query(query, params);
|
||||
try {
|
||||
const pool = await this.getPool();
|
||||
return pool.query(query, params);
|
||||
} catch (e) {
|
||||
if (errorLogLevel !== 'silent') {
|
||||
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async $rollbackAtomic(connection: PoolConnection): Promise<void> {
|
||||
try {
|
||||
await connection.rollback();
|
||||
await connection.release();
|
||||
} catch (e) {
|
||||
logger.warn('Failed to rollback incomplete db transaction: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
|
||||
OkPacket[] | ResultSetHeader>(queries: { query, params }[]): Promise<[T, FieldPacket[]][]>
|
||||
OkPacket[] | ResultSetHeader>(queries: { query, params }[], errorLogLevel: LogLevel | 'silent' = 'debug'): Promise<[T, FieldPacket[]][]>
|
||||
{
|
||||
const pool = await this.getPool();
|
||||
const connection = await pool.getConnection();
|
||||
@@ -75,7 +96,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
|
||||
|
||||
const results: [T, FieldPacket[]][] = [];
|
||||
for (const query of queries) {
|
||||
const result = await this.query(query.query, query.params, connection) as [T, FieldPacket[]];
|
||||
const result = await this.query(query.query, query.params, errorLogLevel, connection) as [T, FieldPacket[]];
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
@@ -83,9 +104,8 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
|
||||
|
||||
return results;
|
||||
} catch (e) {
|
||||
logger.err('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
|
||||
connection.rollback();
|
||||
connection.release();
|
||||
logger.warn('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
|
||||
this.$rollbackAtomic(connection);
|
||||
throw e;
|
||||
} finally {
|
||||
connection.release();
|
||||
@@ -105,26 +125,43 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
|
||||
|
||||
public getPidLock(): boolean {
|
||||
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
|
||||
this.enforcePidLock(filePath);
|
||||
fs.writeFileSync(filePath, `${process.pid}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
private enforcePidLock(filePath: string): void {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const pid = fs.readFileSync(filePath).toString();
|
||||
if (pid !== `${process.pid}`) {
|
||||
const msg = `Already running on PID ${pid} (or pid file '${filePath}' is stale)`;
|
||||
const pid = parseInt(fs.readFileSync(filePath, 'utf-8'));
|
||||
if (pid === process.pid) {
|
||||
logger.warn('PID file already exists for this process');
|
||||
return;
|
||||
}
|
||||
|
||||
let cmd;
|
||||
try {
|
||||
cmd = execSync(`ps -p ${pid} -o args=`);
|
||||
} catch (e) {
|
||||
logger.warn(`Stale PID file at ${filePath}, but no process running on that PID ${pid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd && cmd.toString()?.includes('node')) {
|
||||
const msg = `Another mempool nodejs process is already running on PID ${pid}`;
|
||||
logger.err(msg);
|
||||
throw new Error(msg);
|
||||
} else {
|
||||
return true;
|
||||
logger.warn(`Stale PID file at ${filePath}, but the PID ${pid} does not belong to a running mempool instance`);
|
||||
}
|
||||
} else {
|
||||
fs.writeFileSync(filePath, `${process.pid}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public releasePidLock(): void {
|
||||
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const pid = fs.readFileSync(filePath).toString();
|
||||
if (pid === `${process.pid}`) {
|
||||
const pid = parseInt(fs.readFileSync(filePath, 'utf-8'));
|
||||
// only release our own pid file
|
||||
if (pid === process.pid) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,15 @@ class Server {
|
||||
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
|
||||
|
||||
// Register cleanup listeners for exit events
|
||||
['exit', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'unhandledRejection'].forEach(event => {
|
||||
['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'].forEach(event => {
|
||||
process.on(event, () => { this.onExit(event); });
|
||||
});
|
||||
process.on('uncaughtException', (error) => {
|
||||
this.onUnhandledException('uncaughtException', error);
|
||||
});
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
this.onUnhandledException('unhandledRejection', reason);
|
||||
});
|
||||
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
bitcoinApi.startHealthChecks();
|
||||
@@ -200,7 +206,7 @@ class Server {
|
||||
}
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1);
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||
if (numHandledBlocks === 0) {
|
||||
await memPool.$updateMempool(newMempool, pollRate);
|
||||
}
|
||||
@@ -314,14 +320,18 @@ class Server {
|
||||
}
|
||||
}
|
||||
|
||||
onExit(exitEvent): void {
|
||||
onExit(exitEvent, code = 0): void {
|
||||
logger.debug(`onExit for signal: ${exitEvent}`);
|
||||
if (config.DATABASE.ENABLED) {
|
||||
DB.releasePidLock();
|
||||
}
|
||||
process.exit(0);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
onUnhandledException(type, error): void {
|
||||
console.error(`${type}:`, error);
|
||||
this.onExit(type, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
((): Server => new Server())();
|
||||
|
||||
@@ -15,11 +15,18 @@ export interface CoreIndex {
|
||||
best_block_height: number;
|
||||
}
|
||||
|
||||
type TaskName = 'blocksPrices' | 'coinStatsIndex';
|
||||
|
||||
class Indexer {
|
||||
runIndexer = true;
|
||||
indexerRunning = false;
|
||||
tasksRunning: string[] = [];
|
||||
coreIndexes: CoreIndex[] = [];
|
||||
private runIndexer = true;
|
||||
private indexerRunning = false;
|
||||
private tasksRunning: { [key in TaskName]?: boolean; } = {};
|
||||
private tasksScheduled: { [key in TaskName]?: NodeJS.Timeout; } = {};
|
||||
private coreIndexes: CoreIndex[] = [];
|
||||
|
||||
public indexerIsRunning(): boolean {
|
||||
return this.indexerRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check which core index is available for indexing
|
||||
@@ -69,33 +76,69 @@ class Indexer {
|
||||
}
|
||||
}
|
||||
|
||||
public async runSingleTask(task: 'blocksPrices' | 'coinStatsIndex'): Promise<void> {
|
||||
if (!Common.indexingEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (task === 'blocksPrices' && !this.tasksRunning.includes(task) && !['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
this.tasksRunning.push(task);
|
||||
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
if (priceUpdater.historyInserted === false || lastestPriceId === null) {
|
||||
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
|
||||
setTimeout(() => {
|
||||
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
|
||||
this.runSingleTask('blocksPrices');
|
||||
}, 10000);
|
||||
} else {
|
||||
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
|
||||
await mining.$indexBlockPrices();
|
||||
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
|
||||
/**
|
||||
* schedules a single task to run in `timeout` ms
|
||||
* only one task of each type may be scheduled
|
||||
*
|
||||
* @param {TaskName} task - the type of task
|
||||
* @param {number} timeout - delay in ms
|
||||
* @param {boolean} replace - `true` replaces any already scheduled task (works like a debounce), `false` ignores subsequent requests (works like a throttle)
|
||||
*/
|
||||
public scheduleSingleTask(task: TaskName, timeout: number = 10000, replace = false): void {
|
||||
if (this.tasksScheduled[task]) {
|
||||
if (!replace) { //throttle
|
||||
return;
|
||||
} else { // debounce
|
||||
clearTimeout(this.tasksScheduled[task]);
|
||||
}
|
||||
}
|
||||
this.tasksScheduled[task] = setTimeout(async () => {
|
||||
try {
|
||||
await this.runSingleTask(task);
|
||||
} catch (e) {
|
||||
logger.err(`Unexpected error in scheduled task ${task}: ` + (e instanceof Error ? e.message : e));
|
||||
} finally {
|
||||
clearTimeout(this.tasksScheduled[task]);
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
if (task === 'coinStatsIndex' && !this.tasksRunning.includes(task)) {
|
||||
this.tasksRunning.push(task);
|
||||
logger.debug(`Indexing coinStatsIndex now`);
|
||||
await mining.$indexCoinStatsIndex();
|
||||
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
|
||||
/**
|
||||
* Runs a single task immediately
|
||||
*
|
||||
* (use `scheduleSingleTask` instead to queue a task to run after some timeout)
|
||||
*/
|
||||
public async runSingleTask(task: TaskName): Promise<void> {
|
||||
if (!Common.indexingEnabled() || this.tasksRunning[task]) {
|
||||
return;
|
||||
}
|
||||
this.tasksRunning[task] = true;
|
||||
|
||||
switch (task) {
|
||||
case 'blocksPrices': {
|
||||
if (!['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
let lastestPriceId;
|
||||
try {
|
||||
lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
} catch (e) {
|
||||
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
|
||||
} if (priceUpdater.historyInserted === false || lastestPriceId === null) {
|
||||
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
|
||||
this.scheduleSingleTask(task, 10000);
|
||||
} else {
|
||||
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
|
||||
await mining.$indexBlockPrices();
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case 'coinStatsIndex': {
|
||||
logger.debug(`Indexing coinStatsIndex now`);
|
||||
await mining.$indexCoinStatsIndex();
|
||||
} break;
|
||||
}
|
||||
|
||||
this.tasksRunning[task] = false;
|
||||
}
|
||||
|
||||
public async $run(): Promise<void> {
|
||||
|
||||
@@ -157,4 +157,6 @@ class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
export type LogLevel = 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
|
||||
|
||||
export default new Logger();
|
||||
|
||||
@@ -541,7 +541,7 @@ class BlocksRepository {
|
||||
*/
|
||||
public async $getBlocksDifficulty(): Promise<object[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks`);
|
||||
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks ORDER BY height ASC`);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
|
||||
@@ -14,7 +14,7 @@ class NodesSocketsRepository {
|
||||
await DB.query(`
|
||||
INSERT INTO nodes_sockets(public_key, socket, type)
|
||||
VALUE (?, ?, ?)
|
||||
`, [socket.publicKey, socket.addr, socket.network]);
|
||||
`, [socket.publicKey, socket.addr, socket.network], 'silent');
|
||||
} catch (e: any) {
|
||||
if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this
|
||||
logger.err(`Cannot save node socket (${[socket.publicKey, socket.addr, socket.network]}) into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
|
||||
@@ -79,7 +79,7 @@ class ForensicsService {
|
||||
}
|
||||
|
||||
let progress = 0;
|
||||
const sliceLength = 1000;
|
||||
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 10);
|
||||
// process batches of 1000 channels
|
||||
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
|
||||
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
|
||||
@@ -290,7 +290,7 @@ class NetworkSyncService {
|
||||
|
||||
const allChannels = await channelsApi.$getChannelsByStatus([0, 1]);
|
||||
|
||||
const sliceLength = 5000;
|
||||
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 2);
|
||||
// process batches of 5000 channels
|
||||
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
|
||||
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
|
||||
3
contributors/ncois.txt
Normal file
3
contributors/ncois.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 16, 2023.
|
||||
|
||||
Signed: ncois
|
||||
3
contributors/shubhamkmr04.txt
Normal file
3
contributors/shubhamkmr04.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 15, 2023.
|
||||
|
||||
Signed: shubhamkmr04
|
||||
3
contributors/starius.txt
Normal file
3
contributors/starius.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of Oct 13, 2023.
|
||||
|
||||
Signed starius
|
||||
@@ -53,6 +53,7 @@
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "__ESPLORA_REST_API_URL__",
|
||||
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
|
||||
"BATCH_QUERY_BASE_SIZE": __ESPLORA_BATCH_QUERY_BASE_SIZE__,
|
||||
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
|
||||
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
|
||||
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
|
||||
@@ -146,6 +147,7 @@
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": __REDIS_ENABLED__,
|
||||
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
|
||||
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
|
||||
"BATCH_QUERY_BASE_SIZE": __REDIS_BATCH_QUERY_BASE_SIZE__
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
|
||||
# ESPLORA
|
||||
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
|
||||
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
|
||||
__ESPLORA_BATCH_QUERY_BASE_SIZE__=${ESPLORA_BATCH_QUERY_BASE_SIZE:=1000}
|
||||
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
|
||||
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
|
||||
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
|
||||
@@ -148,6 +149,7 @@ __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||
# REDIS
|
||||
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
|
||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
|
||||
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
|
||||
|
||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||
|
||||
@@ -201,6 +203,7 @@ sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config
|
||||
|
||||
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_BATCH_QUERY_BASE_SIZE__!${__ESPLORA_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
|
||||
@@ -288,5 +291,6 @@ sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS_
|
||||
# REDIS
|
||||
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||
sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
|
||||
|
||||
node /backend/package/index.js
|
||||
|
||||
1322
frontend/package-lock.json
generated
1322
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -74,9 +74,9 @@
|
||||
"@angular/platform-server": "^16.2.2",
|
||||
"@angular/router": "^16.2.2",
|
||||
"@fortawesome/angular-fontawesome": "~0.13.0",
|
||||
"@fortawesome/fontawesome-common-types": "~6.4.0",
|
||||
"@fortawesome/fontawesome-svg-core": "~6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "~6.4.0",
|
||||
"@fortawesome/fontawesome-common-types": "~6.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "~6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "~6.5.1",
|
||||
"@mempool/mempool.js": "2.3.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^15.1.0",
|
||||
"@types/qrcode": "~1.5.0",
|
||||
@@ -90,7 +90,7 @@
|
||||
"ngx-infinite-scroll": "^16.0.0",
|
||||
"qrcode": "1.5.1",
|
||||
"rxjs": "~7.8.1",
|
||||
"tinyify": "^4.0.0",
|
||||
"tinyify": "^3.1.0",
|
||||
"tlite": "^0.1.9",
|
||||
"tslib": "~2.6.0",
|
||||
"zone.js": "~0.13.1"
|
||||
@@ -110,7 +110,7 @@
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.5.0",
|
||||
"cypress": "^13.6.0",
|
||||
"cypress-fail-on-console-error": "~5.0.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy'
|
||||
import { BlockViewComponent } from './components/block-view/block-view.component';
|
||||
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
|
||||
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
|
||||
import { ClockComponent } from './components/clock/clock.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
@@ -124,6 +125,10 @@ let routes: Routes = [
|
||||
path: 'view/mempool-block/:index',
|
||||
component: MempoolBlockViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'view/blocks',
|
||||
component: EightBlocksComponent,
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
|
||||
@@ -30,7 +30,7 @@ export class BisqDashboardComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Bisq market prices, trading activity, and more.`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project™. See Bisq market prices, trading activity, and more.`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
|
||||
|
||||
@@ -225,7 +225,7 @@ const witnessSize = (vin: Vin) => vin.witness ? vin.witness.reduce((S, w) => S +
|
||||
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
|
||||
|
||||
// Power of ten wrapper
|
||||
export function selectPowerOfTen(val: number): { divider: number, unit: string } {
|
||||
export function selectPowerOfTen(val: number, multiplier = 1): { divider: number, unit: string } {
|
||||
const powerOfTen = {
|
||||
exa: Math.pow(10, 18),
|
||||
peta: Math.pow(10, 15),
|
||||
@@ -236,17 +236,17 @@ export function selectPowerOfTen(val: number): { divider: number, unit: string }
|
||||
};
|
||||
|
||||
let selectedPowerOfTen: { divider: number, unit: string };
|
||||
if (val < powerOfTen.kilo) {
|
||||
if (val < powerOfTen.kilo * multiplier) {
|
||||
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
|
||||
} else if (val < powerOfTen.mega) {
|
||||
} else if (val < powerOfTen.mega * multiplier) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' };
|
||||
} else if (val < powerOfTen.giga) {
|
||||
} else if (val < powerOfTen.giga * multiplier) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
|
||||
} else if (val < powerOfTen.tera) {
|
||||
} else if (val < powerOfTen.tera * multiplier) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
|
||||
} else if (val < powerOfTen.peta) {
|
||||
} else if (val < powerOfTen.peta * multiplier) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.tera, unit: 'T' };
|
||||
} else if (val < powerOfTen.exa) {
|
||||
} else if (val < powerOfTen.exa * multiplier) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
|
||||
} else {
|
||||
selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' };
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<div id="become-sponsor-container">
|
||||
<div class="become-sponsor community">
|
||||
<p style="font-weight: 700; font-size: 18px;">If you're an individual...</p>
|
||||
<a href="https://mempool.space/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button" (click)="onSponsorClick($event)">Become a Community Sponsor</a>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Exclusive swag</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Your avatar on the About page</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> And more coming soon :)</p>
|
||||
</div>
|
||||
<div class="become-sponsor enterprise">
|
||||
<p style="font-weight: 700; font-size: 18px;">If you're a business...</p>
|
||||
<a href="https://mempool.space/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button" (click)="onEnterpriseClick($event)">Become an Enterprise Sponsor</a>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Increased API limits</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Co-branded instance</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
#become-sponsor-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin: 68px auto;
|
||||
}
|
||||
|
||||
.become-sponsor {
|
||||
background-color: #1d1f31;
|
||||
border-radius: 16px;
|
||||
padding: 12px 20px;
|
||||
width: 400px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.become-sponsor a {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#become-sponsor-container .btn {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
#become-sponsor-container .ng-fa-icon {
|
||||
color: #2ecc71;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#become-sponsor-container .sponsor-feature {
|
||||
text-align: left;
|
||||
width: 250px;
|
||||
margin: 12px auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
||||
#become-sponsor-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about-sponsors',
|
||||
templateUrl: './about-sponsors.component.html',
|
||||
styleUrls: ['./about-sponsors.component.scss'],
|
||||
})
|
||||
export class AboutSponsorsComponent {
|
||||
constructor(private enterpriseService: EnterpriseService) {
|
||||
}
|
||||
|
||||
onSponsorClick(e): boolean {
|
||||
this.enterpriseService.goal(5);
|
||||
return true;
|
||||
}
|
||||
|
||||
onEnterpriseClick(e): boolean {
|
||||
this.enterpriseService.goal(6);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,8 @@
|
||||
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
|
||||
</video>
|
||||
|
||||
<ng-container *ngIf="false && officialMempoolSpace">
|
||||
<h3 class="mt-5">Sponsor the project</h3>
|
||||
<div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px">
|
||||
<a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Community</a>
|
||||
<a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Enterprise</a>
|
||||
</div>
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
<app-about-sponsors></app-about-sponsors>
|
||||
</ng-container>
|
||||
|
||||
<div class="enterprise-sponsor" id="enterprise-sponsors">
|
||||
@@ -186,14 +182,14 @@
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
<div *ngIf="profiles$ | async as profiles" id="community-sponsors">
|
||||
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
|
||||
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
|
||||
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
|
||||
<div class="wrapper">
|
||||
<ng-container>
|
||||
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
|
||||
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
|
||||
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -205,7 +201,7 @@
|
||||
<div class="wrapper">
|
||||
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
|
||||
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
|
||||
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -299,9 +295,9 @@
|
||||
<img class="image" src="/resources/profile/blixt.png" />
|
||||
<span>Blixt</span>
|
||||
</a>
|
||||
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="Zeus">
|
||||
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS">
|
||||
<img class="image" src="/resources/profile/zeus.png" />
|
||||
<span>Zeus</span>
|
||||
<span>ZEUS</span>
|
||||
</a>
|
||||
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
|
||||
<img class="image" src="/resources/profile/marina.svg" />
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { map, share, tap } from 'rxjs/operators';
|
||||
import { ITranslators } from '../../interfaces/node-api.interface';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
@@ -33,6 +34,7 @@ export class AboutComponent implements OnInit {
|
||||
private websocketService: WebsocketService,
|
||||
private seoService: SeoService,
|
||||
public stateService: StateService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private apiService: ApiService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
@@ -47,8 +49,13 @@ export class AboutComponent implements OnInit {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(
|
||||
tap(() => {
|
||||
this.goToAnchor()
|
||||
tap((profiles: any) => {
|
||||
const scrollToSponsors = this.route.snapshot.fragment === 'community-sponsors';
|
||||
if (scrollToSponsors && !profiles?.whales?.length && !profiles?.chads?.length) {
|
||||
return;
|
||||
} else {
|
||||
this.goToAnchor(scrollToSponsors)
|
||||
}
|
||||
}),
|
||||
share(),
|
||||
)
|
||||
@@ -83,11 +90,19 @@ export class AboutComponent implements OnInit {
|
||||
this.goToAnchor();
|
||||
}
|
||||
|
||||
goToAnchor() {
|
||||
goToAnchor(scrollToSponsor = false) {
|
||||
if (!scrollToSponsor) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (this.route.snapshot.fragment) {
|
||||
if (this.document.getElementById(this.route.snapshot.fragment)) {
|
||||
this.document.getElementById(this.route.snapshot.fragment).scrollIntoView({behavior: 'smooth'});
|
||||
const el = scrollToSponsor ? this.document.getElementById('community-sponsors-anchor') : this.document.getElementById(this.route.snapshot.fragment);
|
||||
if (el) {
|
||||
if (scrollToSponsor) {
|
||||
el.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'center'});
|
||||
} else {
|
||||
el.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 1);
|
||||
@@ -108,4 +123,14 @@ export class AboutComponent implements OnInit {
|
||||
unmutePromoVideo(): void {
|
||||
this.promoVideo.nativeElement.muted = false;
|
||||
}
|
||||
|
||||
onSponsorClick(e): boolean {
|
||||
this.enterpriseService.goal(5);
|
||||
return true;
|
||||
}
|
||||
|
||||
onEnterpriseClick(e): boolean {
|
||||
this.enterpriseService.goal(6);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AboutComponent } from './about.component';
|
||||
import { AboutSponsorsComponent } from './about-sponsors.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -29,6 +30,10 @@ export class AboutRoutingModule { }
|
||||
],
|
||||
declarations: [
|
||||
AboutComponent,
|
||||
AboutSponsorsComponent,
|
||||
],
|
||||
exports: [
|
||||
AboutSponsorsComponent,
|
||||
]
|
||||
})
|
||||
export class AboutModule { }
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
height: 100%;
|
||||
min-width: 120px;
|
||||
width: 120px;
|
||||
max-height: 90vh;
|
||||
margin-left: 4em;
|
||||
margin-right: 1.5em;
|
||||
padding-bottom: 63px;
|
||||
@@ -18,6 +17,7 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
@@ -58,13 +58,15 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
||||
fee: option.fee,
|
||||
}
|
||||
});
|
||||
bars.push({
|
||||
rate: this.estimate.targetFeeRate,
|
||||
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||
class: 'target',
|
||||
label: 'next block',
|
||||
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||
});
|
||||
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
|
||||
bars.push({
|
||||
rate: this.estimate.targetFeeRate,
|
||||
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||
class: 'target',
|
||||
label: 'next block',
|
||||
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||
});
|
||||
}
|
||||
bars.push({
|
||||
rate: baseRate,
|
||||
style: this.getStyle(baseRate, maxRate, 0),
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span>
|
||||
<div class="row" *ngIf="showSuccess">
|
||||
<div class="col" id="successAlert">
|
||||
<div class="col">
|
||||
<div class="alert alert-success">
|
||||
Transaction has now been submitted to mining pools for acceleration. You can track the progress <a class="alert-link" routerLink="/services/accelerator/history">here</a>.
|
||||
Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span id="mempoolError" class="m-0 p-0 d-block" style="height: 1px;"></span>
|
||||
<div class="row" *ngIf="error">
|
||||
<div class="col" id="mempoolError">
|
||||
<app-mempool-error [error]="error"></app-mempool-error>
|
||||
<div class="col">
|
||||
<app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +27,11 @@
|
||||
|
||||
<ng-container *ngIf="estimate">
|
||||
<div [class]="{estimateDisabled: error}">
|
||||
|
||||
<div *ngIf="user && !estimate.hasAccess">
|
||||
<div class="alert alert-mempool">You are currently on the waitlist</div>
|
||||
</div>
|
||||
|
||||
<h5>Your transaction</h5>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
@@ -37,10 +44,10 @@
|
||||
<td class="item">
|
||||
Virtual size
|
||||
</td>
|
||||
<td class="units" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
|
||||
<td style="text-align: end;" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<td class="info" colspan=3>
|
||||
<i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -48,12 +55,12 @@
|
||||
<td class="item">
|
||||
In-band fees
|
||||
</td>
|
||||
<td class="units">
|
||||
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<td style="text-align: end;">
|
||||
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<td class="info" colspan=3>
|
||||
<i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -74,8 +81,8 @@
|
||||
<div class="d-flex mb-0">
|
||||
<ng-container *ngFor="let option of maxRateOptions">
|
||||
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)">
|
||||
<span class="fee">{{ option.fee | number }} <span class="symbol" i18n="shared.sats|sats">sats</span></span>
|
||||
<span class="rate">~ <app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
|
||||
<span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
|
||||
<span class="rate">~<app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -87,23 +94,15 @@
|
||||
<h5>Acceleration summary</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<div class="table-toggle btn-group btn-group-toggle">
|
||||
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'estimated'" (click)="showTable = 'estimated'">
|
||||
<span>Estimated cost</span>
|
||||
</div>
|
||||
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'maximum'" (click)="showTable = 'maximum'">
|
||||
<span>Maximum cost</span>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-borderless table-border table-dark table-accelerator">
|
||||
<tbody>
|
||||
<!-- ESTIMATED FEE -->
|
||||
<ng-container *ngIf="showTable === 'estimated'">
|
||||
<ng-container>
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
Next block market rate
|
||||
</td>
|
||||
<td class="amt" style="font-size: 20px">
|
||||
<td class="amt" style="font-size: 16px">
|
||||
{{ estimate.targetFeeRate | number : '1.0-0' }}
|
||||
</td>
|
||||
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||
@@ -116,34 +115,8 @@
|
||||
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<!-- USER MAX BID -->
|
||||
<ng-container *ngIf="showTable === 'maximum'">
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
Your maximum
|
||||
</td>
|
||||
<td class="amt" style="width: 45%; font-size: 20px">
|
||||
~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }}
|
||||
</td>
|
||||
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<i><small>The maximum extra transaction fee you could pay</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
<span>
|
||||
{{ userBid | number }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="userBid"></app-fiat></span>
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
@@ -162,11 +135,11 @@
|
||||
+{{ estimate.mempoolBaseFee | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<i><small>Transaction vsize fee</small></i>
|
||||
</td>
|
||||
@@ -174,14 +147,14 @@
|
||||
+{{ estimate.vsizeFee | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- NEXT BLOCK ESTIMATE -->
|
||||
<ng-container *ngIf="showTable === 'estimated'">
|
||||
<tr class="group-first">
|
||||
<ng-container>
|
||||
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
|
||||
<td class="item">
|
||||
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
|
||||
</td>
|
||||
@@ -191,19 +164,19 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
|
||||
<td class="info" colspan=3>
|
||||
<i><small>If your tx is accelerated to </small><small>{{ estimate.targetFeeRate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- MAX COST -->
|
||||
<ng-container *ngIf="showTable === 'maximum'">
|
||||
<ng-container>
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
|
||||
@@ -214,21 +187,21 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat">
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1">
|
||||
<app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<td class="info" colspan=3>
|
||||
<i><small>If your tx is accelerated to </small><small>~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- USER BALANCE -->
|
||||
<ng-container *ngIf="estimate.userBalance < maxCost">
|
||||
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
|
||||
<tr class="group-first group-last" style="border-top: 1px dashed grey">
|
||||
<td class="item">
|
||||
Available balance
|
||||
@@ -237,13 +210,24 @@
|
||||
{{ estimate.userBalance | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat">
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1">
|
||||
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- LOGIN CTA -->
|
||||
<ng-container *ngIf="!isLoggedIn()">
|
||||
<tr class="group-first group-last" style="border-top: 1px dashed grey">
|
||||
<td class="item"></td>
|
||||
<td class="amt"></td>
|
||||
<td class="units d-flex">
|
||||
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1">Login</a>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -251,7 +235,7 @@
|
||||
|
||||
<div class="row mb-3" *ngIf="isLoggedIn()">
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess">
|
||||
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.fee {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.rate {
|
||||
font-size: 0.9em;
|
||||
.symbol {
|
||||
@@ -28,7 +25,10 @@
|
||||
.feerate.active {
|
||||
background-color: #105fb0 !important;
|
||||
opacity: 1;
|
||||
border: 1px solid white !important;
|
||||
border: 1px solid #007fff !important;
|
||||
}
|
||||
.feerate:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.estimateDisabled {
|
||||
@@ -41,10 +41,26 @@
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.tab {
|
||||
&:first-child {
|
||||
margin-right: 1px;
|
||||
}
|
||||
border: solid 1px black;
|
||||
border-bottom: none;
|
||||
background-color: #323655;
|
||||
border-top-left-radius: 10px !important;
|
||||
border-top-right-radius: 10px !important;
|
||||
}
|
||||
.tab.active {
|
||||
background-color: #5d659d !important;
|
||||
opacity: 1;
|
||||
}
|
||||
.tab:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.table-accelerator {
|
||||
tr {
|
||||
text-wrap: wrap;
|
||||
|
||||
td {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
@@ -68,6 +84,7 @@
|
||||
}
|
||||
&.info {
|
||||
color: #6c757d;
|
||||
white-space: initial;
|
||||
}
|
||||
&.amt {
|
||||
text-align: right;
|
||||
@@ -76,6 +93,9 @@
|
||||
&.units {
|
||||
padding-left: 0.2em;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,4 +105,8 @@
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.item {
|
||||
white-space: initial;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Subscription, catchError, of, tap } from 'rxjs';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
@@ -55,14 +55,15 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
maxCost = 0;
|
||||
userBid = 0;
|
||||
selectFeeRateIndex = 1;
|
||||
showTable: 'estimated' | 'maximum' = 'maximum';
|
||||
isMobile: boolean = window.innerWidth <= 767.98;
|
||||
user: any = undefined;
|
||||
|
||||
maxRateOptions: RateOption[] = [];
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private storageService: StorageService
|
||||
private storageService: StorageService,
|
||||
private cd: ChangeDetectorRef
|
||||
) { }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -73,11 +74,13 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.scrollEvent) {
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'center');
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.user = this.storageService.getAuth()?.user ?? null;
|
||||
|
||||
this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe(
|
||||
tap((response) => {
|
||||
if (response.status === 204) {
|
||||
@@ -93,7 +96,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
if (this.estimate.userBalance <= 0) {
|
||||
if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) {
|
||||
if (this.isLoggedIn()) {
|
||||
this.error = `not_enough_balance`;
|
||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||
@@ -126,7 +129,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
||||
|
||||
if (!this.error) {
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'center');
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'start');
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -162,13 +165,14 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
scrollToPreview(id: string, position: ScrollLogicalPosition) {
|
||||
const acceleratePreviewAnchor = document.getElementById(id);
|
||||
if (acceleratePreviewAnchor) {
|
||||
this.cd.markForCheck();
|
||||
acceleratePreviewAnchor.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
inline: position,
|
||||
block: position,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send acceleration request
|
||||
@@ -187,7 +191,11 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.estimateSubscription.unsubscribe();
|
||||
},
|
||||
error: (response) => {
|
||||
this.error = response.error;
|
||||
if (response.status === 403 && response.error === 'not_available') {
|
||||
this.error = 'waitlisted';
|
||||
} else {
|
||||
this.error = response.error;
|
||||
}
|
||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
this.addressString = this.addressString.toLowerCase();
|
||||
}
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'} ${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
|
||||
|
||||
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<header>
|
||||
<header class="sticky-header">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
li.nav-item.active {
|
||||
background-color: #653b9c;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
@Input() blockLimit: number;
|
||||
@Input() orientation = 'left';
|
||||
@Input() flip = true;
|
||||
@Input() animationDuration: number = 1000;
|
||||
@Input() animationOffset: number | null = null;
|
||||
@Input() disableSpinner = false;
|
||||
@Input() mirrorTxid: string | void;
|
||||
@Input() unavailable: boolean = false;
|
||||
@@ -141,9 +143,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
}
|
||||
|
||||
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void {
|
||||
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
|
||||
if (this.scene) {
|
||||
this.scene.replace(transactions || [], direction, sort);
|
||||
this.scene.replace(transactions || [], direction, sort, startTime);
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
}
|
||||
@@ -226,7 +228,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
} else {
|
||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
||||
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
|
||||
highlighting: this.auditHighlighting });
|
||||
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset });
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ export default class BlockScene {
|
||||
txs: { [key: string]: TxView };
|
||||
orientation: string;
|
||||
flip: boolean;
|
||||
animationDuration: number = 1000;
|
||||
configAnimationOffset: number | null;
|
||||
animationOffset: number;
|
||||
highlightingEnabled: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -23,11 +26,11 @@ export default class BlockScene {
|
||||
animateUntil = 0;
|
||||
dirty: boolean;
|
||||
|
||||
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||
constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||
) {
|
||||
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
|
||||
this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting });
|
||||
}
|
||||
|
||||
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
||||
@@ -36,6 +39,7 @@ export default class BlockScene {
|
||||
this.gridSize = this.width / this.gridWidth;
|
||||
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5));
|
||||
this.unitWidth = this.gridSize - (this.unitPadding * 2);
|
||||
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
|
||||
|
||||
this.dirty = true;
|
||||
if (this.initialised && this.scene) {
|
||||
@@ -90,8 +94,8 @@ export default class BlockScene {
|
||||
}
|
||||
|
||||
// Animate new block entering scene
|
||||
enter(txs: TransactionStripped[], direction) {
|
||||
this.replace(txs, direction);
|
||||
enter(txs: TransactionStripped[], direction, startTime?: number) {
|
||||
this.replace(txs, direction, false, startTime);
|
||||
}
|
||||
|
||||
// Animate block leaving scene
|
||||
@@ -108,8 +112,7 @@ export default class BlockScene {
|
||||
}
|
||||
|
||||
// Reset layout and replace with new set of transactions
|
||||
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true): void {
|
||||
const startTime = performance.now();
|
||||
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true, startTime: number = performance.now()): void {
|
||||
const nextIds = {};
|
||||
const remove = [];
|
||||
txs.forEach(tx => {
|
||||
@@ -133,7 +136,7 @@ export default class BlockScene {
|
||||
removed.forEach(tx => {
|
||||
tx.destroy();
|
||||
});
|
||||
}, 1000);
|
||||
}, (startTime - performance.now()) + this.animationDuration + 1000);
|
||||
|
||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||
|
||||
@@ -147,7 +150,7 @@ export default class BlockScene {
|
||||
});
|
||||
}
|
||||
|
||||
this.updateAll(startTime, 200, direction);
|
||||
this.updateAll(startTime, 50, direction);
|
||||
}
|
||||
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
@@ -214,10 +217,13 @@ export default class BlockScene {
|
||||
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
|
||||
}
|
||||
|
||||
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||
private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||
): void {
|
||||
this.animationDuration = animationDuration || 1000;
|
||||
this.configAnimationOffset = animationOffset;
|
||||
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
|
||||
this.orientation = orientation;
|
||||
this.flip = flip;
|
||||
this.vertexArray = vertexArray;
|
||||
@@ -261,8 +267,8 @@ export default class BlockScene {
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
position: {
|
||||
x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4,
|
||||
y: tx.screenPosition.y + (direction === 'up' ? -this.height : (direction === 'down' ? this.height : 0)) * 1.4,
|
||||
x: tx.screenPosition.x + (direction === 'right' ? -this.width - this.animationOffset : (direction === 'left' ? this.width + this.animationOffset : 0)),
|
||||
y: tx.screenPosition.y + (direction === 'up' ? -this.height - this.animationOffset : (direction === 'down' ? this.height + this.animationOffset : 0)),
|
||||
s: tx.screenPosition.s
|
||||
},
|
||||
color: txColor,
|
||||
@@ -275,7 +281,7 @@ export default class BlockScene {
|
||||
position: tx.screenPosition,
|
||||
color: txColor
|
||||
},
|
||||
duration: animate ? 1000 : 1,
|
||||
duration: animate ? this.animationDuration : 1,
|
||||
start: startTime,
|
||||
delay: animate ? delay : 0,
|
||||
});
|
||||
@@ -284,8 +290,8 @@ export default class BlockScene {
|
||||
display: {
|
||||
position: tx.screenPosition
|
||||
},
|
||||
duration: animate ? 1000 : 0,
|
||||
minDuration: animate ? 500 : 0,
|
||||
duration: animate ? this.animationDuration : 0,
|
||||
minDuration: animate ? (this.animationDuration / 2) : 0,
|
||||
start: startTime,
|
||||
delay: animate ? delay : 0,
|
||||
adjust: animate
|
||||
@@ -322,11 +328,11 @@ export default class BlockScene {
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
position: {
|
||||
x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4,
|
||||
y: tx.screenPosition.y + (direction === 'up' ? this.height : (direction === 'down' ? -this.height : 0)) * 1.4,
|
||||
x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)),
|
||||
y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)),
|
||||
}
|
||||
},
|
||||
duration: 1000,
|
||||
duration: this.animationDuration,
|
||||
start: startTime,
|
||||
delay: 50
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
|
||||
<td *ngSwitchCase="'accelerated'"><span class="badge badge-success" i18n="transaction.audit.accelerated">Accelerated</span></td>
|
||||
<td *ngSwitchCase="'accelerated'"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -19,4 +19,17 @@
|
||||
|
||||
.td-width {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.badge.badge-accelerated {
|
||||
background-color: #653b9c;
|
||||
box-shadow: #ad7de57f 0px 0px 12px -2px;
|
||||
color: white;
|
||||
animation: acceleratePulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes acceleratePulse {
|
||||
0% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
|
||||
50% { background-color: #8457bb; box-shadow: #ad7de5 0px 0px 18px -2px;}
|
||||
100% { background-color: #653b9c; box-shadow: #ad7de57f 0px 0px 12px -2px; }
|
||||
}
|
||||
@@ -219,13 +219,13 @@
|
||||
<div class="box" *ngIf="!error && webGlEnabled && showAudit">
|
||||
<div class="nav nav-tabs" *ngIf="isMobile && showAudit">
|
||||
<a class="nav-link" [class.active]="mode === 'projected'"
|
||||
fragment="projected" (click)="changeMode('projected')"><ng-container i18n="block.expected">Expected</ng-container> <span class="badge badge-pill badge-warning" i18n="beta">beta</span></a>
|
||||
fragment="projected" (click)="changeMode('projected')"><ng-container i18n="block.expected">Expected</ng-container></a>
|
||||
<a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual"
|
||||
fragment="actual" (click)="changeMode('actual')">Actual</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3>
|
||||
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container></h3>
|
||||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
|
||||
@@ -262,7 +262,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.version">Version</td>
|
||||
<td>{{ block.version | decimal2hex }} <span *ngIf="displayTaprootStatus() && hasTaproot(block.version)" class="badge badge-success ml-1" >Taproot</span></td>
|
||||
<td>{{ block.version | decimal2hex }} <span *ngIf="displayTaprootStatus() && hasTaproot(block.version)" class="badge badge-success ml-1" i18n="tx-features.tag.taproot|Taproot">Taproot</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<td i18n="block.bits">Bits</td>
|
||||
|
||||
@@ -55,7 +55,9 @@ export class BlocksList implements OnInit {
|
||||
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
|
||||
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
|
||||
|
||||
this.seoService.setTitle($localize`:@@meta.title.blocks-list:Blocks`);
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`:@@m8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
|
||||
}
|
||||
if( this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet' ) {
|
||||
this.seoService.setDescription($localize`:@@meta.description.liquid.blocks:See the most recent Liquid${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block size, and more.`);
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="container-xl">
|
||||
<div class="text-center">
|
||||
<h2>Calculator</h2>
|
||||
<h2 i18n="shared.calculator">Calculator</h2>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="price$ | async; else loading">
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<div class="input-group input-group-lg mb-1">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">sats</span>
|
||||
<span class="input-group-text" i18n="shared.sats">sats</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
|
||||
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="bitcoin-satoshis-text">
|
||||
₿
|
||||
<span [innerHTML]="form.get('bitcoin').value | bitcoinsatoshis"></span>
|
||||
<span class="sats"> sats</span>
|
||||
<span class="sats" i18n="shared.sats">sats</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,36 +38,35 @@
|
||||
</div>
|
||||
<ng-container *ngIf="!hideStats">
|
||||
<div class="stats top left">
|
||||
<p class="label" i18n="clock.fiat-price">fiat price</p>
|
||||
<p class="label" i18n>Price</p>
|
||||
<p>
|
||||
<app-fiat [value]="100000000" digitsInfo="1.2-2" colorClass="white-color"></app-fiat>
|
||||
</p>
|
||||
</div>
|
||||
<div class="stats top right">
|
||||
<p class="label" i18n="clock.priority-rate|priority fee rate">priority rate</p>
|
||||
<p class="label" i18n="fees-box.high-priority">High Priority</p>
|
||||
<p *ngIf="recommendedFees$ | async as recommendedFees;">
|
||||
<app-fee-rate [fee]="recommendedFees.fastestFee" unitClass="" rounding="1.0-0"></app-fee-rate>
|
||||
</p>
|
||||
</div>
|
||||
<div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom left">
|
||||
<p [innerHTML]="blocks[blockIndex].size | bytes: 2"></p>
|
||||
<p class="label" i18n="clock.block-size">block size</p>
|
||||
<p class="label" i18n="block.size">Size</p>
|
||||
</div>
|
||||
<div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom right">
|
||||
<p class="force-wrap">
|
||||
<ng-container *ngTemplateOutlet="blocks[blockIndex].tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: blocks[blockIndex].tx_count | number}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} <span class="label">transaction</span></ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} <span class="label">transactions</span></ng-template>
|
||||
{{ blocks[blockIndex].tx_count | number }}
|
||||
<span class="label" i18n="dashboard.txs">Transactions</span>
|
||||
</p>
|
||||
</div>
|
||||
<ng-container *ngIf="mempoolInfo$ | async as mempoolInfo;">
|
||||
<div *ngIf="mode === 'mempool'" class="stats bottom left">
|
||||
<p [innerHTML]="mempoolInfo.usage | bytes: 0"></p>
|
||||
<p class="label" i18n="dashboard.memory-usage|Memory usage">memory usage</p>
|
||||
<p class="label" i18n="dashboard.memory-usage|Memory usage">Memory Usage</p>
|
||||
</div>
|
||||
<div *ngIf="mode === 'mempool'" class="stats bottom right">
|
||||
<p>{{ mempoolInfo.size | number }}</p>
|
||||
<p class="label" i18n="dashboard.unconfirmed|Unconfirmed count">unconfirmed</p>
|
||||
<p class="label" i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
.label {
|
||||
font-size: calc(0.04 * var(--clock-width));
|
||||
line-height: calc(0.05 * var(--clock-width));
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
&.top {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<div class="blocks" [class.wrap]="wrapBlocks">
|
||||
<ng-container *ngFor="let i of blockIndices">
|
||||
<div class="block-wrapper" [style]="wrapperStyle">
|
||||
<div class="block-container" [style]="containerStyle">
|
||||
<app-block-overview-graph
|
||||
#blockGraph
|
||||
[isLoading]="false"
|
||||
[resolution]="resolution"
|
||||
[blockLimit]="stateService.blockVSize"
|
||||
[orientation]="'top'"
|
||||
[flip]="false"
|
||||
[animationDuration]="animationDuration"
|
||||
[animationOffset]="animationOffset"
|
||||
[disableSpinner]="true"
|
||||
(txClickEvent)="onTxClick($event)"
|
||||
></app-block-overview-graph>
|
||||
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
|
||||
<h1 class="height">{{ blockInfo[i].height }}</h1>
|
||||
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -0,0 +1,69 @@
|
||||
.blocks {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
|
||||
&.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.block-wrapper {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
--block-width: 1080px;
|
||||
|
||||
.info {
|
||||
position: absolute;
|
||||
left: 8%;
|
||||
top: 8%;
|
||||
right: 8%;
|
||||
bottom: 8%;
|
||||
height: 84%;
|
||||
width: 84%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: calc(var(--block-width) * 0.03);
|
||||
text-shadow: 0 0 calc(var(--block-width) * 0.05) black;
|
||||
|
||||
h1 {
|
||||
font-size: 6em;
|
||||
line-height: 1;
|
||||
margin-bottom: calc(var(--block-width) * 0.03);
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.8em;
|
||||
line-height: 1;
|
||||
margin-bottom: calc(var(--block-width) * 0.03);
|
||||
}
|
||||
|
||||
.hash {
|
||||
font-family: monospace;
|
||||
word-wrap: break-word;
|
||||
font-size: 1.4em;
|
||||
line-height: 1;
|
||||
margin-bottom: calc(var(--block-width) * 0.03);
|
||||
}
|
||||
|
||||
.mined-by {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.block-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { catchError, startWith } from 'rxjs/operators';
|
||||
import { Subject, Subscription, of } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
|
||||
import { detectWebGL } from '../../shared/graphs.utils';
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
|
||||
|
||||
function bestFitResolution(min, max, n): number {
|
||||
const target = (min + max) / 2;
|
||||
let bestScore = Infinity;
|
||||
let best = null;
|
||||
for (let i = min; i <= max; i++) {
|
||||
const remainder = (n % i);
|
||||
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
|
||||
bestScore = remainder;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
interface BlockInfo extends BlockExtended {
|
||||
timeString: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-eight-blocks',
|
||||
templateUrl: './eight-blocks.component.html',
|
||||
styleUrls: ['./eight-blocks.component.scss'],
|
||||
animations: [
|
||||
trigger('infoChange', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0 }),
|
||||
animate('1000ms', style({ opacity: 1 })),
|
||||
]),
|
||||
transition(':leave', [
|
||||
animate('1000ms 500ms', style({ opacity: 0 }))
|
||||
])
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class EightBlocksComponent implements OnInit, OnDestroy {
|
||||
network = '';
|
||||
latestBlocks: BlockExtended[] = [];
|
||||
isLoadingTransactions = true;
|
||||
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
|
||||
webGlEnabled = true;
|
||||
hoverTx: string | null = null;
|
||||
|
||||
blocksSubscription: Subscription;
|
||||
cacheBlocksSubscription: Subscription;
|
||||
networkChangedSubscription: Subscription;
|
||||
queryParamsSubscription: Subscription;
|
||||
graphChangeSubscription: Subscription;
|
||||
|
||||
numBlocks: number = 8;
|
||||
blockIndices: number[] = [...Array(8).keys()];
|
||||
autofit: boolean = false;
|
||||
padding: number = 0;
|
||||
wrapBlocks: boolean = false;
|
||||
blockWidth: number = 1080;
|
||||
animationDuration: number = 2000;
|
||||
animationOffset: number = 0;
|
||||
stagger: number = 0;
|
||||
testing: boolean = true;
|
||||
testHeight: number = 800000;
|
||||
testShiftTimeout: number;
|
||||
|
||||
showInfo: boolean = true;
|
||||
blockInfo: BlockInfo[] = [];
|
||||
|
||||
wrapperStyle = {
|
||||
'--block-width': '1080px',
|
||||
width: '1080px',
|
||||
maxWidth: '1080px',
|
||||
padding: '',
|
||||
};
|
||||
containerStyle = {};
|
||||
resolution: number = 86;
|
||||
|
||||
@ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
public stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
private apiService: ApiService,
|
||||
private bytesPipe: BytesPipe,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.network = this.stateService.network;
|
||||
|
||||
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
|
||||
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8;
|
||||
this.blockIndices = [...Array(this.numBlocks).keys()];
|
||||
this.autofit = params.autofit !== 'false';
|
||||
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 10;
|
||||
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540;
|
||||
this.wrapBlocks = params.wrap !== 'false';
|
||||
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
|
||||
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
|
||||
this.animationOffset = this.padding * 2;
|
||||
|
||||
if (this.autofit) {
|
||||
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
|
||||
} else {
|
||||
this.resolution = 86;
|
||||
}
|
||||
|
||||
this.wrapperStyle = {
|
||||
'--block-width': this.blockWidth + 'px',
|
||||
width: this.blockWidth + 'px',
|
||||
maxWidth: this.blockWidth + 'px',
|
||||
padding: (this.padding || 0) +'px 0px',
|
||||
};
|
||||
|
||||
if (params.test === 'true') {
|
||||
if (this.blocksSubscription) {
|
||||
this.blocksSubscription.unsubscribe();
|
||||
}
|
||||
this.blocksSubscription = (new Subject<BlockExtended[]>()).subscribe((blocks) => {
|
||||
this.handleNewBlock(blocks.slice(0, this.numBlocks));
|
||||
});
|
||||
this.shiftTestBlocks();
|
||||
} else if (!this.blocksSubscription) {
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe((blocks) => {
|
||||
this.handleNewBlock(blocks.slice(0, this.numBlocks));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.setupBlockGraphs();
|
||||
|
||||
this.networkChangedSubscription = this.stateService.networkChanged$
|
||||
.subscribe((network) => this.network = network);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => {
|
||||
this.setupBlockGraphs();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stateService.markBlock$.next({});
|
||||
if (this.blocksSubscription) {
|
||||
this.blocksSubscription?.unsubscribe();
|
||||
}
|
||||
this.cacheBlocksSubscription?.unsubscribe();
|
||||
this.networkChangedSubscription?.unsubscribe();
|
||||
this.queryParamsSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
shiftTestBlocks(): void {
|
||||
const sub = this.apiService.getBlocks$(this.testHeight).subscribe(result => {
|
||||
sub.unsubscribe();
|
||||
this.handleNewBlock(result.slice(0, this.numBlocks));
|
||||
this.testHeight++;
|
||||
clearTimeout(this.testShiftTimeout);
|
||||
this.testShiftTimeout = window.setTimeout(() => { this.shiftTestBlocks(); }, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async handleNewBlock(blocks: BlockExtended[]): Promise<void> {
|
||||
const readyPromises: Promise<TransactionStripped[]>[] = [];
|
||||
const previousBlocks = this.latestBlocks;
|
||||
const newHeights = {};
|
||||
this.latestBlocks = blocks;
|
||||
for (const block of blocks) {
|
||||
newHeights[block.height] = true;
|
||||
if (!this.strippedTransactions[block.height]) {
|
||||
readyPromises.push(new Promise((resolve) => {
|
||||
const subscription = this.apiService.getStrippedBlockTransactions$(block.id).pipe(
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
}),
|
||||
).subscribe((transactions) => {
|
||||
this.strippedTransactions[block.height] = transactions;
|
||||
subscription.unsubscribe();
|
||||
resolve(transactions);
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
await Promise.allSettled(readyPromises);
|
||||
this.updateBlockGraphs(blocks);
|
||||
|
||||
// free up old transactions
|
||||
previousBlocks.forEach(block => {
|
||||
if (!newHeights[block.height]) {
|
||||
delete this.strippedTransactions[block.height];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateBlockGraphs(blocks): void {
|
||||
const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
|
||||
if (this.blockGraphs) {
|
||||
this.blockGraphs.forEach((graph, index) => {
|
||||
graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index));
|
||||
});
|
||||
}
|
||||
this.showInfo = false;
|
||||
setTimeout(() => {
|
||||
this.blockInfo = blocks.map(block => {
|
||||
return {
|
||||
...block,
|
||||
timeString: (new Date(block.timestamp * 1000)).toLocaleTimeString(),
|
||||
};
|
||||
});
|
||||
this.showInfo = true;
|
||||
}, 1600); // Should match the animation time.
|
||||
}
|
||||
|
||||
setupBlockGraphs(): void {
|
||||
if (this.blockGraphs) {
|
||||
this.blockGraphs.forEach((graph, index) => {
|
||||
graph.destroy();
|
||||
graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
|
||||
if (!event.keyModifier) {
|
||||
this.router.navigate([url]);
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
onTxHover(txid: string): void {
|
||||
if (txid && txid.length) {
|
||||
this.hoverTx = txid;
|
||||
} else {
|
||||
this.hoverTx = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="progress inc-tx-progress-bar">
|
||||
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth, 'background-color': mempoolInfoData.progressColor}"> </div>
|
||||
<div class="progress-text" *only-vsize>‎{{ mempoolInfoData.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
|
||||
<div class="progress-text" *only-weight>‎{{ mempoolInfoData.vBytesPerSecond * 4 | ceil | number }} <ng-container i18n="shared.weight-units-per-second|vB/s">WU/s</ng-container></div>
|
||||
<div class="progress-text" *only-weight>‎{{ mempoolInfoData.vBytesPerSecond * 4 | ceil | number }} <ng-container i18n="shared.weight-per-second|WU/s">WU/s</ng-container></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<a class="dropdown-item" routerLinkActive="active"
|
||||
[routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a>
|
||||
<a *ngIf="stateService.env.AUDIT" class="dropdown-item" routerLinkActive="active"
|
||||
[routerLink]="['/graphs/mining/block-health' | relativeUrl]" i18n="mining.block-health">Block Health</a>
|
||||
[routerLink]="['/graphs/mining/block-health' | relativeUrl]" i18n="mining.blocks-health">Block Health</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -249,10 +249,8 @@ export class HashrateChartComponent implements OnInit {
|
||||
for (const tick of ticks) {
|
||||
if (tick.seriesIndex === 0) { // Hashrate
|
||||
let hashrate = tick.data[1];
|
||||
if (this.isMobile()) {
|
||||
hashratePowerOfTen = selectPowerOfTen(tick.data[1]);
|
||||
hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider);
|
||||
}
|
||||
hashratePowerOfTen = selectPowerOfTen(tick.data[1], 10);
|
||||
hashrate = tick.data[1] / hashratePowerOfTen.divider;
|
||||
hashrateString = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s<br>`;
|
||||
} else if (tick.seriesIndex === 1) { // Difficulty
|
||||
let difficultyPowerOfTen = hashratePowerOfTen;
|
||||
@@ -260,18 +258,14 @@ export class HashrateChartComponent implements OnInit {
|
||||
if (difficulty === null) {
|
||||
difficultyString = `${tick.marker} ${tick.seriesName}: No data<br>`;
|
||||
} else {
|
||||
if (this.isMobile()) {
|
||||
difficultyPowerOfTen = selectPowerOfTen(tick.data[1]);
|
||||
difficulty = Math.round(tick.data[1] / difficultyPowerOfTen.divider);
|
||||
}
|
||||
difficultyPowerOfTen = selectPowerOfTen(tick.data[1]);
|
||||
difficulty = tick.data[1] / difficultyPowerOfTen.divider;
|
||||
difficultyString = `${tick.marker} ${tick.seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}<br>`;
|
||||
}
|
||||
} else if (tick.seriesIndex === 2) { // Hashrate MA
|
||||
let hashrate = tick.data[1];
|
||||
if (this.isMobile()) {
|
||||
hashratePowerOfTen = selectPowerOfTen(tick.data[1]);
|
||||
hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider);
|
||||
}
|
||||
hashratePowerOfTen = selectPowerOfTen(tick.data[1], 10);
|
||||
hashrate = tick.data[1] / hashratePowerOfTen.divider;
|
||||
hashrateStringMA = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { formatNumber } from '@angular/common';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
const OUTLIERS_MEDIAN_MULTIPLIER = 4;
|
||||
|
||||
@Component({
|
||||
selector: 'app-incoming-transactions-graph',
|
||||
templateUrl: './incoming-transactions-graph.component.html',
|
||||
@@ -29,6 +31,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
@Input() left: number | string = '0';
|
||||
@Input() template: ('widget' | 'advanced') = 'widget';
|
||||
@Input() windowPreferenceOverride: string;
|
||||
@Input() outlierCappingEnabled: boolean = false;
|
||||
|
||||
isLoading = true;
|
||||
mempoolStatsChartOption: EChartsOption = {};
|
||||
@@ -40,6 +43,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
MA: number[][] = [];
|
||||
weightMode: boolean = false;
|
||||
rateUnitSub: Subscription;
|
||||
medianVbytesPerSecond: number | undefined;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
@@ -65,16 +69,35 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
|
||||
const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8));
|
||||
this.MA = this.calculateMA(this.data.series[0], windowSize);
|
||||
if (this.outlierCappingEnabled === true) {
|
||||
this.computeMedianVbytesPerSecond(this.data.series[0]);
|
||||
}
|
||||
this.mountChart();
|
||||
}
|
||||
|
||||
rendered() {
|
||||
if (!this.data) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the median value of the vbytes per second chart to hide outliers
|
||||
*/
|
||||
computeMedianVbytesPerSecond(data: number[][]): void {
|
||||
const vBytes: number[] = [];
|
||||
for (const value of data) {
|
||||
vBytes.push(value[1]);
|
||||
}
|
||||
const sorted = vBytes.slice().sort((a, b) => a - b);
|
||||
const middle = Math.floor(sorted.length / 2);
|
||||
this.medianVbytesPerSecond = sorted[middle];
|
||||
if (sorted.length % 2 === 0) {
|
||||
this.medianVbytesPerSecond = (sorted[middle - 1] + sorted[middle]) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// calculate the moving average of the provided data based on windowSize
|
||||
calculateMA(data: number[][], windowSize: number = 100): number[][] {
|
||||
//update const variables that are not changed
|
||||
@@ -186,8 +209,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
|
||||
return obj;
|
||||
},
|
||||
extraCssText: `width: ${(['2h', '24h'].includes(this.windowPreference) || this.template === 'widget') ? '125px' : '135px'};
|
||||
background: transparent;
|
||||
extraCssText: `background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;`,
|
||||
axisPointer: {
|
||||
@@ -210,7 +232,8 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
}
|
||||
}
|
||||
});
|
||||
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
|
||||
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}"
|
||||
style="width: ${(this.windowPreference === '2h' || this.template === 'widget') ? '125px' : '215px'}">${itemFormatted}</div>`;
|
||||
}
|
||||
},
|
||||
xAxis: [
|
||||
@@ -232,6 +255,13 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
||||
}
|
||||
],
|
||||
yAxis: {
|
||||
max: (value) => {
|
||||
if (!this.outlierCappingEnabled || value.max < this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER) {
|
||||
return undefined;
|
||||
} else {
|
||||
return Math.round(this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER);
|
||||
}
|
||||
},
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ng-container *ngIf="{ val: network$ | async } as network">
|
||||
<header>
|
||||
<header class="sticky-header">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
li.nav-item.active {
|
||||
background-color: #653b9c;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<!-- Hamburger -->
|
||||
<ng-container *ngIf="servicesEnabled">
|
||||
<div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)">
|
||||
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/image/' + user.username + '?md5=' + user.imageMd5" class="profile_image">
|
||||
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/images/' + user.username + '?md5=' + user.imageMd5" class="profile_image">
|
||||
<app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images>
|
||||
</div>
|
||||
<div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)">
|
||||
@@ -18,7 +18,7 @@
|
||||
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
|
||||
<ng-template [ngIf]="subdomain">
|
||||
<div class="subdomain_container">
|
||||
<img [src]="'/api/v1/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
|
||||
<img [src]="'/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
@@ -71,13 +71,14 @@
|
||||
<a class="nav-link" [routerLink]="['/about']" (click)="collapse()"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" i18n-title="master-page.about" title="About"></fa-icon></a>
|
||||
</li>
|
||||
</ul>
|
||||
<app-search-form class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
|
||||
<app-search-form [hamburgerOpen]="user != null" class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="d-flex" style="overflow: clip">
|
||||
<app-menu *ngIf="servicesEnabled" [navOpen]="menuOpen" (loggedOut)="onLoggedOut()" (menuToggled)="menuToggled($event)"></app-menu>
|
||||
<div *ngIf="!servicesEnabled" class="sidenav"><!-- empty sidenav needed to push footer down the screen --></div>
|
||||
|
||||
<div class="flex-grow-1 d-flex flex-column">
|
||||
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>
|
||||
|
||||
@@ -238,4 +238,15 @@ nav {
|
||||
main {
|
||||
transition: 0.2s;
|
||||
transition-property: max-width;
|
||||
}
|
||||
}
|
||||
|
||||
// empty sidenav
|
||||
.sidenav {
|
||||
z-index: 1;
|
||||
background-color: transparent;
|
||||
width: 0px;
|
||||
height: calc(100vh - 65px);
|
||||
position: sticky;
|
||||
top: 65px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 100;
|
||||
return positions;
|
||||
},
|
||||
extraCssText: `width: ${(this.template === 'advanced') ? '275px' : '200px'};
|
||||
extraCssText: `width: ${(this.template === 'advanced') ? '300px' : '200px'};
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;`,
|
||||
@@ -254,7 +254,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
|
||||
const { totalValue, totalValueArray } = this.getTotalValues(params);
|
||||
const itemFormatted = [];
|
||||
let totalParcial = 0;
|
||||
let sum = 0;
|
||||
let progressPercentageText = '';
|
||||
let countItem;
|
||||
let items = this.inverted ? [...params].reverse() : params;
|
||||
@@ -262,7 +262,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
countItem = items.pop();
|
||||
}
|
||||
items.map((item: any, index: number) => {
|
||||
totalParcial += item.value[1];
|
||||
sum += item.value[1];
|
||||
const progressPercentage = (item.value[1] / totalValue) * 100;
|
||||
const progressPercentageSum = (totalValueArray[index] / totalValue) * 100;
|
||||
let activeItemClass = '';
|
||||
@@ -279,7 +279,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
<span class="symbol">%</span>
|
||||
</span>
|
||||
<span class="total-parcial-vbytes">
|
||||
${this.vbytesPipe.transform(totalParcial, 2, 'vB', 'MvB', false)}
|
||||
${this.vbytesPipe.transform(sum, 2, 'vB', 'MvB', false)}
|
||||
</span>
|
||||
<div class="total-percentage-bar">
|
||||
<span class="total-percentage-bar-background">
|
||||
@@ -303,12 +303,12 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
</td>
|
||||
<td class="total-progress-sum">
|
||||
<span>
|
||||
${this.vbytesPipe.transform(item.value[1], 2, 'vB', 'MvB', false)}
|
||||
${(item.value[1] / 1_000_000).toFixed(2)} <span class="symbol">MvB</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="total-progress-sum">
|
||||
<span>
|
||||
${this.vbytesPipe.transform(totalValueArray[index], 2, 'vB', 'MvB', false)}
|
||||
${(totalValueArray[index] / 1_000_000).toFixed(2)} <span class="symbol">MvB</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="total-progress-sum-bar">
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
<nav class="scrollable menu-click">
|
||||
<span *ngIf="userAuth" class="menu-click">
|
||||
<strong class="menu-click">@ {{ userAuth.user.username }}</strong>
|
||||
<strong class="menu-click text-nowrap ellipsis">@ {{ userAuth.user.username }}</strong>
|
||||
</span>
|
||||
<a *ngIf="!userAuth" class="d-flex justify-content-center align-items-center nav-link m-0 menu-click" routerLink="/login" role="tab" (click)="onLinkClick('/login')">
|
||||
<fa-icon class="menu-click" [icon]="['fas', 'user-circle']" [fixedWidth]="true" style="font-size: 25px;margin-right: 15px;"></fa-icon>
|
||||
<span class="menu-click" style="font-size: 20px;">Sign in</span>
|
||||
<span class="menu-click" style="font-size: 20px;" i18n="shared.sign-in">Sign in</span>
|
||||
</a>
|
||||
|
||||
<ng-container *ngIf="userMenuGroups$ | async as menuGroups">
|
||||
|
||||
@@ -9,17 +9,27 @@
|
||||
margin-left: -250px;
|
||||
box-shadow: 5px 0px 30px 0px #000;
|
||||
padding-bottom: 20px;
|
||||
@media (max-width: 613px) {
|
||||
top: 105px;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidenav.open {
|
||||
margin-left: 0px;
|
||||
left: 0px;
|
||||
display: block;
|
||||
background-color: #1d1f31;
|
||||
}
|
||||
|
||||
.sidenav a, button{
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Latest blocks -->
|
||||
<!-- Recent blocks -->
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
||||
<h5 class="card-title d-inline" i18n="dashboard.recent-blocks">Recent Blocks</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="pool-distribution" *ngIf="(miningStatsObservable$ | async) as miningStats; else loadingReward">
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block" i18n="mining.miners-luck" i18n-ngbTooltip="mining.miners-luck-1w"
|
||||
ngbTooltip="Pools luck (1 week)" placement="bottom" #minersluck [disableTooltip]="!isEllipsisActive(minersluck)">Pools luck</h5>
|
||||
ngbTooltip="Pools luck (1 week)" placement="bottom" #minersluck [disableTooltip]="!isEllipsisActive(minersluck)">Pools Luck</h5>
|
||||
<p class="card-text" i18n-ngbTooltip="mining.pools-luck-desc"
|
||||
ngbTooltip="The overall luck of all mining pools over the past week. A luck bigger than 100% means the average block time for the current epoch is less than 10 minutes." placement="bottom">
|
||||
{{ miningStats['minersLuck'] }}%
|
||||
@@ -14,14 +14,14 @@
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block" i18n="mining.miners-count" i18n-ngbTooltip="mining.miners-count-1w"
|
||||
ngbTooltip="Pools count (1w)" placement="bottom" #poolscount [disableTooltip]="!isEllipsisActive(poolscount)">Pools count</h5>
|
||||
ngbTooltip="Pools count (1w)" placement="bottom" #poolscount [disableTooltip]="!isEllipsisActive(poolscount)">Pools Count</h5>
|
||||
<p class="card-text" i18n-ngbTooltip="mining.pools-count-desc"
|
||||
ngbTooltip="How many unique pools found at least one block over the past week." placement="bottom">
|
||||
{{ miningStats.pools.length }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block" i18n="master-page.blocks" i18n-ngbTooltip="master-page.blocks"
|
||||
<h5 class="card-title d-inline-block" i18n="shared.blocks-1w" i18n-ngbTooltip="master-page.blocks"
|
||||
ngbTooltip="Blocks (1w)" placement="bottom" #blockscount [disableTooltip]="!isEllipsisActive(blockscount)">Blocks (1w)</h5>
|
||||
<p class="card-text" i18n-ngbTooltip="mining.blocks-count-desc"
|
||||
ngbTooltip="The number of blocks found over the past week." placement="bottom">
|
||||
@@ -95,7 +95,7 @@
|
||||
<th *ngIf="auditAvailable" class="health text-right widget" i18n="latest-blocks.avg_health"
|
||||
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
|
||||
<th *ngIf="auditAvailable" class="d-none d-sm-table-cell" i18n="mining.fees-per-block">Avg Block Fees</th>
|
||||
<th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty blocks</th>
|
||||
<th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty Blocks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody [attr.data-cy]="'pools-table'" *ngIf="(miningStatsObservable$ | async) as miningStats">
|
||||
@@ -153,19 +153,19 @@
|
||||
<ng-template #loadingReward>
|
||||
<div class="pool-distribution">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.miners-luck">Pools Luck (1w)</h5>
|
||||
<h5 class="card-title" i18n="mining.miners-luck">Pools Luck</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="master-page.blocks">Blocks (1w)</h5>
|
||||
<h5 class="card-title" i18n="mining.miners-count" >Pools Count</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.miners-count">Pools Count (1w)</h5>
|
||||
<h5 class="card-title" i18n="shared.blocks-1w">Blocks (1w)</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
<table class="table table-xs table-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks (24h)</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
|
||||
</tr>
|
||||
@@ -165,7 +165,7 @@
|
||||
<table class="table table-xs table-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks (24h)</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
|
||||
</tr>
|
||||
@@ -433,7 +433,7 @@
|
||||
<table class="table table-xs table-data text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks (24h)</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
|
||||
</tr>
|
||||
@@ -458,7 +458,7 @@
|
||||
<table class="table table-xs table-data text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="24h">Blocks (24h)</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="1w">1w</th>
|
||||
<th scope="col" class="data-title text-center" style="width: 33%" i18n="all">All</th>
|
||||
</tr>
|
||||
|
||||
@@ -163,10 +163,8 @@ export class PoolComponent implements OnInit {
|
||||
let hashratePowerOfTen: any = selectPowerOfTen(1);
|
||||
let hashrate = ticks[0].data[1];
|
||||
|
||||
if (this.isMobile()) {
|
||||
hashratePowerOfTen = selectPowerOfTen(ticks[0].data[1]);
|
||||
hashrate = Math.round(ticks[0].data[1] / hashratePowerOfTen.divider);
|
||||
}
|
||||
hashratePowerOfTen = selectPowerOfTen(ticks[0].data[1], 10);
|
||||
hashrate = ticks[0].data[1] / hashratePowerOfTen.divider;
|
||||
|
||||
return `
|
||||
<b style="color: white; margin-left: 18px">${ticks[0].axisValueLabel}</b><br>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<br><br>
|
||||
|
||||
<h2>Privacy Policy</h2>
|
||||
<h6>Updated: November 18, 2021</h6>
|
||||
<h6>Updated: November 23, 2023</h6>
|
||||
|
||||
<br><br>
|
||||
|
||||
@@ -53,6 +53,26 @@
|
||||
|
||||
<br>
|
||||
|
||||
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
|
||||
|
||||
<p>If you sign up for an account on mempool.space, we may collect the following:</p>
|
||||
|
||||
<ol>
|
||||
|
||||
<li>If you provide your name, country, and/or e-mail address, 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 detailed below if you sponsor The Mempool Open Source Project®, purchase a subscription to Mempool Enterprise®, or accelerate transactions using Mempool Accelerator™.</li>
|
||||
|
||||
<li>If you connect your Twitter account, we may store your Twitter identity, e-mail address, and profile photo. We may publicly display your profile photo or link to your profile on our website, if you sponsor The Mempool Open Source Project, claim your Lightning node, or other such use cases.</li>
|
||||
|
||||
<li>If you make a credit card payment, we will process your payment using Square (Block, Inc.), and we will store details about the transaction in our database. 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 Bitcoin or Liquid payment, we will process your payment using our self-hosted BTCPay Server instance and not share these details with any third-party.</li>
|
||||
|
||||
<li>If you accelerate transactions using Mempool Accelerator™, we will store the TXID of your transactions you accelerate with us. We share this information with our mining pool partners, as well as publicly display accelerated transaction details on our website and APIs.</li>
|
||||
|
||||
</ol>
|
||||
|
||||
<br>
|
||||
|
||||
<p>EOF</p>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -6,11 +6,9 @@
|
||||
<form class="formRadioGroup">
|
||||
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||
<label class="btn btn-primary btn-sm" [class.active]="!fullRbf">
|
||||
<input type="radio" [value]="'All'" fragment="" [routerLink]="[]"> All
|
||||
</label>
|
||||
<input type="radio" [value]="'All'" fragment="" [routerLink]="[]"><span i18n="all">All</span></label>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="fullRbf">
|
||||
<input type="radio" [value]="'Full RBF'" fragment="fullrbf" [routerLink]="[]"> Full RBF
|
||||
</label>
|
||||
<input type="radio" [value]="'Full RBF'" fragment="fullrbf" [routerLink]="[]" i18n="transaction.full-rbf">Full RBF</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -33,7 +31,7 @@
|
||||
</div>
|
||||
|
||||
<div class="no-replacements" *ngIf="!trees?.length">
|
||||
<p i18n="rbf.no-replacements-yet">there are no replacements in the mempool yet!</p>
|
||||
<p i18n="rbf.no-replacements-yet">There are no replacements in the mempool yet!</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ export class RbfList implements OnInit, OnDestroy {
|
||||
})
|
||||
);
|
||||
|
||||
this.seoService.setTitle($localize`:@@meta.title.rbf-list:RBF Replacements`);
|
||||
this.seoService.setTitle($localize`:@@5e3d5a82750902f159122fcca487b07f1af3141f:RBF Replacements`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.rbf-list:See the most recent RBF replacements on the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network, updated in real-time.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.status|Transaction Status">Status</td>
|
||||
<td>
|
||||
<span *ngIf="rbfInfo.tx.fullRbf" class="badge badge-info" i18n="rbfInfo-features.tag.full-rbf|Full RBF">Full RBF</span>
|
||||
<span *ngIf="rbfInfo.tx.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
|
||||
<span *ngIf="rbfInfo.tx.rbf; else rbfDisabled" class="badge badge-success" i18n="rbfInfo-features.tag.rbf|RBF">RBF</span>
|
||||
<ng-template #rbfDisabled><span class="badge badge-danger mr-1"><del i18n="rbfInfo-features.tag.rbf|RBF">RBF</del></span></ng-template>
|
||||
<ng-template #rbfDisabled><span class="badge badge-danger mr-1"><del i18n="tx-features.tag.rbf|RBF">RBF</del></span></ng-template>
|
||||
<span *ngIf="rbfInfo.tx.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<form [class]="{hamburgerOpen: hamburgerOpen}" [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<div class="d-flex">
|
||||
<div class="search-box-container mr-2">
|
||||
<input #searchInput (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||
|
||||
@@ -26,6 +26,13 @@ form {
|
||||
@media (min-width: 992px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.hamburgerOpen {
|
||||
@media (max-width: 613px) {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
|
||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef, Input } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
@@ -17,6 +17,8 @@ import { SearchResultsComponent } from './search-results/search-results.componen
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SearchFormComponent implements OnInit {
|
||||
@Input() hamburgerOpen = false;
|
||||
|
||||
network = '';
|
||||
assets: object = {};
|
||||
isSearching = false;
|
||||
@@ -38,6 +40,8 @@ export class SearchFormComponent implements OnInit {
|
||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||
regexBlockheight = /^[0-9]{1,9}$/;
|
||||
regexDate = /^(?:\d{4}[-/]\d{1,2}[-/]\d{1,2}(?: \d{1,2}:\d{2})?)$/;
|
||||
regexUnixTimestamp = /^\d{10}$/;
|
||||
focus$ = new Subject<string>();
|
||||
click$ = new Subject<string>();
|
||||
|
||||
@@ -171,6 +175,8 @@ export class SearchFormComponent implements OnInit {
|
||||
const lightningResults = result[1];
|
||||
|
||||
const matchesBlockHeight = this.regexBlockheight.test(searchText);
|
||||
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date';
|
||||
const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText);
|
||||
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
|
||||
const matchesBlockHash = this.regexBlockhash.test(searchText);
|
||||
const matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
|
||||
@@ -179,10 +185,16 @@ export class SearchFormComponent implements OnInit {
|
||||
searchText = 'B' + searchText;
|
||||
}
|
||||
|
||||
if (matchesDateTime && searchText.indexOf('/') !== -1) {
|
||||
searchText = searchText.replace(/\//g, '-');
|
||||
}
|
||||
|
||||
return {
|
||||
searchText: searchText,
|
||||
hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress),
|
||||
hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress || matchesUnixTimestamp || matchesDateTime),
|
||||
blockHeight: matchesBlockHeight,
|
||||
dateTime: matchesDateTime,
|
||||
unixTimestamp: matchesUnixTimestamp,
|
||||
txId: matchesTxId,
|
||||
blockHash: matchesBlockHash,
|
||||
address: matchesAddress,
|
||||
@@ -241,6 +253,13 @@ export class SearchFormComponent implements OnInit {
|
||||
} else {
|
||||
this.navigate('/tx/', matches[0]);
|
||||
}
|
||||
} else if (this.regexDate.test(searchText) || this.regexUnixTimestamp.test(searchText)) {
|
||||
let timestamp: number;
|
||||
this.regexDate.test(searchText) ? timestamp = Math.floor(new Date(searchText).getTime() / 1000) : timestamp = searchText;
|
||||
this.apiService.getBlockDataFromTimestamp$(timestamp).subscribe(
|
||||
(data) => { this.navigate('/block/', data.hash); },
|
||||
(error) => { console.log(error); this.isSearching = false; }
|
||||
);
|
||||
} else {
|
||||
this.searchResults.searchButtonClick();
|
||||
this.isSearching = false;
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.dateTime">
|
||||
<div class="card-title" i18n="search.bitcoin-block-date">Date</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.unixTimestamp">
|
||||
<div class="card-title" i18n="search.bitcoin-block-timestamp">Timestamp</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.txId">
|
||||
<div class="card-title" i18n="search.bitcoin-transaction">Bitcoin Transaction</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
[class]="(stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING) ? 'mining' : 'no-menu'" (click)="saveGraphPreference()">
|
||||
<div class="small-buttons">
|
||||
<a class="btn btn-primary btn-sm mb-0" [routerLink]="['/clock/mempool/0' | relativeUrl]" style="color: white" id="btn-clock">
|
||||
<fa-icon [icon]="['fas', 'clock']" [fixedWidth]="true" i18n-title="master-page.clockview" title="Clock view"></fa-icon>
|
||||
<fa-icon [icon]="['fas', 'clock']" [fixedWidth]="true" i18n-title="master-page.clockview" i18n-title="footer.clock-mempool" title="Clock (Mempool)"></fa-icon>
|
||||
</a>
|
||||
<a *ngIf="!isMobile()" class="btn btn-primary btn-sm mb-0" [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
|
||||
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
|
||||
@@ -109,18 +109,26 @@
|
||||
<div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline">
|
||||
<span i18n="statistics.transaction-vbytes-per-second">Transaction vBytes per second (vB/s)</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart('incoming')">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="vbytes-title">
|
||||
<div>
|
||||
<span i18n="statistics.transaction-vbytes-per-second">Transaction vBytes per second (vB/s)</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart('incoming')">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input style="margin-top: 9px" class="form-check-input" type="checkbox" [checked]="outlierCappingEnabled" id="hide-outliers" (change)="onOutlierToggleChange($event)">
|
||||
<label class="form-check-label" for="hide-outliers">
|
||||
<small i18n="statistics.cap-outliers">Cap outliers</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="incoming-transactions-graph">
|
||||
<app-incoming-transactions-graph #incominggraph [height]="500" [left]="65" [template]="'advanced'"
|
||||
[data]="mempoolTransactionsWeightPerSecondData"></app-incoming-transactions-graph>
|
||||
[data]="mempoolTransactionsWeightPerSecondData" [outlierCappingEnabled]="outlierCappingEnabled"></app-incoming-transactions-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -222,4 +222,13 @@
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vbytes-title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
@media (max-width: 767px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export class StatisticsComponent implements OnInit {
|
||||
showCount = false;
|
||||
maxFeeIndex: number;
|
||||
dropDownOpen = false;
|
||||
|
||||
outlierCappingEnabled = false;
|
||||
mempoolStats: OptimizedMempoolStats[] = [];
|
||||
|
||||
mempoolVsizeFeesData: any;
|
||||
@@ -67,6 +67,7 @@ export class StatisticsComponent implements OnInit {
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.mempool:See mempool size (in MvB) and transactions per second (in vB/s) visualized over time.`);
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
this.graphWindowPreference = this.storageService.getValue('graphWindowPreference') ? this.storageService.getValue('graphWindowPreference').trim() : '2h';
|
||||
this.outlierCappingEnabled = this.storageService.getValue('cap-outliers') === 'true';
|
||||
|
||||
this.radioGroupForm = this.formBuilder.group({
|
||||
dateSpan: this.graphWindowPreference
|
||||
@@ -156,8 +157,6 @@ export class StatisticsComponent implements OnInit {
|
||||
}
|
||||
this.maxFeeIndex = maxTier;
|
||||
|
||||
this.capExtremeVbytesValues();
|
||||
|
||||
this.mempoolTransactionsWeightPerSecondData = {
|
||||
labels: labels,
|
||||
series: [mempoolStats.map((stats) => [stats.added * 1000, stats.vbytes_per_second])],
|
||||
@@ -211,36 +210,10 @@ export class StatisticsComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* All value higher that "median * capRatio" are capped
|
||||
*/
|
||||
capExtremeVbytesValues() {
|
||||
if (this.stateService.network.length !== 0) {
|
||||
return; // Only cap on Bitcoin mainnet
|
||||
}
|
||||
|
||||
let capRatio = 10;
|
||||
if (['1m', '3m', '6m', '1y', '2y', '3y', '4y'].includes(this.graphWindowPreference)) {
|
||||
capRatio = 4;
|
||||
}
|
||||
|
||||
// Find median value
|
||||
const vBytes: number[] = [];
|
||||
for (const stat of this.mempoolStats) {
|
||||
vBytes.push(stat.vbytes_per_second);
|
||||
}
|
||||
const sorted = vBytes.slice().sort((a, b) => a - b);
|
||||
const middle = Math.floor(sorted.length / 2);
|
||||
let median = sorted[middle];
|
||||
if (sorted.length % 2 === 0) {
|
||||
median = (sorted[middle - 1] + sorted[middle]) / 2;
|
||||
}
|
||||
|
||||
// Cap
|
||||
for (const stat of this.mempoolStats) {
|
||||
stat.vbytes_per_second = Math.min(median * capRatio, stat.vbytes_per_second);
|
||||
}
|
||||
|
||||
onOutlierToggleChange(e): void {
|
||||
this.outlierCappingEnabled = e.target.checked;
|
||||
this.storageService.setValue('cap-outliers', e.target.checked);
|
||||
}
|
||||
|
||||
onSaveChart(name) {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<ng-container [ngSwitch]="extraData">
|
||||
<div class="opreturns" *ngSwitchCase="'coinbase'">
|
||||
<div class="opreturn-row">
|
||||
<span class="label">Coinbase</span>
|
||||
<span class="label" i18n="transactions-list.coinbase">Coinbase</span>
|
||||
<span class="message">{{ tx.vin[0].scriptsig | hex2ascii }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
this.seoService.setTitle(
|
||||
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
||||
);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.transaction:Get real-time status, addresses, fees, script info, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transaction with txid {txid}.`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.transaction:Get real-time status, addresses, fees, script info, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transaction with txid ${this.txId}.`);
|
||||
this.resetTransaction();
|
||||
return merge(
|
||||
of(true),
|
||||
|
||||
@@ -6,12 +6,15 @@
|
||||
<app-truncate [text]="rbfTransaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, rbfTransaction.txid]"></app-truncate>
|
||||
</div>
|
||||
|
||||
<div *ngIf="acceleratorAvailable && accelerateCtaType === 'alert' && !tx?.status?.confirmed && !tx?.acceleration" class="alert alert-mempool alert-dismissible" role="alert">
|
||||
<span><a class="link accelerator" (click)="onAccelerateClicked()">Accelerate</a> this transaction using Mempool Accelerator ™</span>
|
||||
<!-- <div *ngIf="tx && acceleratorAvailable && accelerateCtaType === 'alert' && !tx.status.confirmed && !tx.acceleration" class="alert alert-dismissible alert-purple" role="alert">
|
||||
<div>
|
||||
<a class="btn btn-sm blink-bg" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
<span class="align-middle">this transaction using Mempool Accelerator ™</span>
|
||||
</div>
|
||||
<button type="button" class="close" aria-label="Close" (click)="dismissAccelAlert()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size || tx">
|
||||
<h1 i18n="shared.transaction">Transaction</h1>
|
||||
@@ -80,13 +83,16 @@
|
||||
|
||||
<!-- Accelerator -->
|
||||
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary">
|
||||
<div class="title mt-3" id="acceleratePreviewAnchor">
|
||||
<br>
|
||||
<div class="title float-left">
|
||||
<h2>Accelerate</h2>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-info accelerator-toggle btn-sm float-right" (click)="showAccelerationSummary = false" i18n="hide-accelerator">Hide accelerator</button>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="box">
|
||||
<app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-template #unconfirmedTemplate>
|
||||
@@ -118,7 +124,7 @@
|
||||
<ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit">
|
||||
<span [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
|
||||
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
|
||||
<a *ngIf="acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerateDeepMempool" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
<a *ngIf="!tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
</span>
|
||||
</ng-template>
|
||||
<ng-template #belowBlockLimit>
|
||||
@@ -128,14 +134,14 @@
|
||||
<ng-template #timeEstimateDefault>
|
||||
<span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''">
|
||||
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
<a *ngIf="acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
<a *ngIf="!tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
</span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" id="acceleratePreviewAnchor">
|
||||
<td class="td-width" i18n="transaction.features|Transaction Features">Features</td>
|
||||
<td>
|
||||
<app-tx-features [tx]="tx"></app-tx-features>
|
||||
@@ -506,18 +512,32 @@
|
||||
<app-fee-rate [fee]="tx.feePerVsize"></app-fee-rate>
|
||||
<ng-template [ngIf]="tx?.status?.confirmed">
|
||||
|
||||
<app-tx-fee-rating *ngIf="tx.fee && !hasEffectiveFeeRate" [tx]="tx"></app-tx-fee-rating>
|
||||
<app-tx-fee-rating *ngIf="tx.fee && !hasEffectiveFeeRate && !accelerationInfo" [tx]="tx"></app-tx-fee-rating>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="cpfpInfo && hasEffectiveFeeRate">
|
||||
<td *ngIf="tx.acceleration" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
|
||||
<td *ngIf="!tx.acceleration" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<tr *ngIf="!hasEffectiveFeeRate && accelerationInfo">
|
||||
<td i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
|
||||
<td>
|
||||
<div class="effective-fee-container">
|
||||
<app-fee-rate [fee]="accelerationInfo.effectiveFee + accelerationInfo.feePaid - accelerationInfo.baseFee - accelerationInfo.vsizeFee" [weight]="accelerationInfo.effectiveVsize * 4"></app-fee-rate>
|
||||
|
||||
<span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="!accelerationInfo && cpfpInfo && hasEffectiveFeeRate">
|
||||
<td *ngIf="tx.acceleration || accelerationInfo" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
|
||||
<td *ngIf="!(tx.acceleration || accelerationInfo)" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td>
|
||||
<div class="effective-fee-container">
|
||||
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
<ng-template [ngIf]="tx?.status?.confirmed">
|
||||
<app-tx-fee-rating class="ml-2 mr-2" *ngIf="tx.fee || tx.effectiveFeePerVsize" [tx]="tx"></app-tx-fee-rating>
|
||||
<app-tx-fee-rating class="ml-2 mr-2 effective-fee-rating" *ngIf="!accelerationInfo && (tx.fee || tx.effectiveFeePerVsize)" [tx]="tx"></app-tx-fee-rating>
|
||||
<ng-template [ngIf]="accelerationInfo">
|
||||
|
||||
<span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
<button *ngIf="cpfpInfo.bestDescendant || cpfpInfo.descendants?.length || cpfpInfo.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
|
||||
@@ -60,8 +60,13 @@
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.badge.badge-accelerated {
|
||||
background-color: #653b9c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-small-height {
|
||||
line-height: 1.1;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.arrow-green {
|
||||
@@ -152,6 +157,16 @@
|
||||
@media (min-width: 768px){
|
||||
display: inline-block;
|
||||
}
|
||||
@media (max-width: 425px){
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.effective-fee-rating {
|
||||
@media (max-width: 767px){
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -169,7 +184,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.details-button, .flow-toggle {
|
||||
.details-button, .flow-toggle, .accelerator-toggle {
|
||||
margin-top: -5px;
|
||||
margin-left: 10px;
|
||||
@media (min-width: 768px){
|
||||
@@ -218,8 +233,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.link.accelerator {
|
||||
cursor: pointer;
|
||||
.alert-purple {
|
||||
background-color: #5c3a88;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Blinking block
|
||||
@keyframes shadowyBackground {
|
||||
0% {
|
||||
box-shadow: 0px 0px 20px rgba(#eba814, 1);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0px 0px 20px rgba(#eba814, .3);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0px 0px 20px rgba(#ffae00, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.blink-bg {
|
||||
color: #fff;
|
||||
background: repeating-linear-gradient(#daad0a 0%, #daad0a 5%, #987805 100%) !important;
|
||||
animation: shadowyBackground 1s infinite;
|
||||
box-shadow: 0px 0px 20px rgba(#eba814, 1);
|
||||
transition: 100ms all ease-in;
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
border: 1px solid gold;
|
||||
}
|
||||
|
||||
.eta {
|
||||
@@ -234,7 +274,6 @@
|
||||
.accelerate {
|
||||
display: flex !important;
|
||||
align-self: auto;
|
||||
margin-top: 3px;
|
||||
margin-left: auto;
|
||||
background-color: #653b9c;
|
||||
@media (max-width: 849px) {
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment } from '../../interfaces/node-api.interface';
|
||||
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration } from '../../interfaces/node-api.interface';
|
||||
import { LiquidUnblinding } from './liquid-ublinding';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { Price, PriceService } from '../../services/price.service';
|
||||
@@ -49,6 +49,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
fetchCpfpSubscription: Subscription;
|
||||
fetchRbfSubscription: Subscription;
|
||||
fetchCachedTxSubscription: Subscription;
|
||||
fetchAccelerationSubscription: Subscription;
|
||||
txReplacedSubscription: Subscription;
|
||||
txRbfInfoSubscription: Subscription;
|
||||
mempoolPositionSubscription: Subscription;
|
||||
@@ -62,12 +63,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
rbfReplaces: string[];
|
||||
rbfInfo: RbfTree;
|
||||
cpfpInfo: CpfpInfo | null;
|
||||
accelerationInfo: Acceleration | null = null;
|
||||
sigops: number | null;
|
||||
adjustedVsize: number | null;
|
||||
showCpfpDetails = false;
|
||||
fetchCpfp$ = new Subject<string>();
|
||||
fetchRbfHistory$ = new Subject<string>();
|
||||
fetchCachedTx$ = new Subject<string>();
|
||||
fetchAcceleration$ = new Subject<string>();
|
||||
isCached: boolean = false;
|
||||
now = Date.now();
|
||||
da$: Observable<DifficultyAdjustment>;
|
||||
@@ -92,7 +95,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
rbfEnabled: boolean;
|
||||
taprootEnabled: boolean;
|
||||
hasEffectiveFeeRate: boolean;
|
||||
accelerateCtaType: 'alert' | 'button' = 'alert';
|
||||
accelerateCtaType: 'alert' | 'button' = 'button';
|
||||
acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
|
||||
showAccelerationSummary = false;
|
||||
scrollIntoAccelPreview = false;
|
||||
@@ -126,7 +129,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
);
|
||||
|
||||
this.accelerateCtaType = (this.storageService.getValue('accel-cta-type') as 'alert' | 'button') ?? 'alert';
|
||||
this.accelerateCtaType = (this.storageService.getValue('accel-cta-type') as 'alert' | 'button') ?? 'button';
|
||||
|
||||
this.setFlowEnabled();
|
||||
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
|
||||
@@ -238,6 +241,24 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
this.fetchAccelerationSubscription = this.fetchAcceleration$.pipe(
|
||||
tap(() => {
|
||||
this.accelerationInfo = null;
|
||||
}),
|
||||
switchMap((blockHash: string) => {
|
||||
return this.apiService.getAccelerationHistory$({ blockHash });
|
||||
}),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
).subscribe((accelerationHistory) => {
|
||||
for (const acceleration of accelerationHistory) {
|
||||
if (acceleration.txid === this.txId) {
|
||||
this.accelerationInfo = acceleration;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
|
||||
this.now = Date.now();
|
||||
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
|
||||
@@ -365,6 +386,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.getTransactionTime();
|
||||
}
|
||||
} else {
|
||||
this.fetchAcceleration$.next(tx.status.block_hash);
|
||||
this.transactionTime = 0;
|
||||
}
|
||||
|
||||
@@ -417,6 +439,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
};
|
||||
this.stateService.markBlock$.next({ blockHeight: block.height });
|
||||
this.audioService.playSound('magic');
|
||||
this.fetchAcceleration$.next(block.id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -585,6 +608,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.rbfInfo = null;
|
||||
this.rbfReplaces = [];
|
||||
this.showCpfpDetails = false;
|
||||
this.accelerationInfo = null;
|
||||
this.txInBlockIndex = null;
|
||||
this.mempoolPosition = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
@@ -633,10 +657,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
// simulate normal anchor fragment behavior
|
||||
applyFragment(): void {
|
||||
const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === '');
|
||||
if (anchor) {
|
||||
const anchorElement = document.getElementById(anchor[0]);
|
||||
if (anchorElement) {
|
||||
anchorElement.scrollIntoView();
|
||||
if (anchor?.length) {
|
||||
if (anchor[0] === 'accelerate') {
|
||||
setTimeout(this.onAccelerateClicked.bind(this), 100);
|
||||
} else {
|
||||
const anchorElement = document.getElementById(anchor[0]);
|
||||
if (anchorElement) {
|
||||
anchorElement.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -660,6 +688,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.fetchCpfpSubscription.unsubscribe();
|
||||
this.fetchRbfSubscription.unsubscribe();
|
||||
this.fetchCachedTxSubscription.unsubscribe();
|
||||
this.fetchAccelerationSubscription.unsubscribe();
|
||||
this.txReplacedSubscription.unsubscribe();
|
||||
this.txRbfInfoSubscription.unsubscribe();
|
||||
this.queryParamsSubscription.unsubscribe();
|
||||
|
||||
@@ -156,17 +156,45 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
if (this.address) {
|
||||
const addressIn = tx.vout
|
||||
.filter((v: Vout) => v.scriptpubkey_address === this.address)
|
||||
.map((v: Vout) => v.value || 0)
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
const isP2PKUncompressed = this.address.length === 130;
|
||||
const isP2PKCompressed = this.address.length === 66;
|
||||
if (isP2PKCompressed) {
|
||||
const addressIn = tx.vout
|
||||
.filter((v: Vout) => v.scriptpubkey === '21' + this.address + 'ac')
|
||||
.map((v: Vout) => v.value || 0)
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
|
||||
const addressOut = tx.vin
|
||||
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey_address === this.address)
|
||||
.map((v: Vin) => v.prevout.value || 0)
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
const addressOut = tx.vin
|
||||
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey === '21' + this.address + 'ac')
|
||||
.map((v: Vin) => v.prevout.value || 0)
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
|
||||
tx['addressValue'] = addressIn - addressOut;
|
||||
tx['addressValue'] = addressIn - addressOut;
|
||||
} else if (isP2PKUncompressed) {
|
||||
const addressIn = tx.vout
|
||||
.filter((v: Vout) => v.scriptpubkey === '41' + this.address + 'ac')
|
||||
.map((v: Vout) => v.value || 0)
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
|
||||
const addressOut = tx.vin
|
||||
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey === '41' + this.address + 'ac')
|
||||
.map((v: Vin) => v.prevout.value || 0)
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
|
||||
tx['addressValue'] = addressIn - addressOut;
|
||||
} else {
|
||||
const addressIn = tx.vout
|
||||
.filter((v: Vout) => v.scriptpubkey_address === this.address)
|
||||
.map((v: Vout) => v.value || 0)
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
|
||||
const addressOut = tx.vin
|
||||
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey_address === this.address)
|
||||
.map((v: Vin) => v.prevout.value || 0)
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
|
||||
tx['addressValue'] = addressIn - addressOut;
|
||||
}
|
||||
}
|
||||
|
||||
this.priceService.getBlockPrice$(tx.status.block_time).pipe(
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<ng-template #coinbase>
|
||||
<ng-container *ngIf="line.coinbase; else pegin">
|
||||
<p>Coinbase</p>
|
||||
<p i18n="transactions-list.coinbase">Coinbase</p>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
<div class="card" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else latestBlocks">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-rbf-replacements">Latest replacements</h5>
|
||||
<h5 class="card-title d-inline" i18n="dashboard.recent-rbf-replacements">Recent Replacements</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
@@ -99,7 +99,7 @@
|
||||
<td class="table-cell-badges">
|
||||
<span *ngIf="replacement.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="replacement.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
|
||||
<span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="transaction.rbf">RBF</span>
|
||||
<span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="tx-features.tag.rbf|RBF">RBF</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -110,7 +110,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
||||
<h5 class="card-title d-inline" i18n="dashboard.recent-blocks">Recent Blocks</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
@@ -150,7 +150,7 @@
|
||||
<div class="col" style="max-height: 410px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" i18n="dashboard.latest-transactions">Latest transactions</h5>
|
||||
<h5 class="card-title" i18n="dashboard.recent-transactions">Recent Transactions</h5>
|
||||
<table class="table latest-transactions">
|
||||
<thead>
|
||||
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
@@ -233,11 +233,11 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="item bar">
|
||||
<h5 class="card-title" i18n="dashboard.memory-usage|Memory usage">Memory usage</h5>
|
||||
<h5 class="card-title" i18n="dashboard.memory-usage|Memory usage">Memory Usage</h5>
|
||||
<div class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loadingbig">
|
||||
<div class="progress">
|
||||
<div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }"> </div>
|
||||
<div class="progress-text">‎<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
|
||||
<div class="progress-text">‎<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes : 2 : 'B' : null : false : 3"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -256,7 +256,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #txPerSecond let-mempoolInfoData>
|
||||
<h5 class="card-title" i18n="dashboard.incoming-transactions">Incoming transactions</h5>
|
||||
<h5 class="card-title" i18n="dashboard.incoming-transactions">Incoming Transactions</h5>
|
||||
<ng-template [ngIf]="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value" [ngIfElse]="loadingTransactions">
|
||||
<span *ngIf="(mempoolLoadingStatus$ | async) !== 100; else inSync">
|
||||
<span class="badge badge-pill badge-warning"><ng-container i18n="dashboard.backend-is-synchronizing">Backend is synchronizing</ng-container> ({{ mempoolLoadingStatus$ | async }}%)</span>
|
||||
|
||||
@@ -2922,7 +2922,7 @@ export const restApiDocsData = [
|
||||
fragment: "get-blocks-bulk",
|
||||
title: "GET Blocks (Bulk)",
|
||||
description: {
|
||||
default: "<p>Returns details on the range of blocks between <code>:minHeight</code> and <code>:maxHeight</code>, inclusive, up to 10 blocks. If <code>:maxHeight</code> is not specified, it defaults to the current tip.</p><p>To return data for more than 10 blocks, consider becoming an <a href='/enterprise'>enterprise sponsor</a>.</p>"
|
||||
default: "<p>Returns details on the range of blocks between <code>:minHeight</code> and <code>:maxHeight</code>, inclusive, up to 10 blocks. If <code>:maxHeight</code> is not specified, it defaults to the current tip.</p><p>To return data for more than 10 blocks, consider becoming an <a href='https://mempool.space/enterprise'>enterprise sponsor</a>.</p>"
|
||||
},
|
||||
urlString: "/v1/blocks-bulk/:minHeight[/:maxHeight]",
|
||||
showConditions: bitcoinNetworks,
|
||||
@@ -8775,6 +8775,83 @@ export const faqData = [
|
||||
fragment: "what-is-svb",
|
||||
title: "What is sat/vB?",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
category: "accelerator",
|
||||
fragment: "mempool-accelerator",
|
||||
title: "Mempool Accelerator™",
|
||||
showConditions: bitcoinNetworks
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "accelerator",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "what-is-mempool-accelerator",
|
||||
title: "What is Mempool Accelerator™?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "accelerator",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "how-does-mempool-accelerator-work",
|
||||
title: "How does Mempool Accelerator™ work?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "accelerator",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "join-accelerator-waitlist",
|
||||
title: "How can I join the Waitlist for the Mempool Accelerator™?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "accelerator",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "who-has-access-to-mempool-accelerator",
|
||||
title: "Who has access to Mempool Accelerator™?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "accelerator",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "account-requirement",
|
||||
title: "Why is an account required for Mempool Accelerator™?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "accelerator",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "mempool-accelerator-balance",
|
||||
title: "How do I deposit into Mempool Accelerator™?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "accelerator",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "how-to-request-an-acceleration",
|
||||
title: "How do I request an acceleration?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "accelerator",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "cancelling-an-acceleration",
|
||||
title: "How do I cancel an acceleration?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "accelerator",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "mempool-accelerator-improves-transparency",
|
||||
title: "Does this break fee estimation algorithms?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "accelerator",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "will-mining-pools-underpay-miners",
|
||||
title: "Will mining pools underpay miners?",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
category: "help",
|
||||
@@ -8872,7 +8949,7 @@ export const faqData = [
|
||||
category: "advanced",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "what-is-memory-usage",
|
||||
title: "What is memory usage?",
|
||||
title: "What is Memory usage?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
<div class="doc-content">
|
||||
|
||||
<div id="disclaimer">
|
||||
<table *ngIf="!mobileViewport"><tr><td><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images></td><td><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, wallet issues, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></td></tr></table>
|
||||
<div *ngIf="mobileViewport"><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, wallet issues, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></div>
|
||||
|
||||
<table *ngIf="!mobileViewport"><tr><td><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images></td><td> <ng-container *ngTemplateOutlet="faqDisclaimer"></ng-container></td></tr></table>
|
||||
<div *ngIf="mobileViewport"><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images><ng-container *ngTemplateOutlet="faqDisclaimer"></ng-container></div>
|
||||
<ng-template #faqDisclaimer i18n="faq.big-disclaimer"><p><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, wallet issues, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></ng-template>
|
||||
</div>
|
||||
|
||||
<div class="doc-item-container" *ngFor="let item of faq">
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="doc-content">
|
||||
|
||||
<p class="doc-welcome-note">Below is a reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} <ng-container i18n="api-docs.title">REST API service</ng-container>.</p>
|
||||
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that we enforce rate limits. If you exceed these limits, you will get an HTTP 429 error. If you repeatedly exceed the limits, you may be banned from accessing the service altogether. Consider an <a href="/enterprise">enterprise sponsorship</a> if you need higher API limits.</p>
|
||||
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that we enforce rate limits. If you exceed these limits, you will get an HTTP 429 error. If you repeatedly exceed the limits, you may be banned from accessing the service altogether. Consider an <a href="https://mempool.space/enterprise">enterprise sponsorship</a> if you need higher API limits.</p>
|
||||
|
||||
<div class="doc-item-container" *ngFor="let item of restDocs">
|
||||
<h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3>
|
||||
@@ -123,7 +123,7 @@
|
||||
<p>{{electrsPort}}</p>
|
||||
<p class="subtitle">SSL</p>
|
||||
<p>Enabled</p>
|
||||
<p class="note" *ngIf="network.val !== 'signet'">Electrum RPC interface for Bitcoin Signet is <a href="/signet/docs/api/electrs">publicly available</a>. Electrum RPC interface for all other networks is available to <a href='/enterprise'>sponsors</a> only—whitelisting is required.</p>
|
||||
<p class="note" *ngIf="network.val !== 'signet'">Electrum RPC interface for Bitcoin Signet is <a href="/signet/docs/api/electrs">publicly available</a>. Electrum RPC interface for all other networks is available to <a href='https://mempool.space/enterprise'>sponsors</a> only—whitelisting is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,15 +180,37 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="why-is-transaction-stuck-in-mempool">
|
||||
<p>If it's been a while and your transaction hasn't confirmed, your transaction is probably using a lower feerate relative to other transactions currently in the mempool. Depending on how you made your transaction, there may be <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-to-get-transaction-confirmed-quickly">ways to accelerate the process</a>.</p><p>There's no need to panic—a Bitcoin transaction will always either confirm completely (or not at all) at some point. As long as you have your transaction's ID, you can always see where your funds are.</p><p style='font-weight:700'>This site only provides data about the Bitcoin network—it cannot help you get your transaction confirmed quicker.</p>
|
||||
<p>If it's been a while and your transaction hasn't confirmed, your transaction is probably using a lower feerate relative to other transactions currently in the mempool.</p>
|
||||
<p style='font-weight:700'> You can accelerate your transaction with <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="what-is-mempool-accelerator">Mempool Accelerator™</a></p>
|
||||
<p>Depending on how you made your transaction, there may be <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-to-get-transaction-confirmed-quickly">other ways</a> to increase the fee.</p>
|
||||
<p>There's no need to panic—a Bitcoin transaction will always either confirm completely (or not at all) at some point. As long as you have your transaction's ID, you can always see where your funds are.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="how-to-get-transaction-confirmed-quickly">
|
||||
<p>To get your transaction confirmed quicker, you will need to increase its effective feerate.</p><p>If your transaction was created with RBF enabled, your stuck transaction can simply be replaced with a new one that has a higher fee.</p><p>Otherwise, if you control any of the stuck transaction's outputs, you can use CPFP to increase your stuck transaction's effective feerate.</p><p>If you are not sure how to do RBF or CPFP, work with the tool you used to make the transaction (wallet software, exchange company, etc). This website only provides data about the Bitcoin network, so there is nothing it can do to help you get your transaction confirmed quicker.</p>
|
||||
<p>There are 3 ways to increase the incentive for a miner to include your transaction</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p><b>Replace By Fee (RBF)</b></p>
|
||||
<p>If you are the sender in a bitcoin transaction you can create a new transaction which spends the same inputs but with a higher fee.</p>
|
||||
<p>Full RBF miners will mine blocks containing your replacement even if you don't flag RBF, but you may have issues propagating the transaction. </p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>Child Pays for Parent (CPFP)</b></p>
|
||||
<p>If you are the recipient of an unconfirmed transaction output you can spend it and increase the effective fee (the weighted average of both the original unconfirmed transaction and your CPFP transaction).</p>
|
||||
<p>A miner can't confirm a CPFP transaction without also confirming the original, making it impossible for the miner to take the fee from your CPFP unless your unconfirmed original transaction is included in the same (or a previous) block.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>Out of Band</b></p>
|
||||
<p>The <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="what-is-mempool-accelerator">Mempool Accelerator™</a> is a transaction acceleration marketplace which enables acceleration offers to be made to mining partners, where payment is made via an alternate method (not in the onchain transaction fee).</p>
|
||||
<p>Unlike RBF and CPFP a new onchain transaction does not need to be signed for each fee increase and depending on the payment method used may not be required at all, but trust is required by one or more parties involved (miner or user). </p>
|
||||
</li>
|
||||
</ol>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="how-prevent-stuck-transaction">
|
||||
<p>You must use an adequate transaction fee commensurate with how quickly you need the transaction to be confirmed. See Mempool's fee estimates on the <a [routerLink]="['/' | relativeUrl]">front page</a>.</p><p>Also consider using RBF (if your wallet supports it) so that you can bump the feerate on your transaction if it does end up getting stuck.</p>
|
||||
<p>You must use an adequate transaction fee commensurate with how quickly you need the transaction to be confirmed. See Mempool's fee estimates on the <a [routerLink]="['/' | relativeUrl]">front page</a>.</p>
|
||||
<p>If you need your transaction to be confirmed quickly but don't want to overpay you can use the <a href="https://mempool.space/accelerator" target="_blank">Mempool Accelerator™ </a>.</p>
|
||||
<p>You may be able to increase your transaction bid manually <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-to-get-transaction-confirmed-quickly">using RBF / CPFP</a>, if your wallet supports these features.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="looking-up-transactions">
|
||||
@@ -288,7 +310,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="host-my-own-instance-server">
|
||||
<p>You can manually install Mempool on your own server, but this requires advanced sysadmin skills since you will be manually configuring everything. You could also use our <a href="https://github.com/mempool/mempool/tree/master/docker" target="_blank">Docker images</a>.</p><p>In any case, we only provide support for manual deployments to <a href="/enterprise">enterprise sponsors</a>.</p>
|
||||
<p>You can manually install Mempool on your own server, but this requires advanced sysadmin skills since you will be manually configuring everything. You could also use our <a href="https://github.com/mempool/mempool/tree/master/docker" target="_blank">Docker images</a>.</p><p>In any case, we only provide support for manual deployments to <a href="https://mempool.space/enterprise">enterprise sponsors</a>.</p>
|
||||
<p>For casual users, we strongly suggest installing Mempool using one of the <a href="https://github.com/mempool/mempool#one-click-installation" target="_blank">1-click install methods</a>.</p>
|
||||
</ng-template>
|
||||
|
||||
@@ -299,3 +321,67 @@
|
||||
<ng-template type="address-lookup-issues">
|
||||
<p>If you're getting errors when doing address lookups, it's probably because of your Electrum server backend.</p><p>Mempool uses an Electrum server to do address lookups. There are several implementations of the Electrum server protocol, and Mempool can use any of them, but the implementation you use affects performance:</p><ol><li><a href="https://github.com/romanz/electrs" target="_blank">romanz/electrs</a>. This is a common choice for its low resource requirements, and most full-node distros use it. But while this implementation works great for basic queries, it will struggle with heavier ones (e.g. looking up addresses with many transactions)—especially when running on low-power hardware like a Raspberry Pi.</li><li><a href="https://github.com/cculianu/Fulcrum" target="_blank">Fulcrum</a>. Fulcrum requires more resources than romanz/electrs but it can still run on a Raspberry Pi, and it handles heavy queries much more efficiently. If you're having issues with romanz/electrs, Fulcrum is worth a try.</li><li><a href="https://github.com/Blockstream/electrs" target="_blank">blockstream/electrs</a>. If you have stronger hardware, consider running Blockstream's electrs implementation. It's the backend mempool.space uses, and is also what powers blockstream.info.</li></ol>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="what-is-mempool-accelerator">
|
||||
<p><a href="https://mempool.space/accelerator" target="_blank">Mempool Accelerator™ </a> is a bitcoin transaction prioritisation service offered by Mempool Space K.K. in collaboration bitcoin mining partners - Using the accelerator you can get your transaction confirmed quickly and easily.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="how-does-mempool-accelerator-work">
|
||||
<p>When a user requests an acceleration the following actions are taken:</p>
|
||||
<ol>
|
||||
<li>A hold of an amount equal to the "Maximum Acceleration Bid" is placed on the users account balance.</li>
|
||||
<li>The transaction is monitored to detect ejection from the accelerator mempool. If at any time the transaction is replaced or otherwise ejected from the accelerator mempool the transaction acceleration will be cancelled.</li>
|
||||
<li>The acceleration request is sent to our mining partners, who apply a fee delta equal to the "Maximum Acceleration Bid" to the transaction.</li>
|
||||
<li>The transaction is included in the next block produced by our mining partners.</li>
|
||||
<li>Mempool Space K.K. calculates the acceleration fee retrospectively (taking into account regular and accelerated transactions) and debits the user balance appropriately. </li>
|
||||
<li>Any user balance still on hold is released for future accelerations.</li>
|
||||
</ol>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="join-accelerator-waitlist">
|
||||
<p>If you <a href="https://mempool.space/signup" target="_blank">sign up for an account</a> you will automatically join the Mempool Accelerator™ waitlist.</p>
|
||||
<p>You will get notified once you are granted access, although this will not happy until internal testing is completed and the service is rolled out. See <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="who-has-access-to-mempool-accelerator">here</a> for more details.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="who-has-access-to-mempool-accelerator">
|
||||
<p>Currently access to Mempool Accelerator™ is limited to internal testers.</p>
|
||||
<p>Anyone can <a href="https://mempool.space/signup" target="_blank">sign up</a> to join the waitlist, though their account will not be authorized immediately.
|
||||
<p>Users who have signed up for the waitlist will see a banner in their account accelerator overview tab stating "You are currently on the waitlist. You will get notified once you are granted access". No user action is required and access will be granted as soon as possible.</p>
|
||||
<p>Once the service is ready access will be rolled out to Mempool Enterprise® Sponsors and those on the <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="join-accelerator-waitlist">waitlist</a> for Mempool Accelerator™. </p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="account-requirement">
|
||||
<p>The blockspace market will change between the time when a user requests a transaction acceleration and the time that one of our mining partners finds a block. At the time of making an acceleration request it is not possible to know what bid will be sufficient to get into the next block mined by our mining partners, therefore it is not possible to know how much the user should pay.</p>
|
||||
<p>For this reason, at launch is is necessary to <a href="https://mempool.space/signup" target="_blank">sign up for an account</a> to use the Mempool Accelerator™. Having an account makes it possible to place a hold on user balance while awaiting an acceleration, and retrospectively charge customers the appropriate market rate, leaving the remaining balance for subsequent transaction accelerations. This makes the service very convenient for high volume users who can make many accelerations with a single deposit into their Mempool account.</p>
|
||||
<p>We know that some users would prefer to pay per acceleration without using an account, and we hope to develop this functionality in the future.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="mempool-accelerator-balance">
|
||||
<p>Mempool Accelerator™ will initially use an accounts model in which <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="who-has-access-to-mempool-accelerator">authorized users</a> can top-up their account balance by depositing bitcoin (on chain or lightning) or L-BTC, and draw down on their credit when making an acceleration. </p>
|
||||
<p>After <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-to-request-an-acceleration">requesting an acceleration</a> a hold will be placed on the user account with a value equal to the maximum acceleration bid. Once the acceleration is completed the appropriate market price will be computed retrospectively and only this amount will be debited from the user balance, with the remaining held balance being released.</p>
|
||||
<p>If the transaction is not accelerated the user will not be charged, and the hold will be released. If the transaction is ejected from the Mempool Accelerator™ mempool we reserve the right to charge you. </p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="how-to-request-an-acceleration">
|
||||
<p> A user of Mempool.Space can browse to an unconfirmed transaction and click the accelerate button in the ETA field. They will then be shown the Acceleration details, including a breakdown of the fees associated with the service.</p>
|
||||
<p>Provided the user has an <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="who-has-access-to-mempool-accelerator">authorized account</a> and a <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="who-has-access-to-mempool-accelerator">sufficient balance</a> they can click the Accelerate button.</p>
|
||||
<p> Optionally, prior to clicking the Accelerate button the user may adjust the maximum acceleration bid.</p>
|
||||
<p>Once a transaction acceleration has been requested it cannot be cancelled (<a [routerLink]="['/docs/faq' | relativeUrl]" fragment="cancelling-an-acceleration">read more</a>).</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="cancelling-an-acceleration">
|
||||
<p> Once an acceleration is requested it cannot be cancelled by the user. To give the user control over how much they may pay for the acceleration the user can select the maximum acceleration bid when submitting their acceleration request. If the required fee for acceleration exceeds the maximum bid set by the user the acceleration will be cancelled automatically.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="mempool-accelerator-improves-transparency">
|
||||
<p>Transaction prioritisation services have existed for much of bitcoin's history, and miners have always been able to choose which transactions are included in their blocks. By running this service with a commitment to transparency we hope to improve public access to blockspace market data, including out of band offers. This transparency is novel, current transaction prioritisation services are private and although transactions which were included as a result of out of band payments can be identified, the amount paid is not public data. Furthermore, these transactions can only be identified in retrospect, after a block has been mined wheres acceleration bids are shown public on Mempool Space when they are made.</p>
|
||||
<p>Mempool Accelerator publishes data on all accelerated transactions for public access. This information is available both on the mempool.space accelerator website and via an API. By making this information public other entities within the bitcoin ecosystem can better understand current and historic blockspace market dynamics. </p>
|
||||
<p>"Fee estimation" algorithms can only utilize data which they have access to, and historically "Fee Estimation" algorithms have been blind to transaction prioritisations where out of band payments are made to miners or mining pools.</p>
|
||||
<p>By making this data public Mempool Accelerator™ will ensure that "Fee Estimation" algorithms can take into account more transaction bid data (both in band and out of band).</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="will-mining-pools-underpay-miners">
|
||||
<p>If mining pools do not distribute out of band income to miners the incentives of each group are misaligned - mining pools earn more and miners earn less for each accelerated transaction.</p>
|
||||
<p> To help miners hold mining pools to account we developed the <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-block-audits-work">Mempool Block Audit</a> feature which highlights transactions which were likely included due to out of band payments (those which were included in blocks but which were not expected when sorting by effective fee rate). </p>
|
||||
<p> The Mempool Accelerator™ <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="mempool-accelerator-improves-transparency">improves transparency</a>, giving miners the data required to inform their decision as to which pool to mine with. For example the Mempool Accelerator fees paid to mining pools will be displayed on the block overview pages, making it easy for miners to determine whether this income is shared with miners.</p>
|
||||
</ng-template>
|
||||
|
||||
@@ -64,10 +64,9 @@ export class DocsComponent implements OnInit {
|
||||
}
|
||||
} else {
|
||||
this.activeTab = 3;
|
||||
this.seoService.setTitle($localize`:@@meta.title.docs.websocket:Electrum RPC`);
|
||||
this.seoService.setTitle($localize`:@@meta.title.docs.electrum:Electrum RPC`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.docs.electrumrpc:Documentation for our Electrum RPC interface: get instant, convenient, and reliable access to an Esplora instance.`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
@@ -302,3 +302,26 @@ export interface INode {
|
||||
funding_balance?: number;
|
||||
closing_balance?: number;
|
||||
}
|
||||
|
||||
export interface Acceleration {
|
||||
txid: string;
|
||||
status: 'requested' | 'accelerating' | 'mined' | 'completed' | 'failed';
|
||||
pools: number[];
|
||||
feePaid: number;
|
||||
added: number; // timestamp
|
||||
lastUpdated: number; // timestamp
|
||||
baseFee: number;
|
||||
vsizeFee: number;
|
||||
effectiveFee: number;
|
||||
effectiveVsize: number;
|
||||
feeDelta: number;
|
||||
blockHash: string;
|
||||
blockHeight: number;
|
||||
}
|
||||
|
||||
export interface AccelerationHistoryParams {
|
||||
timeframe?: string,
|
||||
status?: string,
|
||||
pool?: string,
|
||||
blockHash?: string,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user