Compare commits
134 Commits
v2.4.0-alp
...
v2.4.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
936964d273 | ||
|
|
4d274a3cec | ||
|
|
acd342259f | ||
|
|
67456c151f | ||
|
|
13ccf55cc8 | ||
|
|
73bffb5552 | ||
|
|
be8ee52af0 | ||
|
|
fbb16d6f22 | ||
|
|
96f8bf4a34 | ||
|
|
2f9a86524a | ||
|
|
e617e09ae3 | ||
|
|
6934aef60b | ||
|
|
8f4de39e7b | ||
|
|
fcb0c51e51 | ||
|
|
ec80eac6b9 | ||
|
|
84e600ac9f | ||
|
|
c64d95b0ec | ||
|
|
3e2ced2e8b | ||
|
|
6cc04feda8 | ||
|
|
0b50c17ed0 | ||
|
|
81b9153d2b | ||
|
|
e7c5307ca4 | ||
|
|
8fb377b4eb | ||
|
|
5642358937 | ||
|
|
00cd1386b5 | ||
|
|
da6c72e9b7 | ||
|
|
c318993a79 | ||
|
|
87c6e957f0 | ||
|
|
e133467ea1 | ||
|
|
a0429b243f | ||
|
|
21ae1fce2a | ||
|
|
53bc80e899 | ||
|
|
56dc337672 | ||
|
|
a04bafdb4c | ||
|
|
6ff473ab5d | ||
|
|
40bfc6bff3 | ||
|
|
c610cacee4 | ||
|
|
e41a08789a | ||
|
|
9d5bbf1f44 | ||
|
|
22268b8a33 | ||
|
|
0f58ce2322 | ||
|
|
1aad89ac97 | ||
|
|
e99a684354 | ||
|
|
5360f6dd77 | ||
|
|
c8d5708155 | ||
|
|
ebda00dc74 | ||
|
|
789092c76a | ||
|
|
967a2a4461 | ||
|
|
9288628ad7 | ||
|
|
0384ebb2ff | ||
|
|
869c40e835 | ||
|
|
579af85544 | ||
|
|
97f72c1faf | ||
|
|
262c3af33e | ||
|
|
dd7d9b66e5 | ||
|
|
f688da957c | ||
|
|
866ac3d5b8 | ||
|
|
63fce2a3ca | ||
|
|
33e0859847 | ||
|
|
b71922fabf | ||
|
|
ce0564a89c | ||
|
|
2a287b8d66 | ||
|
|
69713ae156 | ||
|
|
b930b9bf4f | ||
|
|
412f118d22 | ||
|
|
b60c2a9341 | ||
|
|
1efac916b7 | ||
|
|
3202629c44 | ||
|
|
3bc55d80ce | ||
|
|
89699f9b7e | ||
|
|
95dd436be5 | ||
|
|
efede07b5c | ||
|
|
a8123cddf7 | ||
|
|
7764cceb86 | ||
|
|
256dbc8c8e | ||
|
|
9ff006e61e | ||
|
|
9de6c716b7 | ||
|
|
538a1b1666 | ||
|
|
56e996c893 | ||
|
|
429b4f2bc6 | ||
|
|
3196c188f1 | ||
|
|
672833930d | ||
|
|
74ee35e273 | ||
|
|
8095a8a5f5 | ||
|
|
ed3a614fb7 | ||
|
|
cabfdcf49c | ||
|
|
69e1474c53 | ||
|
|
11f5056871 | ||
|
|
10ccad16e9 | ||
|
|
18c1be0bd0 | ||
|
|
fe32ef75a2 | ||
|
|
a6517ebdc5 | ||
|
|
83660e9cf3 | ||
|
|
f0a2ddf57b | ||
|
|
ddab579111 | ||
|
|
82471073c3 | ||
|
|
c9b98ed841 | ||
|
|
57cecee3af | ||
|
|
6cd8c448b4 | ||
|
|
3ffc4956f4 | ||
|
|
c2802253b7 | ||
|
|
1aac96a6f6 | ||
|
|
d4c9f6decb | ||
|
|
79dae84363 | ||
|
|
34576c0609 | ||
|
|
ec24549602 | ||
|
|
7f8834a2eb | ||
|
|
ee5cd1cd96 | ||
|
|
9ab3b3293a | ||
|
|
d860344be4 | ||
|
|
cefc927b06 | ||
|
|
72cc2e4df0 | ||
|
|
b4fd98f565 | ||
|
|
2ee1f197d1 | ||
|
|
e629173304 | ||
|
|
6210936ef4 | ||
|
|
de8a51fe9a | ||
|
|
260f883d02 | ||
|
|
e81dfbcc7f | ||
|
|
c7e9b47aa0 | ||
|
|
7f6ea58c74 | ||
|
|
5dcde1c702 | ||
|
|
4c90d8e811 | ||
|
|
dbce727695 | ||
|
|
39f33dded2 | ||
|
|
4b445b1191 | ||
|
|
a9515f8fc1 | ||
|
|
4ccd786fe9 | ||
|
|
72c3eea863 | ||
|
|
6b2b10960a | ||
|
|
f20cf266b6 | ||
|
|
82161d4edf | ||
|
|
85b17927d6 | ||
|
|
7e8e4b1e6c |
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,3 +3,6 @@ contact_links:
|
||||
- name: 🙋 Need help? Chat with us on Matrix
|
||||
url: https://matrix.to/#/#mempool.support:bitcoin.kyoto
|
||||
about: For support requests or general questions
|
||||
- name: 🌐 Want to help with translations? Use Transifex
|
||||
url: https://www.transifex.com/mempool/mempool
|
||||
about: All translations work is done on Transifex
|
||||
|
||||
6
.github/pull_request_template.md
vendored
Normal file
6
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<!--
|
||||
Please do not open pull requests for translations.
|
||||
|
||||
All translations work is done on Transifex:
|
||||
https://www.transifex.com/mempool/mempool
|
||||
-->
|
||||
24
.github/workflows/cypress.yml
vendored
24
.github/workflows/cypress.yml
vendored
@@ -1,10 +1,6 @@
|
||||
name: Cypress Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
cypress:
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -25,7 +21,7 @@ jobs:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: ${{ matrix.browser }} browser tests (Mempool)
|
||||
uses: cypress-io/github-action@v2
|
||||
uses: cypress-io/github-action@v4
|
||||
with:
|
||||
tag: ${{ github.event_name }}
|
||||
working-directory: frontend
|
||||
@@ -36,9 +32,9 @@ jobs:
|
||||
record: true
|
||||
parallel: true
|
||||
spec: |
|
||||
cypress/integration/mainnet/*.spec.ts
|
||||
cypress/integration/signet/*.spec.ts
|
||||
cypress/integration/testnet/*.spec.ts
|
||||
cypress/e2e/mainnet/*.spec.ts
|
||||
cypress/e2e/signet/*.spec.ts
|
||||
cypress/e2e/testnet/*.spec.ts
|
||||
group: Tests on ${{ matrix.browser }} (Mempool)
|
||||
browser: ${{ matrix.browser }}
|
||||
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
||||
@@ -49,7 +45,7 @@ jobs:
|
||||
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||
|
||||
- name: ${{ matrix.browser }} browser tests (Liquid)
|
||||
uses: cypress-io/github-action@v2
|
||||
uses: cypress-io/github-action@v4
|
||||
if: always()
|
||||
with:
|
||||
tag: ${{ github.event_name }}
|
||||
@@ -61,8 +57,8 @@ jobs:
|
||||
record: true
|
||||
parallel: true
|
||||
spec: |
|
||||
cypress/integration/liquid/liquid.spec.ts
|
||||
cypress/integration/liquidtestnet/liquidtestnet.spec.ts
|
||||
cypress/e2e/liquid/liquid.spec.ts
|
||||
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
|
||||
group: Tests on ${{ matrix.browser }} (Liquid)
|
||||
browser: ${{ matrix.browser }}
|
||||
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
||||
@@ -73,7 +69,7 @@ jobs:
|
||||
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||
|
||||
- name: ${{ matrix.browser }} browser tests (Bisq)
|
||||
uses: cypress-io/github-action@v2
|
||||
uses: cypress-io/github-action@v4
|
||||
if: always()
|
||||
with:
|
||||
tag: ${{ github.event_name }}
|
||||
@@ -84,7 +80,7 @@ jobs:
|
||||
wait-on-timeout: 120
|
||||
record: true
|
||||
parallel: true
|
||||
spec: cypress/integration/bisq/bisq.spec.ts
|
||||
spec: cypress/e2e/bisq/bisq.spec.ts
|
||||
group: Tests on ${{ matrix.browser }} (Bisq)
|
||||
browser: ${{ matrix.browser }}
|
||||
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
||||
|
||||
@@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
|
||||
|
||||
#### Build
|
||||
|
||||
_Node.js 16 and npm 7 are recommended._
|
||||
_Make sure to use Node.js 16.15 and npm 7._
|
||||
|
||||
Install dependencies with `npm` and build the backend:
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
"INDEXING_BLOCKS_AMOUNT": 11000,
|
||||
"PRICE_FEED_UPDATE_INTERVAL": 600,
|
||||
"USE_SECOND_NODE_FOR_MINFEE": false,
|
||||
"EXTERNAL_ASSETS": [
|
||||
"https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json"
|
||||
],
|
||||
"EXTERNAL_ASSETS": [],
|
||||
"EXTERNAL_MAX_RETRY": 1,
|
||||
"EXTERNAL_RETRY_INTERVAL": 0,
|
||||
"USER_AGENT": "mempool",
|
||||
"STDOUT_LOG_MIN_PRIORITY": "debug"
|
||||
},
|
||||
"CORE_RPC": {
|
||||
@@ -66,6 +67,7 @@
|
||||
},
|
||||
"SOCKS5PROXY": {
|
||||
"ENABLED": false,
|
||||
"USE_ONION": true,
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 9050,
|
||||
"USERNAME": "",
|
||||
@@ -74,5 +76,13 @@
|
||||
"PRICE_DATA_SERVER": {
|
||||
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
|
||||
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
|
||||
},
|
||||
"EXTERNAL_DATA_SERVER": {
|
||||
"MEMPOOL_API": "https://mempool.space/api/v1",
|
||||
"MEMPOOL_ONION": "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1",
|
||||
"LIQUID_API": "https://liquid.network/api/v1",
|
||||
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
|
||||
"BISQ_URL": "https://bisq.markets/api",
|
||||
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import config from '../../config';
|
||||
import * as fs from 'fs';
|
||||
import axios from 'axios';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
|
||||
import { Common } from '../common';
|
||||
import { BlockExtended } from '../../mempool.interfaces';
|
||||
import { StaticPool } from 'node-worker-threads-pool';
|
||||
import backendInfo from '../backend-info';
|
||||
import logger from '../../logger';
|
||||
|
||||
class Bisq {
|
||||
@@ -35,7 +39,13 @@ class Bisq {
|
||||
constructor() {}
|
||||
|
||||
startBisqService(): void {
|
||||
this.checkForBisqDataFolder();
|
||||
try {
|
||||
this.checkForBisqDataFolder();
|
||||
} catch (e) {
|
||||
logger.info('Retrying to start bisq service in 3 minutes');
|
||||
setTimeout(this.startBisqService.bind(this), 180000);
|
||||
return;
|
||||
}
|
||||
this.loadBisqDumpFile();
|
||||
setInterval(this.updatePrice.bind(this), 1000 * 60 * 60);
|
||||
this.updatePrice();
|
||||
@@ -90,7 +100,7 @@ class Bisq {
|
||||
private checkForBisqDataFolder() {
|
||||
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
|
||||
logger.warn(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
|
||||
return process.exit(1);
|
||||
throw new Error(`Cannot load BISQ ${Bisq.BLOCKS_JSON_FILE_PATH} file`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,12 +147,59 @@ class Bisq {
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
private async updatePrice() {
|
||||
type axiosOptions = {
|
||||
headers: {
|
||||
'User-Agent': string
|
||||
};
|
||||
timeout: number;
|
||||
httpAgent?: http.Agent;
|
||||
httpsAgent?: https.Agent;
|
||||
}
|
||||
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
|
||||
const BISQ_URL = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.EXTERNAL_DATA_SERVER.BISQ_ONION : config.EXTERNAL_DATA_SERVER.BISQ_URL;
|
||||
const isHTTP = (new URL(BISQ_URL).protocol.split(':')[0] === 'http') ? true : false;
|
||||
const axiosOptions: axiosOptions = {
|
||||
headers: {
|
||||
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||
},
|
||||
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
||||
};
|
||||
let retry = 0;
|
||||
|
||||
private updatePrice() {
|
||||
axios.get<BisqTrade[]>('https://bisq.markets/api/trades/?market=bsq_btc', { timeout: 10000 })
|
||||
.then((response) => {
|
||||
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
||||
try {
|
||||
if (config.SOCKS5PROXY.ENABLED) {
|
||||
const socksOptions: any = {
|
||||
agentOptions: {
|
||||
keepAlive: true,
|
||||
},
|
||||
hostname: config.SOCKS5PROXY.HOST,
|
||||
port: config.SOCKS5PROXY.PORT
|
||||
};
|
||||
|
||||
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
||||
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
||||
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
||||
} else {
|
||||
// Retry with different tor circuits https://stackoverflow.com/a/64960234
|
||||
socksOptions.username = `circuit${retry}`;
|
||||
}
|
||||
|
||||
// Handle proxy agent for onion addresses
|
||||
if (isHTTP) {
|
||||
axiosOptions.httpAgent = new SocksProxyAgent(socksOptions);
|
||||
} else {
|
||||
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
||||
}
|
||||
}
|
||||
|
||||
const data: AxiosResponse = await axios.get(`${BISQ_URL}/trades/?market=bsq_btc`, axiosOptions);
|
||||
if (data.statusText === 'error' || !data.data) {
|
||||
throw new Error(`Could not fetch data from Bisq market, Error: ${data.status}`);
|
||||
}
|
||||
const prices: number[] = [];
|
||||
response.data.forEach((trade) => {
|
||||
data.data.forEach((trade) => {
|
||||
prices.push(parseFloat(trade.price) * 100000000);
|
||||
});
|
||||
prices.sort((a, b) => a - b);
|
||||
@@ -150,9 +207,14 @@ class Bisq {
|
||||
if (this.priceUpdateCallbackFunction) {
|
||||
this.priceUpdateCallbackFunction(this.price);
|
||||
}
|
||||
}).catch((err) => {
|
||||
logger.err('Error updating Bisq market price: ' + err);
|
||||
});
|
||||
logger.debug('Successfully updated Bisq market price');
|
||||
break;
|
||||
} catch (e) {
|
||||
logger.err('Error updating Bisq market price: ' + (e instanceof Error ? e.message : e));
|
||||
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
||||
retry++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadBisqDumpFile(): Promise<void> {
|
||||
@@ -162,7 +224,7 @@ class Bisq {
|
||||
this.buildIndex();
|
||||
this.calculateStats();
|
||||
} catch (e) {
|
||||
logger.info('loadBisqDumpFile() error.' + (e instanceof Error ? e.message : e));
|
||||
logger.info('Cannot load bisq dump file because: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,13 @@ class Bisq {
|
||||
constructor() {}
|
||||
|
||||
startBisqService(): void {
|
||||
this.checkForBisqDataFolder();
|
||||
try {
|
||||
this.checkForBisqDataFolder();
|
||||
} catch (e) {
|
||||
logger.info('Retrying to start bisq service (markets) in 3 minutes');
|
||||
setTimeout(this.startBisqService.bind(this), 180000);
|
||||
return;
|
||||
}
|
||||
this.loadBisqDumpFile();
|
||||
this.startBisqDirectoryWatcher();
|
||||
}
|
||||
@@ -34,7 +40,7 @@ class Bisq {
|
||||
private checkForBisqDataFolder() {
|
||||
if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) {
|
||||
logger.err(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
|
||||
return process.exit(1);
|
||||
throw new Error(`Cannot load BISQ ${Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency} file`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import HashratesRepository from '../repositories/HashratesRepository';
|
||||
import indexer from '../indexer';
|
||||
import fiatConversion from './fiat-conversion';
|
||||
import RatesRepository from '../repositories/RatesRepository';
|
||||
import poolsParser from './pools-parser';
|
||||
|
||||
class Blocks {
|
||||
private blocks: BlockExtended[] = [];
|
||||
@@ -139,7 +140,11 @@ class Blocks {
|
||||
if (blockExtended.extras?.coinbaseTx !== undefined) {
|
||||
pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
|
||||
} else {
|
||||
pool = await poolsRepository.$getUnknownPool();
|
||||
if (config.DATABASE.ENABLED === true) {
|
||||
pool = await poolsRepository.$getUnknownPool();
|
||||
} else {
|
||||
pool = poolsParser.unknownPool;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pool) { // We should never have this situation in practise
|
||||
@@ -165,13 +170,22 @@ class Blocks {
|
||||
*/
|
||||
private async $findBlockMiner(txMinerInfo: TransactionMinerInfo | undefined): Promise<PoolTag> {
|
||||
if (txMinerInfo === undefined || txMinerInfo.vout.length < 1) {
|
||||
return await poolsRepository.$getUnknownPool();
|
||||
if (config.DATABASE.ENABLED === true) {
|
||||
return await poolsRepository.$getUnknownPool();
|
||||
} else {
|
||||
return poolsParser.unknownPool;
|
||||
}
|
||||
}
|
||||
|
||||
const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
|
||||
const address = txMinerInfo.vout[0].scriptpubkey_address;
|
||||
|
||||
const pools: PoolTag[] = await poolsRepository.$getPools();
|
||||
let pools: PoolTag[] = [];
|
||||
if (config.DATABASE.ENABLED === true) {
|
||||
pools = await poolsRepository.$getPools();
|
||||
} else {
|
||||
pools = poolsParser.miningPools;
|
||||
}
|
||||
for (let i = 0; i < pools.length; ++i) {
|
||||
if (address !== undefined) {
|
||||
const addresses: string[] = JSON.parse(pools[i].addresses);
|
||||
@@ -190,7 +204,11 @@ class Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
return await poolsRepository.$getUnknownPool();
|
||||
if (config.DATABASE.ENABLED === true) {
|
||||
return await poolsRepository.$getUnknownPool();
|
||||
} else {
|
||||
return poolsParser.unknownPool;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -343,7 +361,7 @@ class Blocks {
|
||||
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
||||
}
|
||||
}
|
||||
if (fiatConversion.ratesInitialized === true) {
|
||||
if (fiatConversion.ratesInitialized === true && config.DATABASE.ENABLED === true) {
|
||||
await RatesRepository.$saveRate(blockExtended.height, fiatConversion.getConversionRates());
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import { TransactionExtended } from '../mempool.interfaces';
|
||||
import { Common } from './common';
|
||||
|
||||
class DiskCache {
|
||||
private cacheSchemaVersion = 1;
|
||||
|
||||
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json';
|
||||
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
|
||||
private static CHUNK_FILES = 25;
|
||||
@@ -39,6 +41,7 @@ class DiskCache {
|
||||
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
|
||||
|
||||
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
|
||||
cacheSchemaVersion: this.cacheSchemaVersion,
|
||||
blocks: blocks.getBlocks(),
|
||||
mempool: {},
|
||||
mempoolArray: mempoolArray.splice(0, chunkSize),
|
||||
@@ -57,6 +60,13 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
|
||||
wipeCache() {
|
||||
fs.unlinkSync(DiskCache.FILE_NAME);
|
||||
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
|
||||
fs.unlinkSync(DiskCache.FILE_NAMES.replace('{number}', i.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
loadMempoolCache() {
|
||||
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
||||
return;
|
||||
@@ -67,6 +77,11 @@ class DiskCache {
|
||||
if (cacheData) {
|
||||
logger.info('Restoring mempool and blocks data from disk cache');
|
||||
data = JSON.parse(cacheData);
|
||||
if (data.cacheSchemaVersion === undefined || data.cacheSchemaVersion !== this.cacheSchemaVersion) {
|
||||
logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
|
||||
return this.wipeCache();
|
||||
}
|
||||
|
||||
if (data.mempoolArray) {
|
||||
for (const tx of data.mempoolArray) {
|
||||
data.mempool[tx.txid] = tx;
|
||||
@@ -88,14 +103,14 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug('Error parsing ' + fileName + '. Skipping.');
|
||||
logger.info('Error parsing ' + fileName + '. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
memPool.setMempool(data.mempool);
|
||||
blocks.setBlocks(data.blocks);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse mempoool and blocks cache. Skipping.');
|
||||
logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import logger from '../logger';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { IConversionRates } from '../mempool.interfaces';
|
||||
import config from '../config';
|
||||
@@ -25,9 +27,10 @@ class FiatConversion {
|
||||
}
|
||||
|
||||
public startService() {
|
||||
const fiatConversionUrl = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.PRICE_DATA_SERVER.TOR_URL : config.PRICE_DATA_SERVER.CLEARNET_URL;
|
||||
logger.info('Starting currency rates service');
|
||||
if (config.SOCKS5PROXY.ENABLED) {
|
||||
logger.info(`Currency rates service will be queried over the Tor network using ${config.PRICE_DATA_SERVER.TOR_URL}`);
|
||||
logger.info(`Currency rates service will be queried over the Tor network using ${fiatConversionUrl}`);
|
||||
} else {
|
||||
logger.info(`Currency rates service will be queried over clearnet using ${config.PRICE_DATA_SERVER.CLEARNET_URL}`);
|
||||
}
|
||||
@@ -40,49 +43,79 @@ class FiatConversion {
|
||||
}
|
||||
|
||||
private async updateCurrency(): Promise<void> {
|
||||
const headers = { 'User-Agent': `mempool/v${backendInfo.getBackendInfo().version}` };
|
||||
let fiatConversionUrl: string;
|
||||
let response: AxiosResponse;
|
||||
type axiosOptions = {
|
||||
headers: {
|
||||
'User-Agent': string
|
||||
};
|
||||
timeout: number;
|
||||
httpAgent?: http.Agent;
|
||||
httpsAgent?: https.Agent;
|
||||
}
|
||||
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
|
||||
const fiatConversionUrl = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.PRICE_DATA_SERVER.TOR_URL : config.PRICE_DATA_SERVER.CLEARNET_URL;
|
||||
const isHTTP = (new URL(fiatConversionUrl).protocol.split(':')[0] === 'http') ? true : false;
|
||||
const axiosOptions: axiosOptions = {
|
||||
headers: {
|
||||
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||
},
|
||||
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
||||
};
|
||||
|
||||
try {
|
||||
if (config.SOCKS5PROXY.ENABLED) {
|
||||
let socksOptions: any = {
|
||||
agentOptions: {
|
||||
keepAlive: true,
|
||||
},
|
||||
hostname: config.SOCKS5PROXY.HOST,
|
||||
port: config.SOCKS5PROXY.PORT
|
||||
};
|
||||
let retry = 0;
|
||||
|
||||
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
||||
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
||||
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
||||
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
||||
try {
|
||||
if (config.SOCKS5PROXY.ENABLED) {
|
||||
let socksOptions: any = {
|
||||
agentOptions: {
|
||||
keepAlive: true,
|
||||
},
|
||||
hostname: config.SOCKS5PROXY.HOST,
|
||||
port: config.SOCKS5PROXY.PORT
|
||||
};
|
||||
|
||||
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
||||
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
||||
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
||||
} else {
|
||||
// Retry with different tor circuits https://stackoverflow.com/a/64960234
|
||||
socksOptions.username = `circuit${retry}`;
|
||||
}
|
||||
|
||||
// Handle proxy agent for onion addresses
|
||||
if (isHTTP) {
|
||||
axiosOptions.httpAgent = new SocksProxyAgent(socksOptions);
|
||||
} else {
|
||||
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Querying currency rates service...');
|
||||
|
||||
const response: AxiosResponse = await axios.get(`${fiatConversionUrl}`, axiosOptions);
|
||||
|
||||
if (response.statusText === 'error' || !response.data) {
|
||||
throw new Error(`Could not fetch data from ${fiatConversionUrl}, Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const agent = new SocksProxyAgent(socksOptions);
|
||||
fiatConversionUrl = config.PRICE_DATA_SERVER.TOR_URL;
|
||||
logger.debug('Querying currency rates service...');
|
||||
response = await axios.get(fiatConversionUrl, { httpAgent: agent, headers: headers, timeout: 30000 });
|
||||
} else {
|
||||
fiatConversionUrl = config.PRICE_DATA_SERVER.CLEARNET_URL;
|
||||
logger.debug('Querying currency rates service...');
|
||||
response = await axios.get(fiatConversionUrl, { headers: headers, timeout: 10000 });
|
||||
}
|
||||
|
||||
for (const rate of response.data.data) {
|
||||
if (this.debasingFiatCurrencies.includes(rate.currencyCode) && rate.provider === 'Bisq-Aggregate') {
|
||||
this.conversionRates[rate.currencyCode] = Math.round(100 * rate.price) / 100;
|
||||
for (const rate of response.data.data) {
|
||||
if (this.debasingFiatCurrencies.includes(rate.currencyCode) && rate.provider === 'Bisq-Aggregate') {
|
||||
this.conversionRates[rate.currencyCode] = Math.round(100 * rate.price) / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.ratesInitialized = true;
|
||||
logger.debug(`USD Conversion Rate: ${this.conversionRates.USD}`);
|
||||
this.ratesInitialized = true;
|
||||
logger.debug(`USD Conversion Rate: ${this.conversionRates.USD}`);
|
||||
|
||||
if (this.ratesChangedCallback) {
|
||||
this.ratesChangedCallback(this.conversionRates);
|
||||
if (this.ratesChangedCallback) {
|
||||
this.ratesChangedCallback(this.conversionRates);
|
||||
}
|
||||
break;
|
||||
} catch (e) {
|
||||
logger.err('Error updating fiat conversion rates: ' + (e instanceof Error ? e.message : e));
|
||||
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
||||
retry++;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err('Error updating fiat conversion rates: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import logger from '../logger';
|
||||
import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta } from '../mempool.interfaces';
|
||||
import { Common } from './common';
|
||||
import config from '../config';
|
||||
|
||||
class MempoolBlocks {
|
||||
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -25,6 +26,10 @@ class MempoolBlocks {
|
||||
return this.mempoolBlocks;
|
||||
}
|
||||
|
||||
public getMempoolBlockDeltas(): MempoolBlockDelta[] {
|
||||
return this.mempoolBlockDeltas;
|
||||
}
|
||||
|
||||
public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }): void {
|
||||
const latestMempool = memPool;
|
||||
const memPoolArray: TransactionExtended[] = [];
|
||||
@@ -66,11 +71,15 @@ class MempoolBlocks {
|
||||
const time = end - start;
|
||||
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
|
||||
|
||||
this.mempoolBlocks = this.calculateMempoolBlocks(memPoolArray);
|
||||
const { blocks, deltas } = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
|
||||
this.mempoolBlocks = blocks;
|
||||
this.mempoolBlockDeltas = deltas;
|
||||
}
|
||||
|
||||
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
|
||||
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]):
|
||||
{ blocks: MempoolBlockWithTransactions[], deltas: MempoolBlockDelta[] } {
|
||||
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||
const mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
||||
let blockWeight = 0;
|
||||
let blockSize = 0;
|
||||
let transactions: TransactionExtended[] = [];
|
||||
@@ -90,7 +99,43 @@ class MempoolBlocks {
|
||||
if (transactions.length) {
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
|
||||
}
|
||||
return mempoolBlocks;
|
||||
// Calculate change from previous block states
|
||||
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
|
||||
let added: TransactionStripped[] = [];
|
||||
let removed: string[] = [];
|
||||
if (mempoolBlocks[i] && !prevBlocks[i]) {
|
||||
added = mempoolBlocks[i].transactions;
|
||||
} else if (!mempoolBlocks[i] && prevBlocks[i]) {
|
||||
removed = prevBlocks[i].transactions.map(tx => tx.txid);
|
||||
} else if (mempoolBlocks[i] && prevBlocks[i]) {
|
||||
const prevIds = {};
|
||||
const newIds = {};
|
||||
prevBlocks[i].transactions.forEach(tx => {
|
||||
prevIds[tx.txid] = true;
|
||||
});
|
||||
mempoolBlocks[i].transactions.forEach(tx => {
|
||||
newIds[tx.txid] = true;
|
||||
});
|
||||
prevBlocks[i].transactions.forEach(tx => {
|
||||
if (!newIds[tx.txid]) {
|
||||
removed.push(tx.txid);
|
||||
}
|
||||
});
|
||||
mempoolBlocks[i].transactions.forEach(tx => {
|
||||
if (!prevIds[tx.txid]) {
|
||||
added.push(tx);
|
||||
}
|
||||
});
|
||||
}
|
||||
mempoolBlockDeltas.push({
|
||||
added,
|
||||
removed
|
||||
});
|
||||
}
|
||||
return {
|
||||
blocks: mempoolBlocks,
|
||||
deltas: mempoolBlockDeltas
|
||||
};
|
||||
}
|
||||
|
||||
private dataToMempoolBlocks(transactions: TransactionExtended[],
|
||||
@@ -112,6 +157,7 @@ class MempoolBlocks {
|
||||
medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
|
||||
feeRange: Common.getFeesInRange(transactions, rangeLength),
|
||||
transactionIds: transactions.map((tx) => tx.txid),
|
||||
transactions: transactions.map((tx) => Common.stripTransaction(tx)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,14 @@ interface Pool {
|
||||
}
|
||||
|
||||
class PoolsParser {
|
||||
miningPools: any[] = [];
|
||||
unknownPool: any = {
|
||||
'name': "Unknown",
|
||||
'link': "https://learnmeabitcoin.com/technical/coinbase-transaction",
|
||||
'regexes': "[]",
|
||||
'addresses': "[]",
|
||||
'slug': 'unknown'
|
||||
};
|
||||
slugWarnFlag = false;
|
||||
|
||||
/**
|
||||
@@ -60,12 +68,18 @@ class PoolsParser {
|
||||
// Get existing pools from the db
|
||||
let existingPools;
|
||||
try {
|
||||
[existingPools] = await DB.query({ sql: 'SELECT * FROM pools;', timeout: 120000 });
|
||||
if (config.DATABASE.ENABLED === true) {
|
||||
[existingPools] = await DB.query({ sql: 'SELECT * FROM pools;', timeout: 120000 });
|
||||
} else {
|
||||
existingPools = [];
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err('Cannot get existing pools from the database, skipping pools.json import');
|
||||
return;
|
||||
}
|
||||
|
||||
this.miningPools = [];
|
||||
|
||||
// Finally, we generate the final consolidated pools data
|
||||
const finalPoolDataAdd: Pool[] = [];
|
||||
const finalPoolDataUpdate: Pool[] = [];
|
||||
@@ -97,24 +111,33 @@ class PoolsParser {
|
||||
logger.warn(`No slug found for '${poolNames[i]}', generating it => '${slug}'`);
|
||||
}
|
||||
|
||||
const poolObj = {
|
||||
'name': finalPoolName,
|
||||
'link': match[0].link,
|
||||
'regexes': allRegexes,
|
||||
'addresses': allAddresses,
|
||||
'slug': slug
|
||||
};
|
||||
|
||||
if (existingPools.find((pool) => pool.name === poolNames[i]) !== undefined) {
|
||||
finalPoolDataUpdate.push({
|
||||
'name': finalPoolName,
|
||||
'link': match[0].link,
|
||||
'regexes': allRegexes,
|
||||
'addresses': allAddresses,
|
||||
'slug': slug
|
||||
});
|
||||
finalPoolDataUpdate.push(poolObj);
|
||||
} else {
|
||||
logger.debug(`Add '${finalPoolName}' mining pool`);
|
||||
finalPoolDataAdd.push({
|
||||
'name': finalPoolName,
|
||||
'link': match[0].link,
|
||||
'regexes': allRegexes,
|
||||
'addresses': allAddresses,
|
||||
'slug': slug
|
||||
});
|
||||
finalPoolDataAdd.push(poolObj);
|
||||
}
|
||||
|
||||
this.miningPools.push({
|
||||
'name': finalPoolName,
|
||||
'link': match[0].link,
|
||||
'regexes': JSON.stringify(allRegexes),
|
||||
'addresses': JSON.stringify(allAddresses),
|
||||
'slug': slug
|
||||
});
|
||||
}
|
||||
|
||||
if (config.DATABASE.ENABLED === false) { // Don't run db operations
|
||||
logger.info('Mining pools.json import completed (no database)');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Update pools table now`);
|
||||
@@ -128,7 +151,7 @@ class PoolsParser {
|
||||
}
|
||||
queryAdd = queryAdd.slice(0, -1) + ';';
|
||||
|
||||
// Add new mining pools into the database
|
||||
// Updated existing mining pools in the database
|
||||
const updateQueries: string[] = [];
|
||||
for (let i = 0; i < finalPoolDataUpdate.length; ++i) {
|
||||
updateQueries.push(`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logger from '../logger';
|
||||
import * as WebSocket from 'ws';
|
||||
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock,
|
||||
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta,
|
||||
OptimizedStatistic, ILoadingIndicators, IConversionRates } from '../mempool.interfaces';
|
||||
import blocks from './blocks';
|
||||
import memPool from './mempool';
|
||||
@@ -13,6 +13,7 @@ import config from '../config';
|
||||
import transactionUtils from './transaction-utils';
|
||||
import rbfCache from './rbf-cache';
|
||||
import difficultyAdjustment from './difficulty-adjustment';
|
||||
import feeApi from './fee-api';
|
||||
|
||||
class WebsocketHandler {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
@@ -110,6 +111,20 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-mempool-block'] !== undefined) {
|
||||
if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) {
|
||||
const index = parsedMessage['track-mempool-block'];
|
||||
client['track-mempool-block'] = index;
|
||||
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
response['projected-block-transactions'] = {
|
||||
index: index,
|
||||
blockTransactions: mBlocksWithTransactions[index]?.transactions || [],
|
||||
};
|
||||
} else {
|
||||
client['track-mempool-block'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage.action === 'init') {
|
||||
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||
if (!_blocks) {
|
||||
@@ -199,6 +214,7 @@ class WebsocketHandler {
|
||||
'backendInfo': backendInfo.getBackendInfo(),
|
||||
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
|
||||
'da': difficultyAdjustment.getDifficultyAdjustment(),
|
||||
'fees': feeApi.getRecommendedFee(),
|
||||
...this.extraInitProperties
|
||||
};
|
||||
}
|
||||
@@ -231,11 +247,13 @@ class WebsocketHandler {
|
||||
|
||||
mempoolBlocks.updateMempoolBlocks(newMempool);
|
||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||
const mempoolInfo = memPool.getMempoolInfo();
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
const recommendedFees = feeApi.getRecommendedFee();
|
||||
|
||||
this.wss.clients.forEach(async (client: WebSocket) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
@@ -249,6 +267,7 @@ class WebsocketHandler {
|
||||
response['vBytesPerSecond'] = vBytesPerSecond;
|
||||
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
|
||||
response['da'] = da;
|
||||
response['fees'] = recommendedFees;
|
||||
}
|
||||
|
||||
if (client['want-mempool-blocks']) {
|
||||
@@ -366,6 +385,16 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-mempool-block'] >= 0) {
|
||||
const index = client['track-mempool-block'];
|
||||
if (mBlockDeltas[index]) {
|
||||
response['projected-block-transactions'] = {
|
||||
index: index,
|
||||
delta: mBlockDeltas[index],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(JSON.stringify(response));
|
||||
}
|
||||
@@ -378,6 +407,7 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
let mBlocks: undefined | MempoolBlock[];
|
||||
let mBlockDeltas: undefined | MempoolBlockDelta[];
|
||||
let matchRate = 0;
|
||||
const _memPool = memPool.getMempool();
|
||||
const _mempoolBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
@@ -394,12 +424,16 @@ class WebsocketHandler {
|
||||
matchRate = Math.round((matches.length / (txIds.length - 1)) * 100);
|
||||
mempoolBlocks.updateMempoolBlocks(_memPool);
|
||||
mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||
}
|
||||
|
||||
if (block.extras) {
|
||||
block.extras.matchRate = matchRate;
|
||||
}
|
||||
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
const fees = feeApi.getRecommendedFee();
|
||||
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
@@ -412,7 +446,8 @@ class WebsocketHandler {
|
||||
const response = {
|
||||
'block': block,
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'da': difficultyAdjustment.getDifficultyAdjustment(),
|
||||
'da': da,
|
||||
'fees': fees,
|
||||
};
|
||||
|
||||
if (mBlocks && client['want-mempool-blocks']) {
|
||||
@@ -487,6 +522,16 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-mempool-block'] >= 0) {
|
||||
const index = client['track-mempool-block'];
|
||||
if (mBlockDeltas && mBlockDeltas[index]) {
|
||||
response['projected-block-transactions'] = {
|
||||
index: index,
|
||||
delta: mBlockDeltas[index],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
client.send(JSON.stringify(response));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ interface IConfig {
|
||||
PRICE_FEED_UPDATE_INTERVAL: number;
|
||||
USE_SECOND_NODE_FOR_MINFEE: boolean;
|
||||
EXTERNAL_ASSETS: string[];
|
||||
EXTERNAL_MAX_RETRY: number;
|
||||
EXTERNAL_RETRY_INTERVAL: number;
|
||||
USER_AGENT: string;
|
||||
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
|
||||
};
|
||||
ESPLORA: {
|
||||
@@ -66,6 +69,7 @@ interface IConfig {
|
||||
};
|
||||
SOCKS5PROXY: {
|
||||
ENABLED: boolean;
|
||||
USE_ONION: boolean;
|
||||
HOST: string;
|
||||
PORT: number;
|
||||
USERNAME: string;
|
||||
@@ -75,6 +79,14 @@ interface IConfig {
|
||||
TOR_URL: string;
|
||||
CLEARNET_URL: string;
|
||||
};
|
||||
EXTERNAL_DATA_SERVER: {
|
||||
MEMPOOL_API: string;
|
||||
MEMPOOL_ONION: string;
|
||||
LIQUID_API: string;
|
||||
LIQUID_ONION: string;
|
||||
BISQ_URL: string;
|
||||
BISQ_ONION: string;
|
||||
};
|
||||
}
|
||||
|
||||
const defaults: IConfig = {
|
||||
@@ -94,9 +106,10 @@ const defaults: IConfig = {
|
||||
'INDEXING_BLOCKS_AMOUNT': 11000, // 0 = disable indexing, -1 = index all blocks
|
||||
'PRICE_FEED_UPDATE_INTERVAL': 600,
|
||||
'USE_SECOND_NODE_FOR_MINFEE': false,
|
||||
'EXTERNAL_ASSETS': [
|
||||
'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json'
|
||||
],
|
||||
'EXTERNAL_ASSETS': [],
|
||||
'EXTERNAL_MAX_RETRY': 1,
|
||||
'EXTERNAL_RETRY_INTERVAL': 0,
|
||||
'USER_AGENT': 'mempool',
|
||||
'STDOUT_LOG_MIN_PRIORITY': 'debug',
|
||||
},
|
||||
'ESPLORA': {
|
||||
@@ -145,6 +158,7 @@ const defaults: IConfig = {
|
||||
},
|
||||
'SOCKS5PROXY': {
|
||||
'ENABLED': false,
|
||||
'USE_ONION': true,
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': 9050,
|
||||
'USERNAME': '',
|
||||
@@ -153,6 +167,14 @@ const defaults: IConfig = {
|
||||
"PRICE_DATA_SERVER": {
|
||||
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
|
||||
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
|
||||
},
|
||||
"EXTERNAL_DATA_SERVER": {
|
||||
'MEMPOOL_API': 'https://mempool.space/api/v1',
|
||||
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
|
||||
'LIQUID_API': 'https://liquid.network/api/v1',
|
||||
'LIQUID_ONION': 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1',
|
||||
'BISQ_URL': 'https://bisq.markets/api',
|
||||
'BISQ_ONION': 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -168,6 +190,7 @@ class Config implements IConfig {
|
||||
BISQ: IConfig['BISQ'];
|
||||
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
|
||||
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
||||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||
|
||||
constructor() {
|
||||
const configs = this.merge(configFile, defaults);
|
||||
@@ -182,6 +205,7 @@ class Config implements IConfig {
|
||||
this.BISQ = configs.BISQ;
|
||||
this.SOCKS5PROXY = configs.SOCKS5PROXY;
|
||||
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
||||
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||
}
|
||||
|
||||
merge = (...objects: object[]): IConfig => {
|
||||
|
||||
@@ -22,12 +22,20 @@ import { PoolOptions } from 'mysql2/typings/mysql';
|
||||
timezone: '+00:00',
|
||||
};
|
||||
|
||||
private checkDBFlag() {
|
||||
if (config.DATABASE.ENABLED === false) {
|
||||
logger.err('Trying to use DB feature but config.DATABASE.ENABLED is set to false, please open an issue');
|
||||
}
|
||||
}
|
||||
|
||||
public async query(query, params?) {
|
||||
this.checkDBFlag();
|
||||
const pool = await this.getPool();
|
||||
return pool.query(query, params);
|
||||
}
|
||||
|
||||
public async checkDbConnection() {
|
||||
this.checkDBFlag();
|
||||
try {
|
||||
await this.query('SELECT ?', [1]);
|
||||
logger.info('Database connection established.');
|
||||
|
||||
@@ -67,7 +67,7 @@ class Server {
|
||||
}
|
||||
|
||||
async startServer(worker = false) {
|
||||
logger.debug(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
|
||||
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
|
||||
|
||||
this.app
|
||||
.use((req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -205,7 +205,7 @@ class Server {
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', routes.$postTransactionForm)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get('https://mempool.space/api/v1/donations', { responseType: 'stream', timeout: 10000 });
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
@@ -213,7 +213,7 @@ class Server {
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get('https://mempool.space/api/v1/donations/images/' + req.params.id, {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
|
||||
responseType: 'stream', timeout: 10000
|
||||
});
|
||||
response.data.pipe(res);
|
||||
@@ -223,7 +223,7 @@ class Server {
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get('https://mempool.space/api/v1/contributors', { responseType: 'stream', timeout: 10000 });
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
@@ -231,7 +231,7 @@ class Server {
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get('https://mempool.space/api/v1/contributors/images/' + req.params.id, {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
|
||||
responseType: 'stream', timeout: 10000
|
||||
});
|
||||
response.data.pipe(res);
|
||||
@@ -241,7 +241,7 @@ class Server {
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get('https://mempool.space/api/v1/translators', { responseType: 'stream', timeout: 10000 });
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
@@ -249,7 +249,7 @@ class Server {
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get('https://mempool.space/api/v1/translators/images/' + req.params.id, {
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
|
||||
responseType: 'stream', timeout: 10000
|
||||
});
|
||||
response.data.pipe(res);
|
||||
|
||||
@@ -13,7 +13,9 @@ class Indexer {
|
||||
}
|
||||
|
||||
public reindex() {
|
||||
this.runIndexer = true;
|
||||
if (Common.indexingEnabled()) {
|
||||
this.runIndexer = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async $run() {
|
||||
|
||||
@@ -33,6 +33,12 @@ export interface MempoolBlock {
|
||||
|
||||
export interface MempoolBlockWithTransactions extends MempoolBlock {
|
||||
transactionIds: string[];
|
||||
transactions: TransactionStripped[];
|
||||
}
|
||||
|
||||
export interface MempoolBlockDelta {
|
||||
added: TransactionStripped[];
|
||||
removed: string[];
|
||||
}
|
||||
|
||||
interface VinStrippedToScriptsig {
|
||||
|
||||
@@ -307,6 +307,7 @@ class BlocksRepository {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`SELECT
|
||||
height,
|
||||
hash,
|
||||
hash as id,
|
||||
UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
|
||||
size,
|
||||
|
||||
@@ -990,7 +990,7 @@ class Routes {
|
||||
|
||||
public async $getAllFeaturedLiquidAssets(req: Request, res: Response) {
|
||||
try {
|
||||
const response = await axios.get('https://liquid.network/api/v1/assets/featured', { responseType: 'stream', timeout: 10000 });
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/featured`, { responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
@@ -999,7 +999,7 @@ class Routes {
|
||||
|
||||
public async $getAssetGroup(req: Request, res: Response) {
|
||||
try {
|
||||
const response = await axios.get('https://liquid.network/api/v1/assets/group/' + parseInt(req.params.id, 10),
|
||||
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/group/${parseInt(req.params.id, 10)}`,
|
||||
{ responseType: 'stream', timeout: 10000 });
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import * as fs from 'fs';
|
||||
import config from './config';
|
||||
import backendInfo from './api/backend-info';
|
||||
import logger from './logger';
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||
|
||||
@@ -42,6 +43,9 @@ class SyncAssets {
|
||||
|
||||
logger.info(`Downloading external asset ${fileName} over the Tor network...`);
|
||||
return axios.get(url, {
|
||||
headers: {
|
||||
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||
},
|
||||
httpAgent: agent,
|
||||
httpsAgent: agent,
|
||||
responseType: 'stream',
|
||||
@@ -57,6 +61,9 @@ class SyncAssets {
|
||||
} else {
|
||||
logger.info(`Downloading external asset ${fileName} over clearnet...`);
|
||||
return axios.get(url, {
|
||||
headers: {
|
||||
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||
},
|
||||
responseType: 'stream',
|
||||
timeout: 30000
|
||||
}).then(function (response) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import axios from 'axios';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import poolsParser from '../api/pools-parser';
|
||||
import config from '../config';
|
||||
import DB from '../database';
|
||||
import backendInfo from '../api/backend-info';
|
||||
import logger from '../logger';
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||
import * as https from 'https';
|
||||
@@ -11,12 +12,13 @@ import * as https from 'https';
|
||||
*/
|
||||
class PoolsUpdater {
|
||||
lastRun: number = 0;
|
||||
currentSha: any = undefined;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public async updatePoolsJson() {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || config.DATABASE.ENABLED === false) {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,14 +40,17 @@ class PoolsUpdater {
|
||||
}
|
||||
|
||||
try {
|
||||
const dbSha = await this.getShaFromDb();
|
||||
const githubSha = await this.fetchPoolsSha(); // Fetch pools.json sha from github
|
||||
if (githubSha === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Pools.json sha | Current: ${dbSha} | Github: ${githubSha}`);
|
||||
if (dbSha !== undefined && dbSha === githubSha) {
|
||||
if (config.DATABASE.ENABLED === true) {
|
||||
this.currentSha = await this.getShaFromDb();
|
||||
}
|
||||
|
||||
logger.debug(`Pools.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
|
||||
if (this.currentSha !== undefined && this.currentSha === githubSha) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,12 +73,14 @@ class PoolsUpdater {
|
||||
* Fetch our latest pools.json sha from the db
|
||||
*/
|
||||
private async updateDBSha(githubSha: string) {
|
||||
try {
|
||||
await DB.query('DELETE FROM state where name="pools_json_sha"');
|
||||
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
|
||||
} catch (e) {
|
||||
logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
return undefined;
|
||||
this.currentSha = githubSha;
|
||||
if (config.DATABASE.ENABLED === true) {
|
||||
try {
|
||||
await DB.query('DELETE FROM state where name="pools_json_sha"');
|
||||
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
|
||||
} catch (e) {
|
||||
logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,33 +120,45 @@ class PoolsUpdater {
|
||||
*/
|
||||
private async query(path): Promise<object | undefined> {
|
||||
type axiosOptions = {
|
||||
headers: {
|
||||
'User-Agent': string
|
||||
};
|
||||
timeout: number;
|
||||
httpsAgent?: https.Agent;
|
||||
}
|
||||
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
|
||||
const axiosOptions: axiosOptions = {};
|
||||
const axiosOptions: axiosOptions = {
|
||||
headers: {
|
||||
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||
},
|
||||
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
||||
};
|
||||
let retry = 0;
|
||||
|
||||
if (config.SOCKS5PROXY.ENABLED) {
|
||||
const socksOptions: any = {
|
||||
agentOptions: {
|
||||
keepAlive: true,
|
||||
},
|
||||
hostname: config.SOCKS5PROXY.HOST,
|
||||
port: config.SOCKS5PROXY.PORT
|
||||
};
|
||||
|
||||
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
||||
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
||||
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
||||
}
|
||||
|
||||
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
||||
}
|
||||
|
||||
while(retry < 5) {
|
||||
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
||||
try {
|
||||
const data = await axios.get(path, axiosOptions);
|
||||
if (data.statusText !== 'OK' || !data.data) {
|
||||
if (config.SOCKS5PROXY.ENABLED) {
|
||||
const socksOptions: any = {
|
||||
agentOptions: {
|
||||
keepAlive: true,
|
||||
},
|
||||
hostname: config.SOCKS5PROXY.HOST,
|
||||
port: config.SOCKS5PROXY.PORT
|
||||
};
|
||||
|
||||
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
||||
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
||||
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
||||
} else {
|
||||
// Retry with different tor circuits https://stackoverflow.com/a/64960234
|
||||
socksOptions.username = `circuit${retry}`;
|
||||
}
|
||||
|
||||
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
||||
}
|
||||
|
||||
const data: AxiosResponse = await axios.get(path, axiosOptions);
|
||||
if (data.statusText === 'error' || !data.data) {
|
||||
throw new Error(`Could not fetch data from Github, Error: ${data.status}`);
|
||||
}
|
||||
return data.data;
|
||||
@@ -147,7 +166,7 @@ class PoolsUpdater {
|
||||
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
retry++;
|
||||
}
|
||||
await setDelay();
|
||||
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
3
contributors/mononaut.txt
Normal file
3
contributors/mononaut.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 May 31, 2022.
|
||||
|
||||
Signed: mononaut
|
||||
@@ -15,6 +15,9 @@
|
||||
"PRICE_FEED_UPDATE_INTERVAL": __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__,
|
||||
"USE_SECOND_NODE_FOR_MINFEE": __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__,
|
||||
"EXTERNAL_ASSETS": __MEMPOOL_EXTERNAL_ASSETS__,
|
||||
"EXTERNAL_MAX_RETRY": __MEMPOOL_EXTERNAL_MAX_RETRY__,
|
||||
"EXTERNAL_RETRY_INTERVAL": __MEMPOOL_EXTERNAL_RETRY_INTERVAL__,
|
||||
"USER_AGENT": "__MEMPOOL_USER_AGENT__",
|
||||
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
|
||||
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__
|
||||
},
|
||||
@@ -64,6 +67,7 @@
|
||||
},
|
||||
"SOCKS5PROXY": {
|
||||
"ENABLED": __SOCKS5PROXY_ENABLED__,
|
||||
"USE_ONION": __SOCKS5PROXY_USE_ONION__,
|
||||
"HOST": "__SOCKS5PROXY_HOST__",
|
||||
"PORT": "__SOCKS5PROXY_PORT__",
|
||||
"USERNAME": "__SOCKS5PROXY_USERNAME__",
|
||||
@@ -72,5 +76,13 @@
|
||||
"PRICE_DATA_SERVER": {
|
||||
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
|
||||
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
|
||||
},
|
||||
"EXTERNAL_DATA_SERVER": {
|
||||
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
|
||||
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
|
||||
"LIQUID_API": "__EXTERNAL_DATA_SERVER_LIQUID_API__",
|
||||
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
|
||||
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
|
||||
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ __MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8}
|
||||
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=11000}
|
||||
__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600}
|
||||
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
|
||||
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[\"https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json\"]}
|
||||
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[]}
|
||||
__MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
|
||||
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
|
||||
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
|
||||
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
|
||||
|
||||
# CORE_RPC
|
||||
@@ -65,6 +68,7 @@ __BISQ_DATA_PATH__=${BISQ_DATA_PATH:=/bisq/statsnode-data/btc_mainnet/db}
|
||||
|
||||
# SOCKS5PROXY
|
||||
__SOCKS5PROXY_ENABLED__=${SOCKS5PROXY_ENABLED:=false}
|
||||
__SOCKS5PROXY_USE_ONION__=${SOCKS5PROXY_USE_ONION:=true}
|
||||
__SOCKS5PROXY_HOST__=${SOCKS5PROXY_HOST:=localhost}
|
||||
__SOCKS5PROXY_PORT__=${SOCKS5PROXY_PORT:=9050}
|
||||
__SOCKS5PROXY_USERNAME__=${SOCKS5PROXY_USERNAME:=""}
|
||||
@@ -74,6 +78,14 @@ __SOCKS5PROXY_PASSWORD__=${SOCKS5PROXY_PASSWORD:=""}
|
||||
__PRICE_DATA_SERVER_TOR_URL__=${PRICE_DATA_SERVER_TOR_URL:=http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices}
|
||||
__PRICE_DATA_SERVER_CLEARNET_URL__=${PRICE_DATA_SERVER_CLEARNET_URL:=https://price.bisq.wiz.biz/getAllMarketPrices}
|
||||
|
||||
# EXTERNAL_DATA_SERVER
|
||||
__EXTERNAL_DATA_SERVER_MEMPOOL_API__=${EXTERNAL_DATA_SERVER_MEMPOOL_API:=https://mempool.space/api/v1}
|
||||
__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__=${EXTERNAL_DATA_SERVER_MEMPOOL_ONION:=http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1}
|
||||
__EXTERNAL_DATA_SERVER_LIQUID_API__=${EXTERNAL_DATA_SERVER_LIQUID_API:=https://liquid.network/api/v1}
|
||||
__EXTERNAL_DATA_SERVER_LIQUID_ONION__=${EXTERNAL_DATA_SERVER_LIQUID_ONION:=http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1}
|
||||
__EXTERNAL_DATA_SERVER_BISQ_URL__=${EXTERNAL_DATA_SERVER_BISQ_URL:=https://bisq.markets/api}
|
||||
__EXTERNAL_DATA_SERVER_BISQ_ONION__=${EXTERNAL_DATA_SERVER_BISQ_ONION:=http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api}
|
||||
|
||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||
|
||||
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
|
||||
@@ -92,6 +104,9 @@ sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT_
|
||||
sed -i "s/__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__/${__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__}/g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__/${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}/g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
|
||||
|
||||
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
|
||||
@@ -132,6 +147,7 @@ sed -i "s/__BISQ_ENABLED__/${__BISQ_ENABLED__}/g" mempool-config.json
|
||||
sed -i "s!__BISQ_DATA_PATH__!${__BISQ_DATA_PATH__}!g" mempool-config.json
|
||||
|
||||
sed -i "s/__SOCKS5PROXY_ENABLED__/${__SOCKS5PROXY_ENABLED__}/g" mempool-config.json
|
||||
sed -i "s/__SOCKS5PROXY_USE_ONION__/${__SOCKS5PROXY_USE_ONION__}/g" mempool-config.json
|
||||
sed -i "s/__SOCKS5PROXY_HOST__/${__SOCKS5PROXY_HOST__}/g" mempool-config.json
|
||||
sed -i "s/__SOCKS5PROXY_PORT__/${__SOCKS5PROXY_PORT__}/g" mempool-config.json
|
||||
sed -i "s/__SOCKS5PROXY_USERNAME__/${__SOCKS5PROXY_USERNAME__}/g" mempool-config.json
|
||||
@@ -140,4 +156,11 @@ sed -i "s/__SOCKS5PROXY_PASSWORD__/${__SOCKS5PROXY_PASSWORD__}/g" mempool-config
|
||||
sed -i "s!__PRICE_DATA_SERVER_TOR_URL__!${__PRICE_DATA_SERVER_TOR_URL__}!g" mempool-config.json
|
||||
sed -i "s!__PRICE_DATA_SERVER_CLEARNET_URL__!${__PRICE_DATA_SERVER_CLEARNET_URL__}!g" mempool-config.json
|
||||
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_API__!${__EXTERNAL_DATA_SERVER_MEMPOOL_API__}!g" mempool-config.json
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__!${__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__}!g" mempool-config.json
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_API__!${__EXTERNAL_DATA_SERVER_LIQUID_API__}!g" mempool-config.json
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_ONION__!${__EXTERNAL_DATA_SERVER_LIQUID_ONION__}!g" mempool-config.json
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_API__!${__EXTERNAL_DATA_SERVER_BISQ_API__}!g" mempool-config.json
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_ONION__!${__EXTERNAL_DATA_SERVER_BISQ_ONION__}!g" mempool-config.json
|
||||
|
||||
node /backend/dist/index.js
|
||||
|
||||
@@ -34,7 +34,7 @@ $ npm run config:defaults:bisq
|
||||
|
||||
### 3. Run the Frontend
|
||||
|
||||
_Node.js 16 and npm 7 are recommended._
|
||||
_Make sure to use Node.js 16.15 and npm 7._
|
||||
|
||||
Install project dependencies and run the frontend server:
|
||||
|
||||
|
||||
23
frontend/cypress.config.ts
Normal file
23
frontend/cypress.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
projectId: 'ry4br7',
|
||||
videosFolder: 'cypress/videos',
|
||||
screenshotsFolder: 'cypress/screenshots',
|
||||
fixturesFolder: 'cypress/fixtures',
|
||||
video: false,
|
||||
retries: {
|
||||
runMode: 3,
|
||||
openMode: 0,
|
||||
},
|
||||
chromeWebSecurity: false,
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
return require('./cypress/plugins/index.js')(on, config)
|
||||
},
|
||||
baseUrl: 'http://localhost:4200',
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
},
|
||||
})
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"projectId": "ry4br7",
|
||||
"integrationFolder": "cypress/integration",
|
||||
"supportFile": "cypress/support/index.ts",
|
||||
"videosFolder": "cypress/videos",
|
||||
"screenshotsFolder": "cypress/screenshots",
|
||||
"pluginsFile": "cypress/plugins/index.js",
|
||||
"fixturesFolder": "cypress/fixtures",
|
||||
"baseUrl": "http://localhost:4200",
|
||||
"video": false,
|
||||
"retries": {
|
||||
"runMode": 3,
|
||||
"openMode": 0
|
||||
},
|
||||
"chromeWebSecurity": false
|
||||
}
|
||||
@@ -35,13 +35,14 @@ describe('Bisq', () => {
|
||||
"Proposal", "Reimbursement request", "Transfer BSQ", "Unlock", "Vote reveal"
|
||||
];
|
||||
filters.forEach((filter) => {
|
||||
it(`filters the transaction screen by ${filter}`, () => {
|
||||
it.only(`filters the transaction screen by ${filter}`, () => {
|
||||
cy.visit(`${basePath}/transactions`);
|
||||
cy.wait('@txs');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('#filter').click();
|
||||
cy.contains(filter).find('input').click();
|
||||
//TODO: change this waiter
|
||||
cy.wait(1000);
|
||||
cy.wait('@txs');
|
||||
cy.wait(500);
|
||||
cy.get('td:nth-of-type(2)').each(($td) => {
|
||||
expect($td.text().trim()).to.eq(filter);
|
||||
});
|
||||
@@ -56,7 +57,7 @@ describe('Bisq', () => {
|
||||
filters.forEach((filter) => {
|
||||
cy.contains(filter).find('input').click();
|
||||
//TODO: change this waiter
|
||||
cy.wait(1000);
|
||||
cy.wait(1500);
|
||||
});
|
||||
cy.get('td:nth-of-type(2)').each(($td) => {
|
||||
const regex = new RegExp(`${filters.join('|')}`, 'g');
|
||||
@@ -124,7 +124,7 @@ describe('Liquid', () => {
|
||||
cy.visit(`${basePath}/assets`);
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
|
||||
cy.get('ngb-typeahead-window').should('have.length', 1);
|
||||
cy.get('ngb-typeahead-window', { timeout: 30000 }).should('have.length', 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('Liquid', () => {
|
||||
cy.visit(`${basePath}/assets`);
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('.container-xl input').click().type('Liquid AUD').then(() => {
|
||||
cy.get('ngb-typeahead-window:nth-of-type(1) button').click();
|
||||
cy.get('ngb-typeahead-window:nth-of-type(1) button', { timeout: 30000 }).click();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -189,7 +189,7 @@ describe('Mainnet', () => {
|
||||
cy.get('[data-cy="tx-2"] .table-tx-vin .highlight').invoke('text').should('contain', `${address}`);
|
||||
});
|
||||
|
||||
it.only('highlights both input and output addresses in the same transaction', () => {
|
||||
it('highlights both input and output addresses in the same transaction', () => {
|
||||
const address = 'bc1q03u63r6hm7a3v6em58zdqtp446w2pw30nm63mv';
|
||||
cy.visit(`/address/${address}`);
|
||||
cy.waitForSkeletonGone();
|
||||
@@ -241,7 +241,7 @@ describe('Mainnet', () => {
|
||||
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
|
||||
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||
cy.document().left();
|
||||
cy.get('.title-block h1').invoke('text').should('equal', 'Next block');
|
||||
cy.get('.title-block h1').invoke('text').should('equal', 'Next Block');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,98 +50,98 @@ import { mockWebSocket } from './websocket';
|
||||
|
||||
/* global Cypress */
|
||||
const codes = {
|
||||
ArrowLeft: 37,
|
||||
ArrowUp: 38,
|
||||
ArrowRight: 39,
|
||||
ArrowDown: 40
|
||||
ArrowLeft: 37,
|
||||
ArrowUp: 38,
|
||||
ArrowRight: 39,
|
||||
ArrowDown: 40
|
||||
}
|
||||
|
||||
Cypress.Commands.add('waitForSkeletonGone', () => {
|
||||
cy.waitUntil(() => {
|
||||
return Cypress.$('.skeleton-loader').length === 0;
|
||||
}, { verbose: true, description: "waitForSkeletonGone", errorMsg: "skeleton loaders never went away", timeout: 15000, interval: 50});
|
||||
cy.waitUntil(() => {
|
||||
return Cypress.$('.skeleton-loader').length === 0;
|
||||
}, { verbose: true, description: "waitForSkeletonGone", errorMsg: "skeleton loaders never went away", timeout: 15000, interval: 50 });
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
"waitForPageIdle",
|
||||
() => {
|
||||
console.warn("Waiting for page idle state");
|
||||
const pageIdleDetector = new PageIdleDetector();
|
||||
pageIdleDetector.WaitForPageToBeIdle();
|
||||
}
|
||||
"waitForPageIdle",
|
||||
() => {
|
||||
console.warn("Waiting for page idle state");
|
||||
const pageIdleDetector = new PageIdleDetector();
|
||||
pageIdleDetector.WaitForPageToBeIdle();
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add('mockMempoolSocket', () => {
|
||||
mockWebSocket();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('changeNetwork', (network: "testnet"|"signet"|"liquid"|"bisq"|"mainnet" ) => {
|
||||
cy.get('.dropdown-toggle').click().then(() => {
|
||||
cy.get(`.${network}`).click().then(() => {
|
||||
cy.waitForPageIdle();
|
||||
if(network !== 'bisq'){
|
||||
cy.waitForSkeletonGone();
|
||||
}
|
||||
});
|
||||
Cypress.Commands.add('changeNetwork', (network: "testnet" | "signet" | "liquid" | "bisq" | "mainnet") => {
|
||||
cy.get('.dropdown-toggle').click().then(() => {
|
||||
cy.get(`a.${network}`).click().then(() => {
|
||||
cy.waitForPageIdle();
|
||||
if (network !== 'bisq') {
|
||||
cy.waitForSkeletonGone();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/bahmutov/cypress-arrows/blob/8f0303842a343550fbeaf01528d01d1ff213b70c/src/index.js
|
||||
function keydownCommand ($el, key) {
|
||||
const message = `sending the "${key}" keydown event`
|
||||
const log = Cypress.log({
|
||||
name: `keydown: ${key}`,
|
||||
message: message,
|
||||
consoleProps: function () {
|
||||
return {
|
||||
Subject: $el
|
||||
}
|
||||
function keydownCommand($el, key) {
|
||||
const message = `sending the "${key}" keydown event`
|
||||
const log = Cypress.log({
|
||||
name: `keydown: ${key}`,
|
||||
message: message,
|
||||
consoleProps: function () {
|
||||
return {
|
||||
Subject: $el
|
||||
}
|
||||
})
|
||||
|
||||
const e = $el.createEvent('KeyboardEvent')
|
||||
|
||||
Object.defineProperty(e, 'key', {
|
||||
get: function () {
|
||||
return key
|
||||
}
|
||||
})
|
||||
|
||||
Object.defineProperty(e, 'keyCode', {
|
||||
get: function () {
|
||||
return this.keyCodeVal
|
||||
}
|
||||
})
|
||||
Object.defineProperty(e, 'which', {
|
||||
get: function () {
|
||||
return this.keyCodeVal
|
||||
}
|
||||
})
|
||||
var metaKey = false
|
||||
|
||||
Object.defineProperty(e, 'metaKey', {
|
||||
get: function () {
|
||||
return metaKey
|
||||
}
|
||||
})
|
||||
|
||||
Object.defineProperty(e, 'shiftKey', {
|
||||
get: function () {
|
||||
return false
|
||||
}
|
||||
})
|
||||
e.keyCodeVal = codes[key]
|
||||
|
||||
e.initKeyboardEvent('keydown', true, true,
|
||||
$el.defaultView, false, false, false, false, e.keyCodeVal, e.keyCodeVal)
|
||||
|
||||
$el.dispatchEvent(e)
|
||||
log.snapshot().end()
|
||||
return $el
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keydown', { prevSubject: "dom" }, keydownCommand)
|
||||
Cypress.Commands.add('left', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowLeft'))
|
||||
Cypress.Commands.add('right', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowRight'))
|
||||
Cypress.Commands.add('up', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowUp'))
|
||||
Cypress.Commands.add('down', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowDown'))
|
||||
}
|
||||
})
|
||||
|
||||
const e = $el.createEvent('KeyboardEvent')
|
||||
|
||||
Object.defineProperty(e, 'key', {
|
||||
get: function () {
|
||||
return key
|
||||
}
|
||||
})
|
||||
|
||||
Object.defineProperty(e, 'keyCode', {
|
||||
get: function () {
|
||||
return this.keyCodeVal
|
||||
}
|
||||
})
|
||||
Object.defineProperty(e, 'which', {
|
||||
get: function () {
|
||||
return this.keyCodeVal
|
||||
}
|
||||
})
|
||||
var metaKey = false
|
||||
|
||||
Object.defineProperty(e, 'metaKey', {
|
||||
get: function () {
|
||||
return metaKey
|
||||
}
|
||||
})
|
||||
|
||||
Object.defineProperty(e, 'shiftKey', {
|
||||
get: function () {
|
||||
return false
|
||||
}
|
||||
})
|
||||
e.keyCodeVal = codes[key]
|
||||
|
||||
e.initKeyboardEvent('keydown', true, true,
|
||||
$el.defaultView, false, false, false, false, e.keyCodeVal, e.keyCodeVal)
|
||||
|
||||
$el.dispatchEvent(e)
|
||||
log.snapshot().end()
|
||||
return $el
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keydown', { prevSubject: "dom" }, keydownCommand)
|
||||
Cypress.Commands.add('left', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowLeft'))
|
||||
Cypress.Commands.add('right', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowRight'))
|
||||
Cypress.Commands.add('up', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowUp'))
|
||||
Cypress.Commands.add('down', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowDown'))
|
||||
|
||||
14
frontend/package-lock.json
generated
14
frontend/package-lock.json
generated
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^1.3.0",
|
||||
"cypress": "^9.6.1",
|
||||
"cypress": "^10.0.2",
|
||||
"cypress-fail-on-console-error": "^2.1.3",
|
||||
"cypress-wait-until": "^1.7.1",
|
||||
"mock-socket": "^9.0.3",
|
||||
@@ -6901,9 +6901,9 @@
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/cypress": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-9.6.1.tgz",
|
||||
"integrity": "sha512-ECzmV7pJSkk+NuAhEw6C3D+RIRATkSb2VAHXDY6qGZbca/F9mv5pPsj2LO6Ty6oIFVBTrwCyL9agl28MtJMe2g==",
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.0.2.tgz",
|
||||
"integrity": "sha512-7+C4KHYBcfZrawss+Gt5PlS35rfc6ySc59JcHDVsIMm1E/J35dqE41UEXpdtwIq3549umCerNWnFADzqib4kcA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -22471,9 +22471,9 @@
|
||||
"devOptional": true
|
||||
},
|
||||
"cypress": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-9.6.1.tgz",
|
||||
"integrity": "sha512-ECzmV7pJSkk+NuAhEw6C3D+RIRATkSb2VAHXDY6qGZbca/F9mv5pPsj2LO6Ty6oIFVBTrwCyL9agl28MtJMe2g==",
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.0.2.tgz",
|
||||
"integrity": "sha512-7+C4KHYBcfZrawss+Gt5PlS35rfc6ySc59JcHDVsIMm1E/J35dqE41UEXpdtwIq3549umCerNWnFADzqib4kcA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@cypress/request": "^2.88.10",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^1.3.0",
|
||||
"cypress": "^9.6.1",
|
||||
"cypress": "^10.0.2",
|
||||
"cypress-fail-on-console-error": "^2.1.3",
|
||||
"cypress-wait-until": "^1.7.1",
|
||||
"mock-socket": "^9.0.3",
|
||||
|
||||
@@ -47,62 +47,19 @@
|
||||
</svg>
|
||||
<span>Spiral</span>
|
||||
</a>
|
||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
|
||||
<rect style="fill: white" width="360" height="360" />
|
||||
<g transform="matrix(0.62 0 0 0.62 180 180)">
|
||||
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
<span>Gemini</span>
|
||||
</a>
|
||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="50%" x2="100%" y2="50%" id="linearGradient-1">
|
||||
<stop stop-color="#00BFFF" offset="0%"></stop>
|
||||
<stop stop-color="#6619FF" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<rect fill="#1A1D40" x="0" y="0" width="400" height="400"></rect>
|
||||
<path d="M244.25,200 L310,265.75 L286.8,265.75 C282.823093,265.746499 279.010347,264.16385 276.2,261.35 L215,200 L276.25,138.6 C279.068515,135.804479 282.880256,134.240227 286.85,134.249954 L310,134.249954 L244.25,200 Z M123.75,138.6 C120.931485,135.804479 117.119744,134.240227 113.15,134.249954 L90,134.249954 L155.75,200 L90,265.75 L113.2,265.75 C117.176907,265.746499 120.989653,264.16385 123.8,261.35 L185,200 L123.75,138.6 Z M200,215 L138.6,276.25 C135.804479,279.068515 134.240227,282.880256 134.249954,286.85 L134.249954,310 L200,244.25 L265.750046,310 L265.750046,286.85 C265.759773,282.880256 264.195521,279.068515 261.4,276.25 L200,215 Z M200,185 L261.4,123.75 C264.195521,120.931485 265.759773,117.119744 265.750046,113.15 L265.750046,90 L200,155.75 L134.249954,90 L134.249954,113.15 C134.240227,117.119744 135.804479,120.931485 138.6,123.75 L200,185 Z" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<span>Exodus</span>
|
||||
</a>
|
||||
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-10 -10 100 100" class="image">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<rect fill="#87E1A1" fill-rule="nonzero" x="0" y="0" width="400" height="400"></rect>
|
||||
<path d="M124,149.256434 L169.106586,149.256434 L169.106586,128.378728 C169.106586,102.958946 183.316852,90 207.489341,90 L276.773787,90 L276.773787,119.404671 L222.192348,119.404671 C216.458028,119.404671 213.968815,122.397366 213.968815,127.633575 L213.968815,149.256434 L276.023264,149.256434 L276.023264,181.902184 L213.968815,181.902184 L213.968815,310 L169.106586,310 L169.106586,181.902184 L124,181.902184 L124,149.256434" fill="#000000"></path>
|
||||
<g transform="translate(-186.000000, -2316.000000)">
|
||||
<g transform="translate(186.000000, 2316.000000)">
|
||||
<rect id="" fill="#023D32" x="-10" y="-10" width="100" height="100" rx="8"></rect>
|
||||
<path d="M61.6666667,9.16666667 L61.6666667,17.0041667 L46.2625,17.0041667 C46.2625,17.0041667 44.1666667,16.6666667 44.1666667,18.3333333 L44.1666667,25.8025 L61.6666667,25.8025 L61.6666667,34.7391667 L44.1666667,34.7391667 L44.1666667,70.5575 L31.7825,70.5575 L31.7825,35 L19.1666667,35 L19.1666667,25.595 L31.6666667,25.595 L31.6666667,17.5 C31.6666667,17.5 32.5,9.16666667 40.4166667,9.16666667 L61.6666667,9.16666667 Z" id="Fill-1" fill="#86E2A0"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<span>Foundry</span>
|
||||
</a>
|
||||
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 216 216" class="image" style="enable-background:new 0 0 216 216;">
|
||||
<style type="text/css">
|
||||
.ucst0{fill:#002248;}
|
||||
.ucst1{opacity:0.5;fill:#FFFFFF;}
|
||||
.ucst2{fill:#FFFFFF;}
|
||||
.ucst3{opacity:0.75;fill:#FFFFFF;}
|
||||
</style>
|
||||
<rect class="ucst0" width="216" height="216"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="ucst1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
|
||||
<path class="ucst2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
|
||||
<path class="ucst3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<span>Unchained</span>
|
||||
</a>
|
||||
<a href="https://blockstream.com/" target="_blank" title="Blockstream">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" x="0px" y="0px" viewBox="200 200 600 600" class="image" style="enable-background:new 0 0 1000 1000;background-color: #111316 !important">
|
||||
<style type="text/css">
|
||||
@@ -149,6 +106,76 @@
|
||||
</svg>
|
||||
<span>Blockstream</span>
|
||||
</a>
|
||||
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 216 216" class="image" style="enable-background:new 0 0 216 216;">
|
||||
<style type="text/css">
|
||||
.ucst0{fill:#002248;}
|
||||
.ucst1{opacity:0.5;fill:#FFFFFF;}
|
||||
.ucst2{fill:#FFFFFF;}
|
||||
.ucst3{opacity:0.75;fill:#FFFFFF;}
|
||||
</style>
|
||||
<rect class="ucst0" width="216" height="216"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="ucst1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
|
||||
<path class="ucst2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
|
||||
<path class="ucst3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<span>Unchained</span>
|
||||
</a>
|
||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
|
||||
<rect style="fill: black" width="360" height="360" />
|
||||
<g transform="matrix(0.62 0 0 0.62 180 180)">
|
||||
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
<span>Gemini</span>
|
||||
</a>
|
||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
||||
<g clip-path="url(#clip0_2_14)">
|
||||
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
|
||||
<path d="M150.638 271.648H229.168V229.351H150.346L140.128 196.097L229.168 138.048V87L89.9159 178.303L112.687 250.646L89.9159 322.989L229.459 414V362.952L140.128 304.903L150.638 271.648Z" fill="url(#paint1_linear_2_14)"/>
|
||||
<mask id="mask0_2_14" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="89" y="87" width="323" height="327">
|
||||
<path d="M411.042 178.303L271.79 87.0001V138.048L361.121 196.097L350.612 229.352H271.79V271.649H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint2_linear_2_14)"/>
|
||||
<path d="M150.638 271.649H229.168V229.352H150.346L140.128 196.097L229.168 138.048V87.0001L89.9161 178.303L112.687 250.646L89.9161 322.989L229.46 414V362.952L140.128 304.903L150.638 271.649Z" fill="url(#paint3_linear_2_14)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2_14)">
|
||||
<path d="M408.913 87.0001H90.0877V414H408.913V87.0001Z" fill="url(#paint4_linear_2_14)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2_14" x1="365.994" y1="436.481" x2="272.717" y2="51.089" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0B46F9"/>
|
||||
<stop offset="1" stop-color="#BBFBE0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2_14" x1="365.994" y1="436.481" x2="272.717" y2="51.089" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0B46F9"/>
|
||||
<stop offset="1" stop-color="#BBFBE0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2_14" x1="365.994" y1="436.481" x2="272.717" y2="51.0891" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0B46F9"/>
|
||||
<stop offset="1" stop-color="#BBFBE0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_2_14" x1="365.994" y1="436.481" x2="272.717" y2="51.0891" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0B46F9"/>
|
||||
<stop offset="1" stop-color="#BBFBE0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_2_14" x1="110.525" y1="160.575" x2="271.982" y2="281.156" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.119792" stop-color="#8952FF" stop-opacity="0.87"/>
|
||||
<stop offset="1" stop-color="#DABDFF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2_14">
|
||||
<rect width="327" height="327" fill="white" transform="translate(86 87)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>Exodus</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -85,6 +85,8 @@ export class AddressLabelsComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.detectMultisig(this.vin.inner_redeemscript_asm);
|
||||
|
||||
this.detectMultisig(this.vin.prevout.scriptpubkey_asm);
|
||||
}
|
||||
|
||||
detectMultisig(script: string) {
|
||||
@@ -118,7 +120,11 @@ export class AddressLabelsComponent implements OnInit {
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
||||
|
||||
this.label = $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:`
|
||||
if (ops.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.label = $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:`;
|
||||
}
|
||||
|
||||
handleVout() {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
|
||||
<td class="text-left" [class]="widget ? 'widget' : ''">
|
||||
<a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
|
||||
<a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<div class="tooltip-custom">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest, Observable, timer } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, concat, Observable, timer } from 'rxjs';
|
||||
import { delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
@@ -15,7 +15,7 @@ import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
export class BlocksList implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
blocks$: Observable<any[]> = undefined;
|
||||
blocks$: Observable<BlockExtended[]> = undefined;
|
||||
|
||||
indexingAvailable = false;
|
||||
isLoading = true;
|
||||
@@ -27,6 +27,7 @@ export class BlocksList implements OnInit {
|
||||
blocksCount: number;
|
||||
fromHeightSubject: BehaviorSubject<number> = new BehaviorSubject(this.fromBlockHeight);
|
||||
skeletonLines: number[] = [];
|
||||
lastBlockHeight = -1;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
@@ -57,6 +58,7 @@ export class BlocksList implements OnInit {
|
||||
this.blocksCount = blocks[0].height + 1;
|
||||
}
|
||||
this.isLoading = false;
|
||||
this.lastBlockHeight = Math.max(...blocks.map(o => o.height))
|
||||
}),
|
||||
map(blocks => {
|
||||
if (this.indexingAvailable) {
|
||||
@@ -73,12 +75,18 @@ export class BlocksList implements OnInit {
|
||||
}),
|
||||
retryWhen(errors => errors.pipe(delayWhen(() => timer(10000))))
|
||||
)
|
||||
})
|
||||
})
|
||||
),
|
||||
this.stateService.blocks$
|
||||
.pipe(
|
||||
skip(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT - 1),
|
||||
),
|
||||
switchMap((block) => {
|
||||
if (block[0].height < this.lastBlockHeight) {
|
||||
return []; // Return an empty stream so the last pipe is not executed
|
||||
}
|
||||
this.lastBlockHeight = block[0].height;
|
||||
return [block];
|
||||
})
|
||||
)
|
||||
])
|
||||
.pipe(
|
||||
scan((acc, blocks) => {
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 22px 20px;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,3 +148,7 @@
|
||||
margin-right: -2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -1,44 +1,73 @@
|
||||
<div class="fee-estimation-wrapper">
|
||||
<div class="fee-estimation-container" *ngIf="(isLoadingWebSocket$ | async) === false && (feeEstimations$ | async) as feeEstimations; else loadingFees">
|
||||
<div class="fee-estimation-wrapper" *ngIf="(isLoadingWebSocket$ | async) === false && (recommendedFees$ | async) as recommendedFees; else loadingFees">
|
||||
<div class="d-flex">
|
||||
<div class="fee-progress-bar" [style.background]="noPriority">
|
||||
<span class="fee-label" i18n="fees-box.no-priority" i18n-ngbTooltip="Transaction feerate tooltip (economy)" ngbTooltip="Either 2x the minimum, or the Low Priority rate (whichever is lower)" placement="top">No Priority</span>
|
||||
</div>
|
||||
<div class="band-separator fill"></div>
|
||||
<div class="fee-progress-bar priority" [style.background]="gradient">
|
||||
<span class="fee-label prority" i18n="fees-box.low-priority" i18n-ngbTooltip="Transaction feerate tooltip (low priority)" ngbTooltip="Places your transaction in between the second and third mempool blocks" placement="top">Low Priority</span>
|
||||
<span class="fee-label prority" i18n="fees-box.medium-priority" i18n-ngbTooltip="Transaction feerate tooltip (medium priority)" ngbTooltip="Places your transaction in between the first and second mempool blocks" placement="top">Medium Priority</span>
|
||||
<span class="fee-label prority" i18n="fees-box.high-priority" i18n-ngbTooltip="Transaction feerate tooltip (high priority)" ngbTooltip="Places your transaction in the first mempool block" placement="top">High Priority</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fee-estimation-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="fees-box.low-priority">Low priority</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom">
|
||||
<div class="fee-text">{{ feeEstimations.hourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="feeEstimations.hourFee * 140" ></app-fiat></span>
|
||||
<div class="card-text">
|
||||
<div class="fee-text">{{ recommendedFees.economyFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.economyFee * 140" ></app-fiat></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="band-separator"></div>
|
||||
<div class="item">
|
||||
<div class="card-text">
|
||||
<div class="fee-text">{{ recommendedFees.hourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.hourFee * 140" ></app-fiat></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="fees-box.medium-priority">Medium priority</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom">
|
||||
<div class="fee-text">{{ feeEstimations.halfHourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="feeEstimations.halfHourFee * 140" ></app-fiat></span>
|
||||
<div class="card-text">
|
||||
<div class="fee-text">{{ recommendedFees.halfHourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.halfHourFee * 140" ></app-fiat></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="fees-box.high-priority">High priority</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom">
|
||||
<div class="fee-text">{{ feeEstimations.fastestFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="feeEstimations.fastestFee * 140" ></app-fiat></span>
|
||||
<div class="card-text">
|
||||
<div class="fee-text">{{ recommendedFees.fastestFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.fastestFee * 140" ></app-fiat></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingFees>
|
||||
<div class="d-flex">
|
||||
<div class="fee-progress-bar" [style.background]="noPriority">
|
||||
<span class="fee-label" i18n="fees-box.no-priority">No Priority</span>
|
||||
</div>
|
||||
<div class="band-separator fill"></div>
|
||||
<div class="fee-progress-bar priority" [style.background]="gradient">
|
||||
<span class="fee-label prority" i18n="fees-box.low-priority">Low Priority</span>
|
||||
<span class="fee-label prority" i18n="fees-box.medium-priority">Medium Priority</span>
|
||||
<span class="fee-label prority" i18n="fees-box.high-priority">High Priority</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fee-estimation-container loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="fees-box.low-priority">Low priority</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="band-separator"></div>
|
||||
<div class="item">
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="fees-box.medium-priority">Medium priority</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="fees-box.high-priority">High priority</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.card-title {
|
||||
color: #4a68b9;
|
||||
font-size: 10px;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@@ -21,28 +21,17 @@
|
||||
.fee-estimation-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
flex-direction: row;
|
||||
.item {
|
||||
max-width: 150px;
|
||||
width: 100px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
&:first-child{
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
&:first-child {
|
||||
@media (767px < width < 992px), (width < 576px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
margin: 0 auto 0px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -55,18 +44,17 @@
|
||||
border-bottom: 1px solid #ffffff1c;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
line-height: 1.45;
|
||||
padding: 0px 2px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.fiat {
|
||||
display: block;
|
||||
font-size: 14px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container{
|
||||
min-height: 76px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
@@ -74,12 +62,62 @@
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
max-width: 90px;
|
||||
margin: 15px auto 3px;
|
||||
max-width: 70px;
|
||||
margin: 10px auto 3px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 3px;
|
||||
max-width: 55px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fee-progress-bar {
|
||||
width: 25%;
|
||||
height: 22px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
transition: background-color 1s;
|
||||
&.priority {
|
||||
@media (767px < width < 992px), (width < 576px) {
|
||||
width: 100%;
|
||||
}
|
||||
width: 75%;
|
||||
border-radius: 0px 10px 10px 0px;
|
||||
}
|
||||
&:first-child {
|
||||
@media (767px < width < 992px), (width < 576px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.band-separator {
|
||||
width: 5%;
|
||||
@media (767px < width < 992px), (width < 576px) {
|
||||
display: none
|
||||
}
|
||||
&.fill {
|
||||
height: 22px;
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
rgb(45, 51, 72),
|
||||
rgb(45, 51, 72) 2px,
|
||||
rgb(29, 31, 49) 2px,
|
||||
rgb(29, 31, 49) 4px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.fee-label {
|
||||
padding-top: 2px;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
@media (767px < width < 992px), (width < 576px) {
|
||||
width: 33%;
|
||||
}
|
||||
&.prority {
|
||||
width: 33%;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { map, filter } from 'rxjs/operators';
|
||||
import { merge, Observable } from 'rxjs';
|
||||
import { MempoolBlock } from 'src/app/interfaces/websocket.interface';
|
||||
|
||||
interface FeeEstimations {
|
||||
fastestFee: number;
|
||||
halfHourFee: number;
|
||||
hourFee: number;
|
||||
}
|
||||
import { Observable } from 'rxjs';
|
||||
import { Recommendedfees } from 'src/app/interfaces/websocket.interface';
|
||||
import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-fees-box',
|
||||
@@ -17,52 +12,32 @@ interface FeeEstimations {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FeesBoxComponent implements OnInit {
|
||||
feeEstimations$: Observable<FeeEstimations>;
|
||||
isLoadingWebSocket$: Observable<boolean>;
|
||||
defaultFee: number;
|
||||
recommendedFees$: Observable<Recommendedfees>;
|
||||
gradient = 'linear-gradient(to right, #2e324e, #2e324e)';
|
||||
noPriority = '#2e324e';
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private stateService: StateService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.defaultFee = this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ? 0.1 : 1;
|
||||
|
||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||
this.feeEstimations$ = this.stateService.mempoolBlocks$
|
||||
this.recommendedFees$ = this.stateService.recommendedFees$
|
||||
.pipe(
|
||||
map((pBlocks) => {
|
||||
if (!pBlocks.length) {
|
||||
return {
|
||||
'fastestFee': this.defaultFee,
|
||||
'halfHourFee': this.defaultFee,
|
||||
'hourFee': this.defaultFee,
|
||||
};
|
||||
}
|
||||
tap((fees) => {
|
||||
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.minimumFee >= feeLvl);
|
||||
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
|
||||
const startColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
|
||||
|
||||
const firstMedianFee = this.optimizeMedianFee(pBlocks[0], pBlocks[1]);
|
||||
const secondMedianFee = pBlocks[1] ? this.optimizeMedianFee(pBlocks[1], pBlocks[2], firstMedianFee) : this.defaultFee;
|
||||
const thirdMedianFee = pBlocks[2] ? this.optimizeMedianFee(pBlocks[2], pBlocks[3], secondMedianFee) : this.defaultFee;
|
||||
feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.fastestFee >= feeLvl);
|
||||
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
|
||||
const endColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
|
||||
|
||||
return {
|
||||
'fastestFee': firstMedianFee,
|
||||
'halfHourFee': secondMedianFee,
|
||||
'hourFee': thirdMedianFee,
|
||||
};
|
||||
})
|
||||
);
|
||||
this.gradient = `linear-gradient(to right, ${startColor}, ${endColor})`;
|
||||
this.noPriority = startColor;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private optimizeMedianFee(pBlock: MempoolBlock, nextBlock: MempoolBlock | undefined, previousFee?: number): number {
|
||||
const useFee = previousFee ? (pBlock.medianFee + previousFee) / 2 : pBlock.medianFee;
|
||||
if (pBlock.blockVSize <= 500000) {
|
||||
return this.defaultFee;
|
||||
}
|
||||
if (pBlock.blockVSize <= 950000 && !nextBlock) {
|
||||
const multiplier = (pBlock.blockVSize - 500000) / 500000;
|
||||
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
|
||||
}
|
||||
return Math.ceil(useFee);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<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">
|
||||
<img [src]="officialMempoolSpace ? './resources/mempool-space-logo.png' : './resources/mempool-logo.png'" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
|
||||
<img *ngIf="!officialMempoolSpace" src="./resources/mempool-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 140px; height: 35px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
|
||||
<div class="connection-badge">
|
||||
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
|
||||
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
|
||||
|
||||
@@ -0,0 +1,682 @@
|
||||
import { FastVertexArray } from './fast-vertex-array'
|
||||
import TxView from './tx-view'
|
||||
import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
|
||||
import { Position, Square } from './sprite-types'
|
||||
|
||||
export default class BlockScene {
|
||||
scene: { count: number, offset: { x: number, y: number}};
|
||||
vertexArray: FastVertexArray;
|
||||
txs: { [key: string]: TxView };
|
||||
width: number;
|
||||
height: number;
|
||||
gridWidth: number;
|
||||
gridHeight: number;
|
||||
gridSize: number;
|
||||
vbytesPerUnit: number;
|
||||
unitPadding: number;
|
||||
unitWidth: number;
|
||||
initialised: boolean;
|
||||
layout: BlockLayout;
|
||||
dirty: boolean;
|
||||
|
||||
constructor ({ width, height, resolution, blockLimit, vertexArray }: { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }) {
|
||||
this.init({ width, height, resolution, blockLimit, vertexArray })
|
||||
}
|
||||
|
||||
destroy (): void {
|
||||
Object.values(this.txs).forEach(tx => tx.destroy())
|
||||
}
|
||||
|
||||
resize ({ width = this.width, height = this.height }: { width?: number, height?: number}): void {
|
||||
this.width = width
|
||||
this.height = height
|
||||
this.gridSize = this.width / this.gridWidth
|
||||
this.unitPadding = Math.floor(Math.max(1, width / 1000))
|
||||
this.unitWidth = this.gridSize - (this.unitPadding * 2)
|
||||
|
||||
this.dirty = true
|
||||
if (this.initialised && this.scene) this.updateAll(performance.now())
|
||||
}
|
||||
|
||||
// Animate new block entering scene
|
||||
enter (txs: TransactionStripped[], direction) {
|
||||
this.replace(txs, direction)
|
||||
}
|
||||
|
||||
// Animate block leaving scene
|
||||
exit (direction: string): void {
|
||||
const startTime = performance.now()
|
||||
const removed = this.removeBatch(Object.keys(this.txs), startTime, direction)
|
||||
|
||||
// clean up sprites
|
||||
setTimeout(() => {
|
||||
removed.forEach(tx => {
|
||||
tx.destroy()
|
||||
})
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// Reset layout and replace with new set of transactions
|
||||
replace (txs: TransactionStripped[], direction: string = 'left'): void {
|
||||
const startTime = performance.now()
|
||||
const nextIds = {}
|
||||
const remove = []
|
||||
txs.forEach(tx => {
|
||||
nextIds[tx.txid] = true
|
||||
})
|
||||
Object.keys(this.txs).forEach(txid => {
|
||||
if (!nextIds[txid]) remove.push(txid)
|
||||
})
|
||||
txs.forEach(tx => {
|
||||
if (!this.txs[tx.txid]) this.txs[tx.txid] = new TxView(tx, this.vertexArray)
|
||||
})
|
||||
|
||||
const removed = this.removeBatch(remove, startTime, direction)
|
||||
|
||||
// clean up sprites
|
||||
setTimeout(() => {
|
||||
removed.forEach(tx => {
|
||||
tx.destroy()
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
|
||||
|
||||
Object.values(this.txs).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
|
||||
this.place(tx)
|
||||
})
|
||||
|
||||
this.updateAll(startTime, direction)
|
||||
}
|
||||
|
||||
update (add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
const startTime = performance.now()
|
||||
const removed = this.removeBatch(remove, startTime, direction)
|
||||
|
||||
// clean up sprites
|
||||
setTimeout(() => {
|
||||
removed.forEach(tx => {
|
||||
tx.destroy()
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
if (resetLayout) {
|
||||
add.forEach(tx => {
|
||||
if (!this.txs[tx.txid]) this.txs[tx.txid] = new TxView(tx, this.vertexArray)
|
||||
})
|
||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
|
||||
Object.values(this.txs).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
|
||||
this.place(tx)
|
||||
})
|
||||
} else {
|
||||
// try to insert new txs directly
|
||||
const remaining = []
|
||||
add.map(tx => new TxView(tx, this.vertexArray)).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
|
||||
if (!this.tryInsertByFee(tx)) {
|
||||
remaining.push(tx)
|
||||
}
|
||||
})
|
||||
this.placeBatch(remaining)
|
||||
this.layout.applyGravity()
|
||||
}
|
||||
|
||||
this.updateAll(startTime, direction)
|
||||
}
|
||||
|
||||
//return the tx at this screen position, if any
|
||||
getTxAt (position: Position): TxView | void {
|
||||
if (this.layout) {
|
||||
const gridPosition = this.screenToGrid(position)
|
||||
return this.layout.getTx(gridPosition)
|
||||
} else return null
|
||||
}
|
||||
|
||||
private init ({ width, height, resolution, blockLimit, vertexArray }: { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }): void {
|
||||
this.vertexArray = vertexArray
|
||||
|
||||
this.scene = {
|
||||
count: 0,
|
||||
offset: {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Set the scale of the visualization (with a 5% margin)
|
||||
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.05, 2)
|
||||
this.gridWidth = resolution
|
||||
this.gridHeight = resolution
|
||||
this.resize({ width, height })
|
||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
|
||||
|
||||
this.txs = {}
|
||||
|
||||
this.initialised = true
|
||||
this.dirty = true
|
||||
}
|
||||
|
||||
private insert (tx: TxView, startTime: number, direction: string = 'left'): void {
|
||||
this.txs[tx.txid] = tx
|
||||
this.place(tx)
|
||||
this.updateTx(tx, startTime, direction)
|
||||
}
|
||||
|
||||
private updateTx (tx: TxView, startTime: number, direction: string = 'left'): void {
|
||||
if (tx.dirty || this.dirty) {
|
||||
this.saveGridToScreenPosition(tx)
|
||||
this.setTxOnScreen(tx, startTime, direction)
|
||||
}
|
||||
}
|
||||
|
||||
private setTxOnScreen (tx: TxView, startTime: number, direction: string = 'left'): void {
|
||||
if (!tx.initialised) {
|
||||
const txColor = tx.getColor()
|
||||
tx.update({
|
||||
display: {
|
||||
position: {
|
||||
x: tx.screenPosition.x + (direction == 'right' ? -this.width : this.width) * 1.4,
|
||||
y: tx.screenPosition.y,
|
||||
s: tx.screenPosition.s
|
||||
},
|
||||
color: txColor,
|
||||
},
|
||||
start: startTime,
|
||||
delay: 0,
|
||||
})
|
||||
tx.update({
|
||||
display: {
|
||||
position: tx.screenPosition,
|
||||
color: txColor
|
||||
},
|
||||
duration: 1000,
|
||||
start: startTime,
|
||||
delay: 50,
|
||||
})
|
||||
} else {
|
||||
tx.update({
|
||||
display: {
|
||||
position: tx.screenPosition
|
||||
},
|
||||
duration: 1000,
|
||||
minDuration: 500,
|
||||
start: startTime,
|
||||
delay: 50,
|
||||
adjust: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private updateAll (startTime: number, direction: string = 'left'): void {
|
||||
this.scene.count = 0
|
||||
const ids = this.getTxList()
|
||||
startTime = startTime || performance.now()
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
this.updateTx(this.txs[ids[i]], startTime, direction)
|
||||
}
|
||||
this.dirty = false
|
||||
}
|
||||
|
||||
private remove (id: string, startTime: number, direction: string = 'left'): TxView | void {
|
||||
const tx = this.txs[id]
|
||||
if (tx) {
|
||||
this.layout.remove(tx)
|
||||
tx.update({
|
||||
display: {
|
||||
position: {
|
||||
x: tx.screenPosition.x + (direction == 'right' ? this.width : -this.width) * 1.4,
|
||||
y: this.txs[id].screenPosition.y,
|
||||
}
|
||||
},
|
||||
duration: 1000,
|
||||
start: startTime,
|
||||
delay: 50
|
||||
})
|
||||
}
|
||||
delete this.txs[id]
|
||||
return tx
|
||||
}
|
||||
|
||||
private getTxList (): string[] {
|
||||
return Object.keys(this.txs)
|
||||
}
|
||||
|
||||
private saveGridToScreenPosition (tx: TxView): void {
|
||||
tx.screenPosition = this.gridToScreen(tx.gridPosition)
|
||||
}
|
||||
|
||||
// convert grid coordinates to screen coordinates
|
||||
private gridToScreen (position: Square | void): Square {
|
||||
if (position) {
|
||||
const slotSize = (position.s * this.gridSize)
|
||||
const squareSize = slotSize - (this.unitPadding * 2)
|
||||
|
||||
// The grid is laid out notionally left-to-right, bottom-to-top
|
||||
// So we rotate 90deg counterclockwise then flip the y axis
|
||||
//
|
||||
// grid screen
|
||||
// ________ ________ ________
|
||||
// | | | b| | a|
|
||||
// | | rotate | | flip | c |
|
||||
// | c | --> | c | --> | |
|
||||
// |a______b| |_______a| |_______b|
|
||||
return {
|
||||
x: this.width + (this.unitPadding * 2) - (this.gridSize * position.y) - slotSize,
|
||||
y: this.height - ((this.gridSize * position.x) + (slotSize - this.unitPadding)),
|
||||
s: squareSize
|
||||
}
|
||||
} else {
|
||||
return { x: 0, y: 0, s: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
screenToGrid (position: Position): Position {
|
||||
const grid = {
|
||||
x: Math.floor((position.y - this.unitPadding) / this.gridSize),
|
||||
y: Math.floor((this.width + (this.unitPadding * 2) - position.x) / this.gridSize)
|
||||
}
|
||||
return grid
|
||||
}
|
||||
|
||||
// calculates and returns the size of the tx in multiples of the grid size
|
||||
private txSize (tx: TxView): number {
|
||||
let scale = Math.max(1,Math.round(Math.sqrt(tx.vsize / this.vbytesPerUnit)))
|
||||
return Math.min(this.gridWidth, Math.max(1, scale)) // bound between 1 and the max displayable size (just in case!)
|
||||
}
|
||||
|
||||
private place (tx: TxView): void {
|
||||
const size = this.txSize(tx)
|
||||
this.layout.insert(tx, size)
|
||||
}
|
||||
|
||||
private tryInsertByFee (tx: TxView): boolean {
|
||||
const size = this.txSize(tx)
|
||||
const position = this.layout.tryInsertByFee(tx, size)
|
||||
if (position) {
|
||||
this.txs[tx.txid] = tx
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Add a list of transactions to the layout,
|
||||
// keeping everything approximately sorted by feerate.
|
||||
private placeBatch (txs: TxView[]): void {
|
||||
if (txs.length) {
|
||||
// grab the new tx with the highest fee rate
|
||||
txs = txs.sort((a,b) => { return b.feerate - a.feerate })
|
||||
let i = 0
|
||||
let maxSize = txs.reduce((max, tx) => {
|
||||
return Math.max(this.txSize(tx), max)
|
||||
}, 1) * 2
|
||||
|
||||
// find a reasonable place for it in the layout
|
||||
const root = this.layout.getReplacementRoot(txs[0].feerate, maxSize)
|
||||
|
||||
// extract a sub tree of transactions from the layout, rooted at that point
|
||||
const popped = this.layout.popTree(root.x, root.y, maxSize)
|
||||
// combine those with the new transactions and sort
|
||||
txs = txs.concat(popped)
|
||||
txs = txs.sort((a,b) => { return b.feerate - a.feerate })
|
||||
|
||||
// insert everything back into the layout
|
||||
txs.forEach(tx => {
|
||||
this.txs[tx.txid] = tx
|
||||
this.place(tx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private removeBatch (ids: string[], startTime: number, direction: string = 'left'): TxView[] {
|
||||
if (!startTime) startTime = performance.now()
|
||||
return ids.map(id => {
|
||||
return this.remove(id, startTime, direction)
|
||||
}).filter(tx => tx != null) as TxView[]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Slot {
|
||||
l: number
|
||||
r: number
|
||||
w: number
|
||||
|
||||
constructor (l: number, r: number) {
|
||||
this.l = l
|
||||
this.r = r
|
||||
this.w = r - l
|
||||
}
|
||||
|
||||
intersects (slot: Slot): boolean {
|
||||
return !((slot.r <= this.l) || (slot.l >= this.r))
|
||||
}
|
||||
|
||||
subtract (slot: Slot): Slot[] | void {
|
||||
if (this.intersects(slot)) {
|
||||
// from middle
|
||||
if (slot.l > this.l && slot.r < this.r) {
|
||||
return [
|
||||
new Slot(this.l, slot.l),
|
||||
new Slot(slot.r, this.r)
|
||||
]
|
||||
} // totally covered
|
||||
else if (slot.l <= this.l && slot.r >= this.r) {
|
||||
return []
|
||||
} // from left side
|
||||
else if (slot.l <= this.l) {
|
||||
if (slot.r == this.r) return []
|
||||
else return [new Slot(slot.r, this.r)]
|
||||
} // from right side
|
||||
else if (slot.r >= this.r) {
|
||||
if (slot.l == this.l) return []
|
||||
else return [new Slot(this.l, slot.l)]
|
||||
}
|
||||
} else return [this]
|
||||
}
|
||||
}
|
||||
|
||||
class TxSlot extends Slot {
|
||||
tx: TxView
|
||||
|
||||
constructor (l: number, r: number, tx: TxView) {
|
||||
super(l, r)
|
||||
this.tx = tx
|
||||
}
|
||||
}
|
||||
|
||||
class Row {
|
||||
y: number
|
||||
w: number
|
||||
filled: TxSlot[]
|
||||
slots: Slot[]
|
||||
|
||||
|
||||
constructor (y: number, width: number) {
|
||||
this.y = y
|
||||
this.w = width
|
||||
this.filled = []
|
||||
this.slots = [new Slot(0, this.w)]
|
||||
}
|
||||
|
||||
// insert a transaction w/ given width into row starting at position x
|
||||
insert (x: number, w: number, tx: TxView): void {
|
||||
const newSlot = new TxSlot(x, x + w, tx)
|
||||
// insert into filled list
|
||||
let index = this.filled.findIndex((slot) => { return slot.l >= newSlot.r })
|
||||
if (index < 0) index = this.filled.length
|
||||
this.filled.splice(index || 0, 0, newSlot)
|
||||
// subtract from overlapping slots
|
||||
for (let i = 0; i < this.slots.length; i++) {
|
||||
if (newSlot.intersects(this.slots[i])) {
|
||||
const diff = this.slots[i].subtract(newSlot)
|
||||
if (diff) {
|
||||
this.slots.splice(i, 1, ...diff)
|
||||
i += diff.length - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remove (x: number, w: number): void {
|
||||
const txIndex = this.filled.findIndex((slot) => { return slot.l == x })
|
||||
this.filled.splice(txIndex, 1)
|
||||
|
||||
const newSlot = new Slot(x, x + w)
|
||||
let slotIndex = this.slots.findIndex((slot) => { return slot.l >= newSlot.r })
|
||||
if (slotIndex < 0) slotIndex = this.slots.length
|
||||
this.slots.splice(slotIndex || 0, 0, newSlot)
|
||||
this.normalize()
|
||||
}
|
||||
|
||||
// merge any contiguous empty slots
|
||||
private normalize (): void {
|
||||
for (let i = 0; i < this.slots.length - 1; i++) {
|
||||
if (this.slots[i].r == this.slots[i+1].l) {
|
||||
this.slots[i].r = this.slots[i+1].r
|
||||
this.slots[i].w += this.slots[i+1].w
|
||||
this.slots.splice(i+1, 1)
|
||||
i--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txAt (x: number): TxView | void {
|
||||
let i = 0
|
||||
while (i < this.filled.length && this.filled[i].l <= x) {
|
||||
if (this.filled[i].l <= x && this.filled[i].r > x) return this.filled[i].tx
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
getSlotsBetween (left: number, right: number): TxSlot[] {
|
||||
const range = new Slot(left, right)
|
||||
return this.filled.filter(slot => {
|
||||
return slot.intersects(range)
|
||||
})
|
||||
}
|
||||
|
||||
slotAt (x: number): Slot | void {
|
||||
let i = 0
|
||||
while (i < this.slots.length && this.slots[i].l <= x) {
|
||||
if (this.slots[i].l <= x && this.slots[i].r > x) return this.slots[i]
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
getAvgFeerate (): number {
|
||||
let count = 0
|
||||
let total = 0
|
||||
this.filled.forEach(slot => {
|
||||
if (slot.tx) {
|
||||
count += slot.w
|
||||
total += (slot.tx.feerate * slot.w)
|
||||
}
|
||||
})
|
||||
return total / count
|
||||
}
|
||||
}
|
||||
|
||||
class BlockLayout {
|
||||
width: number;
|
||||
height: number;
|
||||
rows: Row[];
|
||||
txPositions: { [key: string]: Square }
|
||||
txs: { [key: string]: TxView }
|
||||
|
||||
constructor ({ width, height } : { width: number, height: number }) {
|
||||
this.width = width
|
||||
this.height = height
|
||||
this.rows = [new Row(0, this.width)]
|
||||
this.txPositions = {}
|
||||
this.txs = {}
|
||||
}
|
||||
|
||||
getRow (position: Square): Row {
|
||||
return this.rows[position.y]
|
||||
}
|
||||
|
||||
getTx (position: Square): TxView | void {
|
||||
if (this.getRow(position)) {
|
||||
return this.getRow(position).txAt(position.x)
|
||||
}
|
||||
}
|
||||
|
||||
addRow (): void {
|
||||
this.rows.push(new Row(this.rows.length, this.width))
|
||||
}
|
||||
|
||||
remove (tx: TxView) {
|
||||
const position = this.txPositions[tx.txid]
|
||||
if (position) {
|
||||
for (let y = position.y; y < position.y + position.s && y < this.rows.length; y++) {
|
||||
this.rows[y].remove(position.x, position.s)
|
||||
}
|
||||
}
|
||||
delete this.txPositions[tx.txid]
|
||||
delete this.txs[tx.txid]
|
||||
}
|
||||
|
||||
insert (tx: TxView, width: number): Square {
|
||||
const fit = this.fit(tx, width)
|
||||
|
||||
// insert the tx into rows at that position
|
||||
for (let y = fit.y; y < fit.y + width; y++) {
|
||||
if (y >= this.rows.length) this.addRow()
|
||||
this.rows[y].insert(fit.x, width, tx)
|
||||
}
|
||||
const position = { x: fit.x, y: fit.y, s: width }
|
||||
this.txPositions[tx.txid] = position
|
||||
this.txs[tx.txid] = tx
|
||||
tx.applyGridPosition(position)
|
||||
return position
|
||||
}
|
||||
|
||||
// Find the first slot large enough to hold a transaction of this size
|
||||
fit (tx: TxView, width: number): Square {
|
||||
let fit
|
||||
for (let y = 0; y < this.rows.length && !fit; y++) {
|
||||
fit = this.findFit(0, this.width, y, y, width)
|
||||
}
|
||||
// fall back to placing tx in a new row at the top of the layout
|
||||
if (!fit) {
|
||||
fit = { x: 0, y: this.rows.length }
|
||||
}
|
||||
return fit
|
||||
}
|
||||
|
||||
// recursively check rows to see if there's space for a tx (depth-first)
|
||||
// left/right: initial column boundaries to check
|
||||
// row: current row to check
|
||||
// start: starting row
|
||||
// size: size of space needed
|
||||
findFit (left: number, right: number, row: number, start: number, size: number) : Square {
|
||||
if ((row - start) >= size || row >= this.rows.length) {
|
||||
return { x: left, y: start }
|
||||
}
|
||||
for (let i = 0; i < this.rows[row].slots.length; i++) {
|
||||
const slot = this.rows[row].slots[i]
|
||||
const l = Math.max(left, slot.l)
|
||||
const r = Math.min(right, slot.r)
|
||||
if (r - l >= size) {
|
||||
const fit = this.findFit(l, r, row + 1, start, size)
|
||||
if (fit) return fit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// insert only if the tx fits into a fee-appropriate position
|
||||
tryInsertByFee (tx: TxView, size: number): Square | void {
|
||||
const fit = this.fit(tx, size)
|
||||
|
||||
if (this.checkRowFees(fit.y, tx.feerate)) {
|
||||
// insert the tx into rows at that position
|
||||
for (let y = fit.y; y < fit.y + size; y++) {
|
||||
if (y >= this.rows.length) this.addRow()
|
||||
this.rows[y].insert(fit.x, size, tx)
|
||||
}
|
||||
const position = { x: fit.x, y: fit.y, s: size }
|
||||
this.txPositions[tx.txid] = position
|
||||
this.txs[tx.txid] = tx
|
||||
tx.applyGridPosition(position)
|
||||
return position
|
||||
}
|
||||
}
|
||||
|
||||
// Return the first slot with a lower feerate
|
||||
getReplacementRoot (feerate: number, width: number): Square {
|
||||
let slot
|
||||
for (let row = 0; row <= this.rows.length; row++) {
|
||||
if (this.rows[row].slots.length > 0) {
|
||||
return { x: this.rows[row].slots[0].l, y: row }
|
||||
} else {
|
||||
slot = this.rows[row].filled.find(x => {
|
||||
return x.tx.feerate < feerate
|
||||
})
|
||||
if (slot) {
|
||||
return { x: Math.min(slot.l, this.width - width), y: row }
|
||||
}
|
||||
}
|
||||
}
|
||||
return { x: 0, y: this.rows.length }
|
||||
}
|
||||
|
||||
// remove and return all transactions in a subtree of the layout
|
||||
popTree (x: number, y: number, width: number) {
|
||||
const selected: { [key: string]: TxView } = {}
|
||||
let left = x
|
||||
let right = x + width
|
||||
let prevWidth = right - left
|
||||
let prevFee = Infinity
|
||||
// scan rows upwards within a channel bounded by 'left' and 'right'
|
||||
for (let row = y; row < this.rows.length; row++) {
|
||||
let rowMax = 0
|
||||
let slots = this.rows[row].getSlotsBetween(left, right)
|
||||
// check each slot in this row overlapping the search channel
|
||||
slots.forEach(slot => {
|
||||
// select the associated transaction
|
||||
selected[slot.tx.txid] = slot.tx
|
||||
rowMax = Math.max(rowMax, slot.tx.feerate)
|
||||
// widen the search channel to accommodate this slot if necessary
|
||||
if (slot.w > prevWidth) {
|
||||
left = slot.l
|
||||
right = slot.r
|
||||
// if this slot's tx has a higher feerate than the max in the previous row
|
||||
// (i.e. it's out of position)
|
||||
// select all txs overlapping the slot's full width in some rows *below*
|
||||
// to free up space for this tx to sink down to its proper position
|
||||
if (slot.tx.feerate > prevFee) {
|
||||
let count = 0
|
||||
// keep scanning back down until we find a full row of higher-feerate txs
|
||||
for (let echo = row - 1; echo >= 0 && count < slot.w; echo--) {
|
||||
let echoSlots = this.rows[echo].getSlotsBetween(slot.l, slot.r)
|
||||
count = 0
|
||||
echoSlots.forEach(echoSlot => {
|
||||
selected[echoSlot.tx.txid] = echoSlot.tx
|
||||
if (echoSlot.tx.feerate >= slot.tx.feerate) {
|
||||
count += echoSlot.w
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
prevWidth = right - left
|
||||
prevFee = rowMax
|
||||
}
|
||||
|
||||
const txList = Object.values(selected)
|
||||
|
||||
txList.forEach(tx => {
|
||||
this.remove(tx)
|
||||
})
|
||||
return txList
|
||||
}
|
||||
|
||||
// Check if this row has high enough avg fees
|
||||
// for a tx with this feerate to make sense here
|
||||
checkRowFees (row: number, targetFee: number): boolean {
|
||||
// first row is always fine
|
||||
if (row == 0 || !this.rows[row]) return true
|
||||
return (this.rows[row].getAvgFeerate() > (targetFee * 0.9))
|
||||
}
|
||||
|
||||
// drop any free-floating transactions down into empty spaces
|
||||
applyGravity (): void {
|
||||
Object.entries(this.txPositions).sort(([keyA, posA], [keyB, posB]) => {
|
||||
return posA.y - posB.y || posA.x - posB.x
|
||||
}).forEach(([txid, position]) => {
|
||||
// see how far this transaction can fall
|
||||
let dropTo = position.y
|
||||
while (dropTo > 0 && !this.rows[dropTo - 1].getSlotsBetween(position.x, position.x + position.s).length) {
|
||||
dropTo--;
|
||||
}
|
||||
// if it can fall at all
|
||||
if (dropTo < position.y) {
|
||||
// remove and reinsert in the row we found
|
||||
const tx = this.txs[txid]
|
||||
this.remove(tx)
|
||||
this.insert(tx, position.s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
Utility class for access and management of low-level sprite data
|
||||
|
||||
Maintains a single Float32Array of sprite data, keeping track of empty slots
|
||||
to allow constant-time insertion and deletion
|
||||
|
||||
Automatically resizes by copying to a new, larger Float32Array when necessary,
|
||||
or compacting into a smaller Float32Array when there's space to do so.
|
||||
*/
|
||||
|
||||
import TxSprite from './tx-sprite';
|
||||
|
||||
export class FastVertexArray {
|
||||
length: number;
|
||||
count: number;
|
||||
stride: number;
|
||||
sprites: TxSprite[];
|
||||
data: Float32Array;
|
||||
freeSlots: number[];
|
||||
lastSlot: number;
|
||||
|
||||
constructor(length, stride) {
|
||||
this.length = length;
|
||||
this.count = 0;
|
||||
this.stride = stride;
|
||||
this.sprites = [];
|
||||
this.data = new Float32Array(this.length * this.stride);
|
||||
this.freeSlots = [];
|
||||
this.lastSlot = 0;
|
||||
}
|
||||
|
||||
insert(sprite: TxSprite): number {
|
||||
this.count++;
|
||||
|
||||
let position;
|
||||
if (this.freeSlots.length) {
|
||||
position = this.freeSlots.shift();
|
||||
} else {
|
||||
position = this.lastSlot;
|
||||
this.lastSlot++;
|
||||
if (this.lastSlot > this.length) {
|
||||
this.expand();
|
||||
}
|
||||
}
|
||||
this.sprites[position] = sprite;
|
||||
return position;
|
||||
}
|
||||
|
||||
remove(index: number): void {
|
||||
this.count--;
|
||||
this.clearData(index);
|
||||
this.freeSlots.push(index);
|
||||
this.sprites[index] = null;
|
||||
if (this.length > 2048 && this.count < (this.length * 0.4)) {
|
||||
this.compact();
|
||||
}
|
||||
}
|
||||
|
||||
setData(index: number, dataChunk: number[]): void {
|
||||
this.data.set(dataChunk, (index * this.stride));
|
||||
}
|
||||
|
||||
clearData(index: number): void {
|
||||
this.data.fill(0, (index * this.stride), ((index + 1) * this.stride));
|
||||
}
|
||||
|
||||
getData(index: number): Float32Array {
|
||||
return this.data.subarray(index, this.stride);
|
||||
}
|
||||
|
||||
expand(): void {
|
||||
this.length *= 2;
|
||||
const newData = new Float32Array(this.length * this.stride);
|
||||
newData.set(this.data);
|
||||
this.data = newData;
|
||||
}
|
||||
|
||||
compact(): void {
|
||||
// New array length is the smallest power of 2 larger than the sprite count (but no smaller than 512)
|
||||
const newLength = Math.max(512, Math.pow(2, Math.ceil(Math.log2(this.count))));
|
||||
if (newLength !== this.length) {
|
||||
this.length = newLength;
|
||||
this.data = new Float32Array(this.length * this.stride);
|
||||
let sprite;
|
||||
const newSprites = [];
|
||||
let i = 0;
|
||||
for (const index in this.sprites) {
|
||||
sprite = this.sprites[index];
|
||||
if (sprite) {
|
||||
newSprites.push(sprite);
|
||||
sprite.moveVertexPointer(i);
|
||||
sprite.compile();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
this.sprites = newSprites;
|
||||
this.freeSlots = [];
|
||||
this.lastSlot = i;
|
||||
}
|
||||
}
|
||||
|
||||
getVertexData(): Float32Array {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="mempool-block-overview">
|
||||
<canvas class="block-overview" #blockCanvas></canvas>
|
||||
<div class="loader-wrapper" [class.hidden]="!(isLoading$ | async)">
|
||||
<div class="spinner-border ml-3 loading" role="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
.mempool-block-overview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
background: #181b2d;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.block-overview {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loader-wrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: opacity 500ms 500ms;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
transition: opacity 500ms;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, OnInit,
|
||||
OnDestroy, OnChanges, ChangeDetectionStrategy, NgZone, AfterViewInit } from '@angular/core';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface';
|
||||
import { Subscription, BehaviorSubject, merge, of } from 'rxjs';
|
||||
import { switchMap, filter } from 'rxjs/operators';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { FastVertexArray } from './fast-vertex-array';
|
||||
import BlockScene from './block-scene';
|
||||
import TxSprite from './tx-sprite';
|
||||
import TxView from './tx-view';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mempool-block-overview',
|
||||
templateUrl: './mempool-block-overview.component.html',
|
||||
styleUrls: ['./mempool-block-overview.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
|
||||
@Input() index: number;
|
||||
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
||||
|
||||
@ViewChild('blockCanvas')
|
||||
canvas: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
gl: WebGLRenderingContext;
|
||||
animationFrameRequest: number;
|
||||
displayWidth: number;
|
||||
displayHeight: number;
|
||||
shaderProgram: WebGLProgram;
|
||||
vertexArray: FastVertexArray;
|
||||
running: boolean;
|
||||
scene: BlockScene;
|
||||
hoverTx: TxView | void;
|
||||
selectedTx: TxView | void;
|
||||
lastBlockHeight: number;
|
||||
blockIndex: number;
|
||||
isLoading$ = new BehaviorSubject<boolean>(true);
|
||||
|
||||
blockSub: Subscription;
|
||||
deltaSub: Subscription;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
readonly ngZone: NgZone,
|
||||
) {
|
||||
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.blockSub = merge(
|
||||
of(true),
|
||||
this.stateService.connectionState$.pipe(filter((state) => state === 2))
|
||||
)
|
||||
.pipe(switchMap(() => this.stateService.mempoolBlockTransactions$))
|
||||
.subscribe((transactionsStripped) => {
|
||||
this.replaceBlock(transactionsStripped);
|
||||
});
|
||||
this.deltaSub = this.stateService.mempoolBlockDelta$.subscribe((delta) => {
|
||||
this.updateBlock(delta);
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
|
||||
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
|
||||
this.gl = this.canvas.nativeElement.getContext('webgl');
|
||||
this.initCanvas();
|
||||
|
||||
this.resizeCanvas();
|
||||
}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
if (changes.index) {
|
||||
this.clearBlock(changes.index.currentValue > changes.index.previousValue ? 'right' : 'left');
|
||||
this.isLoading$.next(true);
|
||||
this.websocketService.startTrackMempoolBlock(changes.index.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.blockSub.unsubscribe();
|
||||
this.deltaSub.unsubscribe();
|
||||
this.websocketService.stopTrackMempoolBlock();
|
||||
}
|
||||
|
||||
clearBlock(direction): void {
|
||||
if (this.scene) {
|
||||
this.scene.exit(direction);
|
||||
}
|
||||
this.hoverTx = null;
|
||||
this.selectedTx = null;
|
||||
this.txPreviewEvent.emit(null);
|
||||
}
|
||||
|
||||
replaceBlock(transactionsStripped: TransactionStripped[]): void {
|
||||
if (!this.scene) {
|
||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
|
||||
blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
|
||||
}
|
||||
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
|
||||
if (this.blockIndex !== this.index) {
|
||||
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right';
|
||||
this.scene.enter(transactionsStripped, direction);
|
||||
} else {
|
||||
this.scene.replace(transactionsStripped, blockMined ? 'right' : 'left');
|
||||
}
|
||||
|
||||
this.lastBlockHeight = this.stateService.latestBlockHeight;
|
||||
this.blockIndex = this.index;
|
||||
this.isLoading$.next(false);
|
||||
}
|
||||
|
||||
updateBlock(delta: MempoolBlockDelta): void {
|
||||
if (!this.scene) {
|
||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
|
||||
blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
|
||||
}
|
||||
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
|
||||
|
||||
if (this.blockIndex !== this.index) {
|
||||
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right';
|
||||
this.scene.exit(direction);
|
||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
|
||||
blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
|
||||
this.scene.enter(delta.added, direction);
|
||||
} else {
|
||||
this.scene.update(delta.added, delta.removed, blockMined ? 'right' : 'left', blockMined);
|
||||
}
|
||||
|
||||
this.lastBlockHeight = this.stateService.latestBlockHeight;
|
||||
this.blockIndex = this.index;
|
||||
this.isLoading$.next(false);
|
||||
}
|
||||
|
||||
initCanvas(): void {
|
||||
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
||||
|
||||
const shaderSet = [
|
||||
{
|
||||
type: this.gl.VERTEX_SHADER,
|
||||
src: vertShaderSrc
|
||||
},
|
||||
{
|
||||
type: this.gl.FRAGMENT_SHADER,
|
||||
src: fragShaderSrc
|
||||
}
|
||||
];
|
||||
|
||||
this.shaderProgram = this.buildShaderProgram(shaderSet);
|
||||
|
||||
this.gl.useProgram(this.shaderProgram);
|
||||
|
||||
// Set up alpha blending
|
||||
this.gl.enable(this.gl.BLEND);
|
||||
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
const glBuffer = this.gl.createBuffer();
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer);
|
||||
|
||||
/* SET UP SHADER ATTRIBUTES */
|
||||
Object.keys(attribs).forEach((key, i) => {
|
||||
attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key);
|
||||
this.gl.enableVertexAttribArray(attribs[key].pointer);
|
||||
});
|
||||
|
||||
this.start();
|
||||
}
|
||||
|
||||
handleContextLost(event): void {
|
||||
event.preventDefault();
|
||||
cancelAnimationFrame(this.animationFrameRequest);
|
||||
this.animationFrameRequest = null;
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
handleContextRestored(event): void {
|
||||
this.initCanvas();
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
resizeCanvas(): void {
|
||||
this.displayWidth = this.canvas.nativeElement.parentElement.clientWidth;
|
||||
this.displayHeight = this.canvas.nativeElement.parentElement.clientHeight;
|
||||
this.canvas.nativeElement.width = this.displayWidth;
|
||||
this.canvas.nativeElement.height = this.displayHeight;
|
||||
if (this.gl) {
|
||||
this.gl.viewport(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
|
||||
}
|
||||
if (this.scene) {
|
||||
this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
|
||||
}
|
||||
}
|
||||
|
||||
compileShader(src, type): WebGLShader {
|
||||
const shader = this.gl.createShader(type);
|
||||
|
||||
this.gl.shaderSource(shader, src);
|
||||
this.gl.compileShader(shader);
|
||||
|
||||
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
||||
console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`);
|
||||
console.log(this.gl.getShaderInfoLog(shader));
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
buildShaderProgram(shaderInfo): WebGLProgram {
|
||||
const program = this.gl.createProgram();
|
||||
|
||||
shaderInfo.forEach((desc) => {
|
||||
const shader = this.compileShader(desc.src, desc.type);
|
||||
if (shader) {
|
||||
this.gl.attachShader(program, shader);
|
||||
}
|
||||
});
|
||||
|
||||
this.gl.linkProgram(program);
|
||||
|
||||
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
||||
console.log('Error linking shader program:');
|
||||
console.log(this.gl.getProgramInfoLog(program));
|
||||
}
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.running = true;
|
||||
this.ngZone.runOutsideAngular(() => this.run());
|
||||
}
|
||||
|
||||
run(now?: DOMHighResTimeStamp): void {
|
||||
if (!now) {
|
||||
now = performance.now();
|
||||
}
|
||||
|
||||
/* SET UP SHADER UNIFORMS */
|
||||
// screen dimensions
|
||||
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
|
||||
// frame timestamp
|
||||
this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now);
|
||||
|
||||
/* SET UP SHADER ATTRIBUTES */
|
||||
Object.keys(attribs).forEach((key, i) => {
|
||||
this.gl.vertexAttribPointer(attribs[key].pointer,
|
||||
attribs[key].count, // number of primitives in this attribute
|
||||
this.gl[attribs[key].type], // type of primitive in this attribute (e.g. gl.FLOAT)
|
||||
false, // never normalised
|
||||
stride, // distance between values of the same attribute
|
||||
attribs[key].offset); // offset of the first value
|
||||
});
|
||||
|
||||
const pointArray = this.vertexArray.getVertexData();
|
||||
|
||||
if (pointArray.length) {
|
||||
this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW);
|
||||
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
|
||||
}
|
||||
|
||||
/* LOOP */
|
||||
if (this.running) {
|
||||
if (this.animationFrameRequest) {
|
||||
cancelAnimationFrame(this.animationFrameRequest);
|
||||
this.animationFrameRequest = null;
|
||||
}
|
||||
this.animationFrameRequest = requestAnimationFrame(() => this.run());
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
onClick(event) {
|
||||
this.setPreviewTx(event.offsetX, event.offsetY, true);
|
||||
}
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
this.setPreviewTx(event.offsetX, event.offsetY, false);
|
||||
}
|
||||
|
||||
@HostListener('pointerleave', ['$event'])
|
||||
onPointerLeave(event) {
|
||||
this.setPreviewTx(-1, -1, false);
|
||||
}
|
||||
|
||||
setPreviewTx(x: number, y: number, clicked: boolean = false) {
|
||||
if (this.scene && (!this.selectedTx || clicked)) {
|
||||
const selected = this.scene.getTxAt({ x, y });
|
||||
const currentPreview = this.selectedTx || this.hoverTx;
|
||||
|
||||
if (selected !== currentPreview) {
|
||||
if (currentPreview) {
|
||||
currentPreview.setHover(false);
|
||||
}
|
||||
if (selected) {
|
||||
selected.setHover(true);
|
||||
this.txPreviewEvent.emit({
|
||||
txid: selected.txid,
|
||||
fee: selected.fee,
|
||||
vsize: selected.vsize,
|
||||
value: selected.value
|
||||
});
|
||||
if (clicked) {
|
||||
this.selectedTx = selected;
|
||||
} else {
|
||||
this.hoverTx = selected;
|
||||
}
|
||||
} else {
|
||||
if (clicked) {
|
||||
this.selectedTx = null;
|
||||
}
|
||||
this.hoverTx = null;
|
||||
this.txPreviewEvent.emit(null);
|
||||
}
|
||||
} else if (clicked) {
|
||||
if (selected === this.selectedTx) {
|
||||
this.hoverTx = this.selectedTx;
|
||||
this.selectedTx = null;
|
||||
} else {
|
||||
this.selectedTx = selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebGL shader attributes
|
||||
const attribs = {
|
||||
offset: { type: 'FLOAT', count: 2, pointer: null, offset: 0 },
|
||||
posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||
posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||
posR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||
colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||
colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||
colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||
colA: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }
|
||||
};
|
||||
// Calculate the number of bytes per vertex based on specified attributes
|
||||
const stride = Object.values(attribs).reduce((total, attrib) => {
|
||||
return total + (attrib.count * 4);
|
||||
}, 0);
|
||||
// Calculate vertex attribute offsets
|
||||
for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) {
|
||||
const attrib = Object.values(attribs)[i];
|
||||
attrib.offset = offset;
|
||||
offset += (attrib.count * 4);
|
||||
}
|
||||
|
||||
const vertShaderSrc = `
|
||||
varying lowp vec4 vColor;
|
||||
|
||||
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
|
||||
// shader interpolates between start and end values at the given rate, from the given time
|
||||
|
||||
attribute vec2 offset;
|
||||
attribute vec4 posX;
|
||||
attribute vec4 posY;
|
||||
attribute vec4 posR;
|
||||
attribute vec4 colR;
|
||||
attribute vec4 colG;
|
||||
attribute vec4 colB;
|
||||
attribute vec4 colA;
|
||||
|
||||
uniform vec2 screenSize;
|
||||
uniform float now;
|
||||
|
||||
float smootherstep(float x) {
|
||||
x = clamp(x, 0.0, 1.0);
|
||||
float ix = 1.0 - x;
|
||||
x = x * x;
|
||||
return x / (x + ix * ix);
|
||||
}
|
||||
|
||||
float interpolateAttribute(vec4 attr) {
|
||||
float d = (now - attr.z) * attr.w;
|
||||
float delta = smootherstep(d);
|
||||
return mix(attr.x, attr.y, delta);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
|
||||
// vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
|
||||
|
||||
float radius = interpolateAttribute(posR);
|
||||
vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset);
|
||||
|
||||
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
|
||||
|
||||
float red = interpolateAttribute(colR);
|
||||
float green = interpolateAttribute(colG);
|
||||
float blue = interpolateAttribute(colB);
|
||||
float alpha = interpolateAttribute(colA);
|
||||
|
||||
vColor = vec4(red, green, blue, alpha);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragShaderSrc = `
|
||||
varying lowp vec4 vColor;
|
||||
|
||||
void main() {
|
||||
gl_FragColor = vColor;
|
||||
// premultiply alpha
|
||||
gl_FragColor.rgb *= gl_FragColor.a;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,74 @@
|
||||
export type Position = {
|
||||
x: number,
|
||||
y: number,
|
||||
};
|
||||
|
||||
export type Square = Position & {
|
||||
s?: number
|
||||
};
|
||||
|
||||
export type Color = {
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
a: number,
|
||||
};
|
||||
|
||||
export type InterpolatedAttribute = {
|
||||
a: number,
|
||||
b: number,
|
||||
t: number,
|
||||
v: number,
|
||||
d: number
|
||||
};
|
||||
|
||||
export type Update = Position & { s: number } & Color;
|
||||
|
||||
export type Attributes = {
|
||||
x: InterpolatedAttribute,
|
||||
y: InterpolatedAttribute,
|
||||
s: InterpolatedAttribute,
|
||||
r: InterpolatedAttribute,
|
||||
g: InterpolatedAttribute,
|
||||
b: InterpolatedAttribute,
|
||||
a: InterpolatedAttribute
|
||||
};
|
||||
|
||||
export type OptionalAttributes = {
|
||||
x?: InterpolatedAttribute,
|
||||
y?: InterpolatedAttribute,
|
||||
s?: InterpolatedAttribute,
|
||||
r?: InterpolatedAttribute,
|
||||
g?: InterpolatedAttribute,
|
||||
b?: InterpolatedAttribute,
|
||||
a?: InterpolatedAttribute
|
||||
};
|
||||
|
||||
export type SpriteUpdateParams = {
|
||||
x?: number,
|
||||
y?: number,
|
||||
s?: number,
|
||||
r?: number,
|
||||
g?: number,
|
||||
b?: number,
|
||||
a?: number
|
||||
start?: DOMHighResTimeStamp,
|
||||
duration?: number,
|
||||
minDuration?: number,
|
||||
adjust?: boolean,
|
||||
temp?: boolean
|
||||
};
|
||||
|
||||
export type ViewUpdateParams = {
|
||||
display: {
|
||||
position?: Square,
|
||||
color?: Color,
|
||||
},
|
||||
start?: number,
|
||||
duration?: number,
|
||||
minDuration?: number,
|
||||
delay?: number,
|
||||
jitter?: number,
|
||||
state?: string,
|
||||
adjust?: boolean
|
||||
};
|
||||
215
frontend/src/app/components/mempool-block-overview/tx-sprite.ts
Normal file
215
frontend/src/app/components/mempool-block-overview/tx-sprite.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { FastVertexArray } from './fast-vertex-array';
|
||||
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types';
|
||||
|
||||
const attribKeys = ['a', 'b', 't', 'v'];
|
||||
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
|
||||
|
||||
export default class TxSprite {
|
||||
static vertexSize = 30;
|
||||
static vertexCount = 6;
|
||||
static dataSize: number = (30 * 6);
|
||||
|
||||
vertexArray: FastVertexArray;
|
||||
vertexPointer: number;
|
||||
vertexData: number[];
|
||||
updateMap: Update;
|
||||
attributes: Attributes;
|
||||
tempAttributes: OptionalAttributes;
|
||||
|
||||
|
||||
constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray) {
|
||||
const offsetTime = params.start;
|
||||
this.vertexArray = vertexArray;
|
||||
this.vertexData = Array(VI.length).fill(0);
|
||||
this.updateMap = {
|
||||
x: 0, y: 0, s: 0, r: 0, g: 0, b: 0, a: 0
|
||||
};
|
||||
|
||||
this.attributes = {
|
||||
x: { a: params.x, b: params.x, t: offsetTime, v: 0, d: 0 },
|
||||
y: { a: params.y, b: params.y, t: offsetTime, v: 0, d: 0 },
|
||||
s: { a: params.s, b: params.s, t: offsetTime, v: 0, d: 0 },
|
||||
r: { a: params.r, b: params.r, t: offsetTime, v: 0, d: 0 },
|
||||
g: { a: params.g, b: params.g, t: offsetTime, v: 0, d: 0 },
|
||||
b: { a: params.b, b: params.b, t: offsetTime, v: 0, d: 0 },
|
||||
a: { a: params.a, b: params.a, t: offsetTime, v: 0, d: 0 },
|
||||
};
|
||||
|
||||
// Used to temporarily modify the sprite, so that the base view can be resumed later
|
||||
this.tempAttributes = null;
|
||||
|
||||
this.vertexPointer = this.vertexArray.insert(this);
|
||||
|
||||
this.compile();
|
||||
}
|
||||
|
||||
private interpolateAttributes(updateMap: Update, attributes: OptionalAttributes, offsetTime: DOMHighResTimeStamp, v: number,
|
||||
duration: number, minDuration: number, adjust: boolean): void {
|
||||
for (const key of Object.keys(updateMap)) {
|
||||
// for each non-null attribute:
|
||||
if (updateMap[key] != null) {
|
||||
// calculate current interpolated value, and set as 'from'
|
||||
interpolateAttributeStart(attributes[key], offsetTime);
|
||||
// update start time
|
||||
attributes[key].t = offsetTime;
|
||||
|
||||
if (!adjust || (duration && attributes[key].d === 0)) {
|
||||
attributes[key].v = v;
|
||||
attributes[key].d = duration;
|
||||
} else if (minDuration > attributes[key].d) {
|
||||
// enforce minimum transition duration
|
||||
attributes[key].v = 1 / minDuration;
|
||||
attributes[key].d = minDuration;
|
||||
}
|
||||
// set 'to' to target value
|
||||
attributes[key].b = updateMap[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
params:
|
||||
x, y, s: position & size of the sprite
|
||||
r, g, b, a: color & opacity
|
||||
start: performance.now() timestamp, when to start the transition
|
||||
duration: of the tweening animation
|
||||
adjust: if true, alter the target value of any conflicting transitions without changing the duration
|
||||
minDuration: minimum remaining transition duration when adjust = true
|
||||
temp: if true, this update is only temporary (can be reversed with 'resume')
|
||||
*/
|
||||
update(params: SpriteUpdateParams): void {
|
||||
const offsetTime = params.start || performance.now();
|
||||
const v = params.duration > 0 ? (1 / params.duration) : 0;
|
||||
|
||||
updateKeys.forEach(key => {
|
||||
this.updateMap[key] = params[key];
|
||||
});
|
||||
|
||||
const isModified = !!this.tempAttributes;
|
||||
if (!params.temp) {
|
||||
this.interpolateAttributes(this.updateMap, this.attributes, offsetTime, v, params.duration, params.minDuration, params.adjust);
|
||||
} else {
|
||||
if (!isModified) { // set up tempAttributes
|
||||
this.tempAttributes = {};
|
||||
for (const key of Object.keys(this.updateMap)) {
|
||||
if (this.updateMap[key] != null) {
|
||||
this.tempAttributes[key] = { ...this.attributes[key] };
|
||||
}
|
||||
}
|
||||
}
|
||||
this.interpolateAttributes(this.updateMap, this.tempAttributes, offsetTime, v, params.duration, params.minDuration, params.adjust);
|
||||
}
|
||||
|
||||
this.compile();
|
||||
}
|
||||
|
||||
// Transition back from modified state back to base attributes
|
||||
resume(duration: number, start: DOMHighResTimeStamp = performance.now()): void {
|
||||
// If not in modified state, there's nothing to do
|
||||
if (!this.tempAttributes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offsetTime = start;
|
||||
const v = duration > 0 ? (1 / duration) : 0;
|
||||
|
||||
for (const key of Object.keys(this.tempAttributes)) {
|
||||
// If this base attribute is static (fixed or post-transition), transition smoothly back
|
||||
if (this.attributes[key].v === 0 || (this.attributes[key].t + this.attributes[key].d) <= start) {
|
||||
// calculate current interpolated value, and set as 'from'
|
||||
interpolateAttributeStart(this.tempAttributes[key], offsetTime);
|
||||
this.attributes[key].a = this.tempAttributes[key].a;
|
||||
this.attributes[key].t = offsetTime;
|
||||
this.attributes[key].v = v;
|
||||
this.attributes[key].d = duration;
|
||||
}
|
||||
}
|
||||
|
||||
this.tempAttributes = null;
|
||||
|
||||
this.compile();
|
||||
}
|
||||
|
||||
// Write current state into the graphics vertex array for rendering
|
||||
compile(): void {
|
||||
let attributes = this.attributes;
|
||||
if (this.tempAttributes) {
|
||||
attributes = {
|
||||
...this.attributes,
|
||||
...this.tempAttributes
|
||||
};
|
||||
}
|
||||
const size = attributes.s;
|
||||
|
||||
// update vertex data in place
|
||||
// ugly, but avoids overhead of allocating large temporary arrays
|
||||
const vertexStride = VI.length + 2;
|
||||
for (let vertex = 0; vertex < 6; vertex++) {
|
||||
this.vertexData[vertex * vertexStride] = vertexOffsetFactors[vertex][0];
|
||||
this.vertexData[(vertex * vertexStride) + 1] = vertexOffsetFactors[vertex][1];
|
||||
for (let step = 0; step < VI.length; step++) {
|
||||
// components of each field in the vertex array are defined by an entry in VI:
|
||||
// VI[i].a is the attribute, VI[i].f is the inner field, VI[i].offA and VI[i].offB are offset factors
|
||||
this.vertexData[(vertex * vertexStride) + step + 2] = attributes[VI[step].a][VI[step].f];
|
||||
}
|
||||
}
|
||||
|
||||
this.vertexArray.setData(this.vertexPointer, this.vertexData);
|
||||
}
|
||||
|
||||
moveVertexPointer(index: number): void {
|
||||
this.vertexPointer = index;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.vertexArray.remove(this.vertexPointer);
|
||||
this.vertexPointer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// expects 0 <= x <= 1
|
||||
function smootherstep(x: number): number {
|
||||
const ix = 1 - x;
|
||||
x = x * x;
|
||||
return x / (x + ix * ix);
|
||||
}
|
||||
|
||||
function interpolateAttributeStart(attribute: InterpolatedAttribute, start: DOMHighResTimeStamp): void {
|
||||
if (attribute.v === 0 || (attribute.t + attribute.d) <= start) {
|
||||
// transition finished, next transition starts from current end state
|
||||
// (clamp to 1)
|
||||
attribute.a = attribute.b;
|
||||
attribute.v = 0;
|
||||
attribute.d = 0;
|
||||
} else if (attribute.t > start) {
|
||||
// transition not started
|
||||
// (clamp to 0)
|
||||
} else {
|
||||
// transition in progress
|
||||
// (interpolate)
|
||||
const progress = (start - attribute.t);
|
||||
const delta = smootherstep(progress / attribute.d);
|
||||
attribute.a = attribute.a + (delta * (attribute.b - attribute.a));
|
||||
attribute.d = attribute.d - progress;
|
||||
attribute.v = 1 / attribute.d;
|
||||
}
|
||||
}
|
||||
|
||||
const vertexOffsetFactors = [
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
[1, 0],
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
[0, 1]
|
||||
];
|
||||
|
||||
const VI = [];
|
||||
updateKeys.forEach((attribute, aIndex) => {
|
||||
attribKeys.forEach(field => {
|
||||
VI.push({
|
||||
a: attribute,
|
||||
f: field
|
||||
});
|
||||
});
|
||||
});
|
||||
150
frontend/src/app/components/mempool-block-overview/tx-view.ts
Normal file
150
frontend/src/app/components/mempool-block-overview/tx-view.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import TxSprite from './tx-sprite';
|
||||
import { FastVertexArray } from './fast-vertex-array';
|
||||
import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
|
||||
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
|
||||
import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
|
||||
|
||||
const hoverTransitionTime = 300;
|
||||
const defaultHoverColor = hexToColor('1bd8f4');
|
||||
|
||||
// convert from this class's update format to TxSprite's update format
|
||||
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
|
||||
return {
|
||||
start: (params.start || performance.now()) + (params.delay || 0),
|
||||
duration: params.duration,
|
||||
minDuration: params.minDuration,
|
||||
...params.display.position,
|
||||
...params.display.color,
|
||||
adjust: params.adjust
|
||||
};
|
||||
}
|
||||
|
||||
export default class TxView implements TransactionStripped {
|
||||
txid: string;
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
feerate: number;
|
||||
|
||||
initialised: boolean;
|
||||
vertexArray: FastVertexArray;
|
||||
hover: boolean;
|
||||
sprite: TxSprite;
|
||||
hoverColor: Color | void;
|
||||
|
||||
screenPosition: Square;
|
||||
gridPosition: Square | void;
|
||||
|
||||
dirty: boolean;
|
||||
|
||||
constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
|
||||
this.txid = tx.txid;
|
||||
this.fee = tx.fee;
|
||||
this.vsize = tx.vsize;
|
||||
this.value = tx.value;
|
||||
this.feerate = tx.fee / tx.vsize;
|
||||
this.initialised = false;
|
||||
this.vertexArray = vertexArray;
|
||||
|
||||
this.hover = false;
|
||||
|
||||
this.screenPosition = { x: 0, y: 0, s: 0 };
|
||||
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.sprite) {
|
||||
this.sprite.destroy();
|
||||
this.sprite = null;
|
||||
this.initialised = false;
|
||||
}
|
||||
}
|
||||
|
||||
applyGridPosition(position: Square): void {
|
||||
if (!this.gridPosition) {
|
||||
this.gridPosition = { x: 0, y: 0, s: 0 };
|
||||
}
|
||||
if (this.gridPosition.x !== position.x || this.gridPosition.y !== position.y || this.gridPosition.s !== position.s) {
|
||||
this.gridPosition.x = position.x;
|
||||
this.gridPosition.y = position.y;
|
||||
this.gridPosition.s = position.s;
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
display: defines the final appearance of the sprite
|
||||
position: { x, y, s } (coordinates & size)
|
||||
color: { r, g, b, a} (color channels & alpha)
|
||||
duration: of the tweening animation from the previous display state
|
||||
start: performance.now() timestamp, when to start the transition
|
||||
delay: additional milliseconds to wait before starting
|
||||
jitter: if set, adds a random amount to the delay,
|
||||
adjust: if true, modify an in-progress transition instead of replacing it
|
||||
*/
|
||||
update(params: ViewUpdateParams): void {
|
||||
if (params.jitter) {
|
||||
params.delay += (Math.random() * params.jitter);
|
||||
}
|
||||
|
||||
if (!this.initialised || !this.sprite) {
|
||||
this.initialised = true;
|
||||
this.sprite = new TxSprite(
|
||||
toSpriteUpdate(params),
|
||||
this.vertexArray
|
||||
);
|
||||
// apply any pending hover event
|
||||
if (this.hover) {
|
||||
this.sprite.update({
|
||||
...this.hoverColor,
|
||||
duration: hoverTransitionTime,
|
||||
adjust: false,
|
||||
temp: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.sprite.update(
|
||||
toSpriteUpdate(params)
|
||||
);
|
||||
}
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
// Temporarily override the tx color
|
||||
setHover(hoverOn: boolean, color: Color | void = defaultHoverColor): void {
|
||||
if (hoverOn) {
|
||||
this.hover = true;
|
||||
this.hoverColor = color;
|
||||
|
||||
this.sprite.update({
|
||||
...this.hoverColor,
|
||||
duration: hoverTransitionTime,
|
||||
adjust: false,
|
||||
temp: true
|
||||
});
|
||||
} else {
|
||||
this.hover = false;
|
||||
this.hoverColor = null;
|
||||
if (this.sprite) {
|
||||
this.sprite.resume(hoverTransitionTime);
|
||||
}
|
||||
}
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
getColor(): Color {
|
||||
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => (this.feerate || 1) >= feeLvl);
|
||||
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
|
||||
return hexToColor(mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
function hexToColor(hex: string): Color {
|
||||
return {
|
||||
r: parseInt(hex.slice(0, 2), 16) / 255,
|
||||
g: parseInt(hex.slice(2, 4), 16) / 255,
|
||||
b: parseInt(hex.slice(4, 6), 16) / 255,
|
||||
a: 1
|
||||
};
|
||||
}
|
||||
@@ -10,38 +10,69 @@
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<table class="table table-borderless table-striped table-fixed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="mempool-block.median-fee">Median fee</td>
|
||||
<td>~{{ mempoolBlock.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="mempoolBlock.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="mempool-block.fee-span">Fee span</td>
|
||||
<td><span class="yellow-color">{{ mempoolBlock.feeRange[0] | number:'1.0-0' }} - {{ mempoolBlock.feeRange[mempoolBlock.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||
<td><app-amount [satoshis]="mempoolBlock.totalFees" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolBlock.totalFees" digitsInfo="1.0-0"></app-fiat></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="mempool-block.transactions">Transactions</td>
|
||||
<td>{{ mempoolBlock.nTx }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="mempool-block.size">Size</td>
|
||||
<td>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool {{ (network$ | async) }}" role="progressbar" [ngStyle]="{'width': (mempoolBlock.blockVSize / stateService.blockVSize) * 100 + '%' }"></div>
|
||||
<div class="progress-text" [innerHTML]="mempoolBlock.blockSize | bytes: 2"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-container *ngIf="!previewTx">
|
||||
<tr>
|
||||
<td i18n="mempool-block.median-fee">Median fee</td>
|
||||
<td>~{{ mempoolBlock.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="mempoolBlock.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="mempool-block.fee-span">Fee span</td>
|
||||
<td><span class="yellow-color">{{ mempoolBlock.feeRange[0] | number:'1.0-0' }} - {{ mempoolBlock.feeRange[mempoolBlock.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||
<td><app-amount [satoshis]="mempoolBlock.totalFees" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolBlock.totalFees" digitsInfo="1.0-0"></app-fiat></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="mempool-block.transactions">Transactions</td>
|
||||
<td>{{ mempoolBlock.nTx }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="mempool-block.size">Size</td>
|
||||
<td>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool {{ (network$ | async) }}" role="progressbar" [ngStyle]="{'width': (mempoolBlock.blockVSize / stateService.blockVSize) * 100 + '%' }"></div>
|
||||
<div class="progress-text" [innerHTML]="mempoolBlock.blockSize | bytes: 2"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="previewTx">
|
||||
<tr>
|
||||
<td i18n="shared.transaction">Transaction</td>
|
||||
<td>
|
||||
<a [routerLink]="['/tx/' | relativeUrl, previewTx.txid]">{{ previewTx.txid | shortenString : 16}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.value|Transaction value">Value</td>
|
||||
<td><app-amount [satoshis]="previewTx.value"></app-amount></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td>{{ previewTx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="previewTx.fee"></app-fiat></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||
<td>
|
||||
{{ (previewTx.fee / previewTx.vsize) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||
<td [innerHTML]="'‎' + (previewTx.vsize | vbytes: 2)"></td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
<app-fee-distribution-graph *ngIf="webGlEnabled" [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
|
||||
</div>
|
||||
<div class="col-md chart-container">
|
||||
<app-fee-distribution-graph [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
|
||||
<app-mempool-block-overview *ngIf="webGlEnabled" [index]="mempoolBlockIndex" (txPreviewEvent)="setTxPreview($event)"></app-mempool-block-overview>
|
||||
<app-fee-distribution-graph *ngIf="!webGlEnabled" [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { switchMap, map, tap, filter } from 'rxjs/operators';
|
||||
import { MempoolBlock } from 'src/app/interfaces/websocket.interface';
|
||||
import { MempoolBlock, TransactionStripped } from 'src/app/interfaces/websocket.interface';
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
@@ -18,13 +18,17 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
||||
mempoolBlockIndex: number;
|
||||
mempoolBlock$: Observable<MempoolBlock>;
|
||||
ordinal$: BehaviorSubject<string> = new BehaviorSubject('');
|
||||
previewTx: TransactionStripped | void;
|
||||
webGlEnabled: boolean;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
) { }
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||
@@ -74,5 +78,15 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
return $localize`:@@mempool-block.block.no:Mempool block ${this.mempoolBlockIndex + 1}:INTERPOLATION:`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTxPreview(event: TransactionStripped | void): void {
|
||||
this.previewTx = event
|
||||
}
|
||||
}
|
||||
|
||||
function detectWebGL () {
|
||||
const canvas = document.createElement("canvas");
|
||||
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
||||
return (gl && gl instanceof WebGLRenderingContext)
|
||||
}
|
||||
|
||||
@@ -238,7 +238,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
gradientColors.push(mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
|
||||
});
|
||||
|
||||
|
||||
gradientColors.forEach((color, i, gc) => {
|
||||
backgroundGradients.push(`
|
||||
#${i === 0 ? color : gc[i - 1]} ${ i === 0 ? emptyBackgroundSpacePercentage : ((i / gradientColors.length) * 100) * usedBlockSpace / 100 + emptyBackgroundSpacePercentage }%,
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 18px;
|
||||
padding: 24px 20px !important;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
import { combineLatest, Observable, of } from 'rxjs';
|
||||
import { catchError, map, share, skip, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { concat, Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SinglePoolStats } from 'src/app/interfaces/node-api.interface';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StorageService } from '../..//services/storage.service';
|
||||
@@ -58,15 +58,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
// When...
|
||||
this.miningStatsObservable$ = combineLatest([
|
||||
// ...a new block is mined
|
||||
this.stateService.blocks$
|
||||
.pipe(
|
||||
// (we always receives some blocks at start so only trigger for the last one)
|
||||
skip(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT - 1),
|
||||
),
|
||||
// ...or we change the timespan
|
||||
this.miningStatsObservable$ = concat(
|
||||
this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith(this.miningWindowPreference), // (trigger when the page loads)
|
||||
@@ -76,18 +68,19 @@ export class PoolRankingComponent implements OnInit {
|
||||
this.storageService.setValue('miningWindowPreference', value);
|
||||
}
|
||||
this.miningWindowPreference = value;
|
||||
}),
|
||||
switchMap(() => {
|
||||
return this.miningService.getMiningStats(this.miningWindowPreference);
|
||||
})
|
||||
)
|
||||
])
|
||||
// ...then refresh the mining stats
|
||||
),
|
||||
this.stateService.blocks$
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
return this.miningService.getMiningStats(this.miningWindowPreference);
|
||||
})
|
||||
)
|
||||
)
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
this.isLoading = true;
|
||||
return this.miningService.getMiningStats(this.miningWindowPreference)
|
||||
.pipe(
|
||||
catchError((e) => of(this.getEmptyMiningStat()))
|
||||
);
|
||||
}),
|
||||
map(data => {
|
||||
data.pools = data.pools.map((pool: SinglePoolStats) => this.formatPoolUI(pool));
|
||||
data['minersLuck'] = (100 * (data.blockCount / 1008)).toFixed(2); // luck 1w
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<img [src]="'./resources/mempool-space-logo.png'" height="35" width="140" class="logo">
|
||||
<app-svg-images name="officialMempoolSpace" class="logo" style="width: 140px; height: 35px; margin: 30px;" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
|
||||
|
||||
<div class="sponsor-page">
|
||||
|
||||
|
||||
@@ -124,10 +124,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 30px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<ng-container [ngSwitch]="name">
|
||||
<ng-container *ngSwitchCase="'officialMempoolSpace'">
|
||||
<svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M163.658 113.263C161.089 113.263 158.992 111.146 158.992 108.535C158.992 105.966 161.048 103.951 163.658 103.951C166.269 103.951 168.325 105.966 168.325 108.535C168.325 111.125 166.228 113.263 163.658 113.263Z" fill="#9857FF"/>
|
||||
<path d="M189.767 113.263C183.867 113.263 177.803 111.392 174.637 108.617L174 108.062L177.104 102.594L178.05 103.334C180.434 105.205 185.08 107.096 190.054 107.096C195.79 107.096 198.565 105.554 198.565 102.368C198.565 99.3461 195.194 98.5856 189.664 97.6194C183.312 96.5094 175.398 95.1115 175.398 86.6012C175.398 79.8999 181.359 75.5625 190.589 75.5625C195.584 75.5625 200.785 76.9192 203.807 79.0365L204.567 79.571L201.484 85.2034L200.559 84.5867C197.804 82.7572 194.269 81.7499 190.609 81.7499C185.368 81.7499 182.366 83.4767 182.366 86.4779C182.366 89.7052 185.84 90.548 191.576 91.5759C197.784 92.6859 205.513 94.0632 205.513 102.224C205.513 105.575 204.012 108.37 201.175 110.323C198.421 112.255 194.474 113.263 189.767 113.263Z" fill="#9857FF"/>
|
||||
<path d="M212.316 125.863V75.9733H218.997V80.8451C222.122 77.4945 226.541 75.7061 231.722 75.7061C236.984 75.7061 241.774 77.5356 245.186 80.8862C248.66 84.278 250.572 89.0882 250.572 94.4123C250.572 99.7569 248.66 104.588 245.206 108C241.774 111.392 237.005 113.242 231.742 113.242C226.624 113.242 222.327 111.536 219.162 108.247V125.843H212.316V125.863ZM231.434 81.8729C228.001 81.8729 224.876 83.1063 222.656 85.3264C220.354 87.6287 219.141 90.7738 219.141 94.4123C219.141 101.792 224.198 106.952 231.454 106.952C238.69 106.952 243.768 101.792 243.768 94.4123C243.768 90.7738 242.555 87.6287 240.252 85.3264C237.991 83.1063 234.867 81.8729 231.434 81.8729Z" fill="#9857FF"/>
|
||||
<path d="M269.194 113.262C260.643 113.262 255.113 108.884 255.113 102.1C255.113 98.8936 256.285 96.2212 258.526 94.3712C261.054 92.2744 264.878 91.2055 269.893 91.2055H280.233V90.1777C280.233 84.7714 277.17 82.0168 271.147 82.0168C266.995 82.0168 262.781 83.4558 259.882 85.8814L258.916 86.7037L255.956 81.1329L256.593 80.5985C260.417 77.4328 265.823 75.7061 271.846 75.7061C281.734 75.7061 287.181 80.8862 287.181 90.301V112.975H280.5V109.398C277.89 111.906 273.984 113.262 269.194 113.262ZM270.181 96.6735C262.925 96.6735 262.082 99.613 262.082 101.813C262.082 105.348 264.98 107.363 270.037 107.363H270.058C270.181 107.363 270.304 107.363 270.448 107.363C275.115 107.363 278.547 105.225 280.377 101.175V96.6735H270.181V96.6735Z" fill="#9857FF"/>
|
||||
<path d="M313.658 112.995C302.742 112.995 294.828 105.184 294.828 94.4123C294.828 89.1088 296.781 84.3191 300.337 80.9274C303.852 77.5561 308.786 75.7061 314.233 75.7061C320.77 75.7061 326.053 78.2961 329.137 83.0241L329.712 83.8875L324.347 87.3204L323.792 86.4776C321.777 83.4558 318.303 81.729 314.254 81.729C310.656 81.729 307.552 82.9213 305.291 85.1825C303.01 87.4643 301.797 90.6094 301.797 94.2684C301.797 97.9685 303.01 101.134 305.332 103.395C307.573 105.595 310.739 106.808 314.233 106.808C318.303 106.808 321.695 105.163 323.771 102.183L324.326 101.381L329.671 104.793L329.137 105.657C326.238 110.323 320.811 112.995 314.254 112.995C314.048 112.995 313.863 112.995 313.658 112.995Z" fill="#9857FF"/>
|
||||
<path d="M352.118 113.386C346.506 113.386 341.429 111.516 337.852 108.103C334.255 104.691 332.281 99.881 332.281 94.5363C332.281 83.7031 340.031 75.8301 350.7 75.8301C361.204 75.8301 368.83 83.5387 368.995 94.228C368.995 94.8447 368.995 96.9414 368.995 96.9414H339.373C340.36 103.047 345.396 107.076 352.241 107.076C356.414 107.076 360.032 105.596 362.417 102.903L363.177 102.039L367.206 106.603L366.61 107.281C363.218 111.228 358.059 113.386 352.118 113.386ZM362.335 91.3295C361.368 85.6354 356.805 81.7297 350.864 81.7297C344.964 81.7297 340.483 85.5327 339.414 91.3295H362.335Z" fill="#9857FF"/>
|
||||
<path d="M219.548 56.1979V33.8327C219.548 30.3587 218.746 27.6863 217.163 25.9185C215.601 24.1507 213.237 23.2667 210.195 23.2667C206.762 23.2667 203.946 24.3768 202.013 26.453C200.081 28.5497 199.053 31.6331 199.053 35.395V56.2185H191.447V33.8327C191.447 26.823 188.282 23.2667 182.032 23.2667C178.6 23.2667 175.783 24.3768 173.851 26.453C171.919 28.5497 170.891 31.6331 170.891 35.395V56.2185H163.285V16.4215H170.685V20.7589C173.687 17.7988 178.003 16.1748 182.999 16.1748C188.96 16.1748 193.667 18.4977 196.36 22.7528C199.608 18.5593 204.85 16.1748 210.955 16.1748C215.93 16.1748 219.877 17.6137 222.693 20.43C225.632 23.3901 227.174 27.8713 227.154 33.3599V56.1979H219.548V56.1979Z" fill="white"/>
|
||||
<path d="M255.397 56.5679C243.145 56.5679 234.594 48.2426 234.594 36.32C234.594 24.644 242.775 16.1748 254.04 16.1748C265.387 16.1748 273.342 24.459 273.383 36.32C273.383 36.4433 273.363 36.5872 273.363 36.7106C273.342 36.8339 273.342 36.9367 273.342 37.0395V37.1011L273.322 37.1628C273.281 37.3684 273.281 37.5945 273.281 37.8617V39.1568H242.343C243.412 45.4881 248.49 49.5377 255.5 49.5377C259.796 49.5377 263.393 47.996 265.901 45.0975L266.888 44.008L271.41 49.1677L270.732 50.0516C267.196 54.2451 261.749 56.5679 255.397 56.5679ZM265.798 32.846C265.284 30.0503 263.989 27.6658 262.036 25.9185C259.919 24.0273 257.144 23.0406 254.061 23.0406C250.998 23.0406 248.264 24.0273 246.146 25.9185C244.194 27.6658 242.919 30.0503 242.405 32.846H265.798Z" fill="white"/>
|
||||
<path d="M337.274 56.198V33.8327C337.274 30.3587 336.473 27.6864 334.89 25.9185C333.328 24.1507 330.964 23.2668 327.921 23.2668C324.488 23.2668 321.672 24.3768 319.74 26.453C317.808 28.5497 316.78 31.6332 316.78 35.395V56.2185H309.174V33.8327C309.174 26.823 306.008 23.2668 299.759 23.2668C296.326 23.2668 293.51 24.3768 291.578 26.453C289.645 28.5497 288.618 31.6332 288.618 35.395V56.2185H281.012V16.4215H288.412V20.7589C291.413 17.7782 295.73 16.1543 300.725 16.1543C306.687 16.1543 311.394 18.4772 314.087 22.7323C317.335 18.5388 322.577 16.1543 328.682 16.1543C333.656 16.1543 337.603 17.5932 340.419 20.4095C343.359 23.3696 344.901 27.8508 344.88 33.3394V56.1774H337.274V56.198Z" fill="white"/>
|
||||
<path d="M354.297 69.9296V16.4215H361.697V21.3139C364.986 17.9427 369.467 16.1543 374.75 16.1543C386.241 16.1543 394.567 24.6235 394.567 36.2995C394.567 48.0371 386.241 56.5474 374.75 56.5474C369.57 56.5474 365.171 54.8413 361.923 51.5728V69.909H354.297V69.9296ZM374.422 23.1023C367.021 23.1023 361.841 28.5292 361.841 36.32C361.841 44.1109 367.021 49.5377 374.422 49.5377C381.822 49.5377 387.002 44.1109 387.002 36.32C387.002 28.6525 381.719 23.1023 374.422 23.1023Z" fill="white"/>
|
||||
<path d="M420.321 56.5679C408.604 56.5679 400.094 48.0576 400.094 36.32C400.094 24.644 408.604 16.1748 420.321 16.1748C425.995 16.1748 431.134 18.2099 434.813 21.91C438.472 25.5896 440.466 30.7081 440.446 36.3405C440.446 48.0576 431.977 56.5679 420.321 56.5679ZM420.321 23.1023C412.921 23.1023 407.741 28.5291 407.741 36.32C407.741 44.1108 412.921 49.5377 420.321 49.5377C427.66 49.5377 432.799 44.1108 432.799 36.32C432.778 28.5291 427.66 23.1023 420.321 23.1023Z" fill="white"/>
|
||||
<path d="M464.598 56.5679C452.881 56.5679 444.371 48.0576 444.371 36.32C444.371 24.644 452.881 16.1748 464.598 16.1748C476.254 16.1748 484.723 24.644 484.723 36.32C484.723 48.0576 476.254 56.5679 464.598 56.5679ZM464.598 23.1023C457.198 23.1023 452.018 28.5291 452.018 36.32C452.018 44.1108 457.198 49.5377 464.598 49.5377C471.937 49.5377 477.076 44.1108 477.076 36.32C477.076 28.5291 471.958 23.1023 464.598 23.1023Z" fill="white"/>
|
||||
<path d="M499.996 1.14844H492.391V56.1982H499.996V1.14844Z" fill="white"/>
|
||||
<path d="M124.706 110.25C124.706 118.849 117.772 125.791 109.183 125.791H15.5236C6.93387 125.791 0 118.849 0 110.25V16.4837C0 7.88416 6.98561 0.942383 15.5236 0.942383H109.183C117.772 0.942383 124.706 7.88416 124.706 16.4837V110.25Z" fill="#2E3349"/>
|
||||
<path d="M0 63.5225V110.25C0 118.849 6.98561 125.791 15.5753 125.791H109.183C117.772 125.791 124.758 118.849 124.758 110.25V63.5225H0Z" [attr.fill]="'url(#paint0_linear' + randomId + ')'"/>
|
||||
<path opacity="0.3" d="M109.909 109.11C109.909 111.026 108.615 112.581 107.011 112.581H90.8665C89.2624 112.581 87.9688 111.026 87.9688 109.11V17.6232C87.9688 15.7065 89.2624 14.1523 90.8665 14.1523H107.011C108.615 14.1523 109.909 15.7065 109.909 17.6232V109.11Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient [id]="'paint0_linear' + randomId" x1="62.3768" y1="36.3949" x2="62.3768" y2="156.837" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#AE61FF"/>
|
||||
<stop offset="1" stop-color="#13EFD8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-svg-images',
|
||||
templateUrl: './svg-images.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SvgImagesComponent {
|
||||
randomId = Math.floor(Math.random() * 10000);
|
||||
@Input() name: string;
|
||||
@Input() class: string;
|
||||
@Input() style: string;
|
||||
@Input() width: string;
|
||||
@Input() height: string;
|
||||
@Input() viewBox: string;
|
||||
}
|
||||
@@ -66,13 +66,16 @@
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template #defaultAddress>
|
||||
<a [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
|
||||
<a *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
|
||||
<span class="d-block d-lg-none">{{ vin.prevout.scriptpubkey_address | shortenString : 16 }}</span>
|
||||
<span class="d-none d-lg-flex justify-content-start">
|
||||
<span class="addr-left flex-grow-1" [style]="vin.prevout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vin.prevout.scriptpubkey_address }}</span>
|
||||
<span *ngIf="vin.prevout.scriptpubkey_address.length > 40" class="addr-right">{{ vin.prevout.scriptpubkey_address | capAddress: 40: 10 }}</span>
|
||||
</span>
|
||||
</a>
|
||||
<ng-template #vinScriptPubkeyType>
|
||||
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
|
||||
</ng-template>
|
||||
<div>
|
||||
<app-address-labels [vin]="vin"></app-address-labels>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="col card-wrapper" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
|
||||
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-body less-padding">
|
||||
<app-fees-box class="d-block"></app-fees-box>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="col card-wrapper">
|
||||
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-body less-padding">
|
||||
<app-fees-box class="d-block"></app-fees-box>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -269,6 +269,9 @@
|
||||
justify-content: space-around;
|
||||
padding: 22px 20px;
|
||||
}
|
||||
.less-padding {
|
||||
padding: 20px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.retarget-sign {
|
||||
|
||||
@@ -2672,48 +2672,216 @@ export const restApiDocsData = [
|
||||
fragment: "get-blocks",
|
||||
title: "GET Blocks",
|
||||
description: {
|
||||
default: "Returns details on the past 10 blocks. If <code>:startHeight</code> is specified, the past 10 blocks before (and including) <code>:startHeight</code> are returned."
|
||||
default: "Returns details on the past 15 blocks with fee and mining details in an <code>extras</code> field. If <code>:startHeight</code> is specified, the past 15 blocks before (and including) <code>:startHeight</code> are returned."
|
||||
},
|
||||
urlString: "/blocks[/:startHeight]",
|
||||
showConditions: bitcoinNetworks.concat(liquidNetworks).concat(["bisq"]),
|
||||
urlString: "/v1/blocks[/:startHeight]",
|
||||
showConditions: bitcoinNetworks,
|
||||
showJsExamples: showJsExamplesDefault,
|
||||
codeExample: {
|
||||
bisq: {
|
||||
default: {
|
||||
codeTemplate: {
|
||||
curl: `/api/blocks/%{1}/%{2}`,
|
||||
curl: `/api/v1/blocks/%{1}`,
|
||||
commonJS: `
|
||||
const { %{0}: { blocks } } = mempoolJS();
|
||||
|
||||
const getBlocks = await blocks.getBlocks({ index: %{1}, length: %{2} });
|
||||
const getBlocks = await blocks.getBlocks({ startHeight: %{1} });
|
||||
|
||||
document.getElementById("result").textContent = JSON.stringify(getBlocks, undefined, 2);
|
||||
`,
|
||||
esModule: `
|
||||
const { %{0}: { blocks } } = mempoolJS();
|
||||
|
||||
const getBlocks = await blocks.getBlocks({ index: %{1}, length: %{2} });
|
||||
const getBlocks = await blocks.getBlocks({ startHeight: %{1} });
|
||||
console.log(getBlocks);
|
||||
`,
|
||||
},
|
||||
codeSampleMainnet: emptyCodeSample,
|
||||
codeSampleTestnet: emptyCodeSample,
|
||||
codeSampleSignet: emptyCodeSample,
|
||||
codeSampleLiquid: emptyCodeSample,
|
||||
codeSampleBisq: {
|
||||
esModule: ['0', '1'],
|
||||
commonJS: ['0', '1'],
|
||||
curl: ['0', '1'],
|
||||
codeSampleMainnet: {
|
||||
esModule: ['730000'],
|
||||
commonJS: ['730000'],
|
||||
curl: ['730000'],
|
||||
response: `[
|
||||
{
|
||||
height: 698771,
|
||||
time: 1630636953000,
|
||||
hash: "0000000000000000000a33c6ac863eee8c76ca72435f25d679609c0949ac9374",
|
||||
previousBlockHash: "00000000000000000001e4184639e5600d3fb4c4e06c2a625e76804c4bc93cb1",
|
||||
txs: []
|
||||
}
|
||||
"id": "0000000000000000000384f28cb3b9cf4377a39cfd6c29ae9466951de38c0529",
|
||||
"timestamp": 1648829449,
|
||||
"height": 730000,
|
||||
"version": 536870912,
|
||||
"bits": 386521239,
|
||||
"nonce": 3580664066,
|
||||
"difficulty": 28587155782195.14,
|
||||
"merkle_root": "efa344bcd6c0607f93b709515dd6dc5496178112d680338ebea459e3de7b4fbc",
|
||||
"tx_count": 1627,
|
||||
"size": 1210916,
|
||||
"weight": 3993515,
|
||||
"previousblockhash": "00000000000000000008b6f6fb83f8d74512ef1e0af29e642dd20daddd7d318f",
|
||||
"extras": {
|
||||
"coinbaseRaw": "0390230b1362696e616e63652f383038e0006f02cd583765fabe6d6d686355577affaad03015e732428a927a5d2d842471b350394139616bcb4401d804000000000000001a750000c9ad0000",
|
||||
"medianFee": 11,
|
||||
"feeRange": [
|
||||
1,
|
||||
11,
|
||||
11,
|
||||
11,
|
||||
18,
|
||||
21,
|
||||
660
|
||||
],
|
||||
"reward": 641321983,
|
||||
"totalFees": 16321983,
|
||||
"avgFee": 10038,
|
||||
"avgFeeRate": 16,
|
||||
"pool": {
|
||||
"id": 105,
|
||||
"name": "Binance Pool",
|
||||
"slug": "binancepool"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "00000000000000000008b6f6fb83f8d74512ef1e0af29e642dd20daddd7d318f",
|
||||
"timestamp": 1648828946,
|
||||
"height": 729999,
|
||||
"version": 793796608,
|
||||
"bits": 386521239,
|
||||
"nonce": 3477019455,
|
||||
"difficulty": 28587155782195.14,
|
||||
"merkle_root": "d84f9cc1823bd069c505061b1f6faabd809d67ab5354e9f6234312dc4bdb1ecf",
|
||||
"tx_count": 2574,
|
||||
"size": 1481957,
|
||||
"weight": 3993485,
|
||||
"previousblockhash": "000000000000000000071e6c86c2175aa86817cae2a77acd95372b55c1103d89",
|
||||
"extras": {
|
||||
"coinbaseRaw": "038f230b1362696e616e63652f373739d8002900ca5de7a9fabe6d6dda31112c36c10a523154eae76847579755cd4ae558ee2e6f9f200b05dd32a0bf04000000000000006372000000020000",
|
||||
"medianFee": 17,
|
||||
"feeRange": [
|
||||
2,
|
||||
11,
|
||||
14,
|
||||
17,
|
||||
19,
|
||||
28,
|
||||
502
|
||||
],
|
||||
"reward": 649090210,
|
||||
"totalFees": 24090210,
|
||||
"avgFee": 9362,
|
||||
"avgFeeRate": 24,
|
||||
"pool": {
|
||||
"id": 105,
|
||||
"name": "Binance Pool",
|
||||
"slug": "binancepool"
|
||||
}
|
||||
}
|
||||
},
|
||||
...
|
||||
]`,
|
||||
},
|
||||
codeSampleTestnet: {
|
||||
esModule: ['2091187'],
|
||||
commonJS: ['2091187'],
|
||||
curl: ['2091187'],
|
||||
response: `[
|
||||
{
|
||||
"id": "00000000000000533f63df886281a9fd74da163e84a21445153ff480e5f57970",
|
||||
"timestamp": 1630641890,
|
||||
"height": 2091187,
|
||||
"version": 545259520,
|
||||
"bits": 436273151,
|
||||
"nonce": 309403673,
|
||||
"difficulty": 16777216,
|
||||
"merkle_root": "4d6df12a4af11bb928c7b2930e0a4d2c3e268c6dc6a07462943ad1c4b6b96468",
|
||||
"tx_count": 26,
|
||||
"size": 8390,
|
||||
"weight": 22985,
|
||||
"previousblockhash": "0000000000000079103da7d296e1480295df795b7379e7dffd27743e214b0b32",
|
||||
"extras": {
|
||||
"coinbaseRaw": "03b3e81f3a205468697320626c6f636b20776173206d696e65642077697468206120636172626f6e206e6567617469766520706f77657220736f75726365201209687a2009092009020de601d7986a040000",
|
||||
"medianFee": 1,
|
||||
"feeRange": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
5,
|
||||
56,
|
||||
5053
|
||||
],
|
||||
"reward": 10547567,
|
||||
"totalFees": 781942,
|
||||
"avgFee": 31277,
|
||||
"avgFeeRate": 143,
|
||||
"pool": {
|
||||
"id": 137,
|
||||
"name": "Unknown",
|
||||
"slug": "unknown"
|
||||
}
|
||||
}
|
||||
},
|
||||
...
|
||||
]`
|
||||
},
|
||||
},
|
||||
codeSampleSignet: {
|
||||
esModule: ['53783'],
|
||||
commonJS: ['53783'],
|
||||
curl: ['53783'],
|
||||
response: `[
|
||||
{
|
||||
"id": "0000010eeacb878340bae34af4e13551413d76a172ec302f7e50b62cb45374f2",
|
||||
"timestamp": 1630641504,
|
||||
"height": 53783,
|
||||
"version": 536870912,
|
||||
"bits": 503404179,
|
||||
"nonce": 11753379,
|
||||
"difficulty": 0.002919030932507782,
|
||||
"merkle_root": "3063ff3802c920eea68bdc9303957f3e7bfd0a03c93547fd7dad14b77a07d4e8",
|
||||
"tx_count": 1,
|
||||
"size": 343,
|
||||
"weight": 1264,
|
||||
"previousblockhash": "00000109a7ea774fcc2d173f9a1da9595a47ff401dac67ca9edea149954210fa",
|
||||
"extras": {
|
||||
"coinbaseRaw": "0317d200",
|
||||
"medianFee": 0,
|
||||
"feeRange": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"reward": 5000000000,
|
||||
"totalFees": 0,
|
||||
"avgFee": 0,
|
||||
"avgFeeRate": 0,
|
||||
"pool": {
|
||||
"id": 137,
|
||||
"name": "Unknown",
|
||||
"slug": "unknown"
|
||||
}
|
||||
}
|
||||
},
|
||||
...
|
||||
]`
|
||||
},
|
||||
codeSampleLiquid: emptyCodeSample,
|
||||
codeSampleLiquidTestnet: emptyCodeSample,
|
||||
codeSampleBisq: emptyCodeSample,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "blocks",
|
||||
httpRequestMethod: "GET",
|
||||
fragment: "get-blocks",
|
||||
title: "GET Blocks",
|
||||
description: {
|
||||
default: "Returns details on the past 10 blocks with fee and mining details in an <code>extras</code> field. If <code>:startHeight</code> is specified, the past 10 blocks before (and including) <code>:startHeight</code> are returned."
|
||||
},
|
||||
urlString: "/blocks[/:startHeight]",
|
||||
showConditions: liquidNetworks,
|
||||
showJsExamples: showJsExamplesDefault,
|
||||
codeExample: {
|
||||
default: {
|
||||
codeTemplate: {
|
||||
curl: `/api/blocks/%{1}`,
|
||||
@@ -2731,75 +2899,9 @@ export const restApiDocsData = [
|
||||
console.log(getBlocks);
|
||||
`,
|
||||
},
|
||||
codeSampleMainnet: {
|
||||
esModule: ['698777'],
|
||||
commonJS: ['698777'],
|
||||
curl: ['698777'],
|
||||
response: `[
|
||||
{
|
||||
id: "00000000000000000003002915e015c47610c55b6f0228ad62bfcc59b65e67b7",
|
||||
height: 698777,
|
||||
version: 536870916,
|
||||
timestamp: 1630641711,
|
||||
tx_count: 2327,
|
||||
size: 1466537,
|
||||
weight: 3999653,
|
||||
merkle_root: "023e27dde144eedc65ff3b27c535ebc7dced6c49fe78f94cdf85cf2000608d2f",
|
||||
previousblockhash: "0000000000000000000701a7f14e362d3f10aa524200db1710ce3bbf0c0f8b75",
|
||||
mediantime: 1630636986,
|
||||
nonce: 1926094388,
|
||||
bits: 386923168,
|
||||
difficulty: 17615033039278
|
||||
},
|
||||
...
|
||||
]`
|
||||
},
|
||||
codeSampleTestnet: {
|
||||
esModule: ['2091187'],
|
||||
commonJS: ['2091187'],
|
||||
curl: ['2091187'],
|
||||
response: `[
|
||||
{
|
||||
id: "00000000000000533f63df886281a9fd74da163e84a21445153ff480e5f57970",
|
||||
height: 2091187,
|
||||
version: 545259520,
|
||||
timestamp: 1630641890,
|
||||
tx_count: 26,
|
||||
size: 8390,
|
||||
weight: 22985,
|
||||
merkle_root: "4d6df12a4af11bb928c7b2930e0a4d2c3e268c6dc6a07462943ad1c4b6b96468",
|
||||
previousblockhash: "0000000000000079103da7d296e1480295df795b7379e7dffd27743e214b0b32",
|
||||
mediantime: 1630639627,
|
||||
nonce: 309403673,
|
||||
bits: 436273151,
|
||||
difficulty: 16777216
|
||||
},
|
||||
...
|
||||
]`
|
||||
},
|
||||
codeSampleSignet: {
|
||||
esModule: ['53783'],
|
||||
commonJS: ['53783'],
|
||||
curl: ['53783'],
|
||||
response: `[
|
||||
{
|
||||
id: "0000010eeacb878340bae34af4e13551413d76a172ec302f7e50b62cb45374f2",
|
||||
height: 53783,
|
||||
version: 536870912,
|
||||
timestamp: 1630641504,
|
||||
tx_count: 1,
|
||||
size: 343,
|
||||
weight: 1264,
|
||||
merkle_root: "3063ff3802c920eea68bdc9303957f3e7bfd0a03c93547fd7dad14b77a07d4e8",
|
||||
previousblockhash: "00000109a7ea774fcc2d173f9a1da9595a47ff401dac67ca9edea149954210fa",
|
||||
mediantime: 1630638966,
|
||||
nonce: 11753379,
|
||||
bits: 503404179,
|
||||
difficulty: 0
|
||||
},
|
||||
...
|
||||
]`
|
||||
},
|
||||
codeSampleMainnet: emptyCodeSample,
|
||||
codeSampleTestnet: emptyCodeSample,
|
||||
codeSampleSignet: emptyCodeSample,
|
||||
codeSampleLiquid: {
|
||||
esModule: ['1472246'],
|
||||
commonJS: ['1472246'],
|
||||
@@ -2848,144 +2950,79 @@ export const restApiDocsData = [
|
||||
type: "endpoint",
|
||||
category: "blocks",
|
||||
httpRequestMethod: "GET",
|
||||
fragment: "get-blocks-extras",
|
||||
title: "GET Blocks Extras",
|
||||
fragment: "get-blocks",
|
||||
title: "GET Blocks",
|
||||
description: {
|
||||
default: "Returns details on the past 15 blocks with fee and mining details in an <code>extras</code> field. If <code>:startHeight</code> is specified, the past 15 blocks before (and including) <code>:startHeight</code> are returned."
|
||||
default: "<p>Returns the past <code>n</code> blocks with BSQ transactions starting <code>m</code> blocks ago.</p><p>Assume a block height of 700,000. Query <code>/blocks/0/10</code> for the past 10 blocks before 700,000 with BSQ transactions. Query <code>/blocks/1000/10</code> for the past 10 blocks before 699,000 with BSQ transactions."
|
||||
},
|
||||
urlString: "/blocks-extras[/:startHeight]",
|
||||
showConditions: bitcoinNetworks,
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
urlString: "/blocks/:m/:n",
|
||||
showConditions: ["bisq"],
|
||||
showJsExamples: showJsExamplesDefault,
|
||||
codeExample: {
|
||||
default: {
|
||||
codeTemplate: {
|
||||
curl: `/api/blocks-extras/%{1}`,
|
||||
commonJS: ``,
|
||||
esModule: ``
|
||||
},
|
||||
codeSampleMainnet: {
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: ['736915'],
|
||||
response: `[
|
||||
{
|
||||
"extras": {
|
||||
"reward": 629766074,
|
||||
"coinbaseTx": {
|
||||
"vin": [
|
||||
{
|
||||
"scriptsig": "03933e0b215c204d41524120506f6f6c205c00000000be82a250e5ef942790d2542ca87d0000"
|
||||
}
|
||||
],
|
||||
"vout": [
|
||||
{
|
||||
"scriptpubkey_address": "1A32KFEX7JNPmU1PVjrtiXRrTQcesT3Nf1",
|
||||
"value": 629766074
|
||||
}
|
||||
]
|
||||
},
|
||||
"coinbaseRaw": "03933e0b215c204d41524120506f6f6c205c00000000be82a250e5ef942790d2542ca87d0000",
|
||||
"medianFee": 14,
|
||||
"feeRange": [
|
||||
1,
|
||||
1,
|
||||
4,
|
||||
14,
|
||||
15,
|
||||
20,
|
||||
364
|
||||
],
|
||||
"totalFees": 4766074,
|
||||
"avgFee": 5043,
|
||||
"avgFeeRate": 14,
|
||||
"pool": {
|
||||
"id": 115,
|
||||
"name": "MARA Pool",
|
||||
"slug": "marapool"
|
||||
},
|
||||
"matchRate": 100
|
||||
},
|
||||
"id": "00000000000000000000a742ae476dbe2a58c48b193484945c52b05967f2d74c",
|
||||
"height": 736915,
|
||||
"version": 541065216,
|
||||
"timestamp": 1652877171,
|
||||
"bits": 386466234,
|
||||
"nonce": 4069175824,
|
||||
"difficulty": 31251101365711.12,
|
||||
"merkle_root": "de54fd1adee9f010534e8efbf1244a01528e20dd283c8927026f5442c3e03459",
|
||||
"tx_count": 946,
|
||||
"size": 524907,
|
||||
"weight": 1362339,
|
||||
"previousblockhash": "000000000000000000070760a253405ca69498464d9f8e9fab2452cbbfc10cbe"
|
||||
},
|
||||
{
|
||||
"extras": {
|
||||
"reward": 638804415,
|
||||
"coinbaseTx": {
|
||||
"vin": [
|
||||
{
|
||||
"scriptsig": "03923e0bfabe6d6dc3e96cee3cb68ee52bd31fde8e1f4983a780ea836115788d81a559e03791071f01000000000000001065040008d708c7010000000000007a6d4683012f736c7573682f"
|
||||
}
|
||||
],
|
||||
"vout": [
|
||||
{
|
||||
"scriptpubkey_address": "1CK6KHY6MHgYvmRQ4PAafKYDrg1ejbH1cE",
|
||||
"value": 638804415
|
||||
}
|
||||
]
|
||||
},
|
||||
"coinbaseRaw": "03923e0bfabe6d6dc3e96cee3cb68ee52bd31fde8e1f4983a780ea836115788d81a559e03791071f01000000000000001065040008d708c7010000000000007a6d4683012f736c7573682f",
|
||||
"medianFee": 14,
|
||||
"feeRange": [
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
14,
|
||||
15,
|
||||
20,
|
||||
347
|
||||
],
|
||||
"totalFees": 13804415,
|
||||
"avgFee": 5287,
|
||||
"avgFeeRate": 14,
|
||||
"pool": {
|
||||
"id": 43,
|
||||
"name": "SlushPool",
|
||||
"slug": "slushpool"
|
||||
},
|
||||
"matchRate": 100
|
||||
},
|
||||
"id": "000000000000000000070760a253405ca69498464d9f8e9fab2452cbbfc10cbe",
|
||||
"height": 736914,
|
||||
"version": 555696132,
|
||||
"timestamp": 1652876939,
|
||||
"bits": 386466234,
|
||||
"nonce": 3839610443,
|
||||
"difficulty": 31251101365711.12,
|
||||
"merkle_root": "dc6d15f641e7af26dbaf3ee37203155f8053a8755e85f4955d11ea0c54008b16",
|
||||
"tx_count": 2612,
|
||||
"size": 1450209,
|
||||
"weight": 3931749,
|
||||
"previousblockhash": "00000000000000000002b5b2afc1c62e61e53f966b965a9a8ce99112e24066ae"
|
||||
},
|
||||
...
|
||||
]`,
|
||||
},
|
||||
codeSampleTestnet: {
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: ['2226118'],
|
||||
response: `[]`
|
||||
},
|
||||
codeSampleSignet: {
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: ['88832'],
|
||||
response: `[]`
|
||||
curl: `/api/blocks/%{1}/%{2}`,
|
||||
commonJS: `
|
||||
const { %{0}: { blocks } } = mempoolJS();
|
||||
|
||||
const getBlocks = await blocks.getBlocks({ index: %{1}, length: %{2} });
|
||||
|
||||
document.getElementById("result").textContent = JSON.stringify(getBlocks, undefined, 2);
|
||||
`,
|
||||
esModule: `
|
||||
const { %{0}: { blocks } } = mempoolJS();
|
||||
|
||||
const getBlocks = await blocks.getBlocks({ index: %{1}, length: %{2} });
|
||||
console.log(getBlocks);
|
||||
`,
|
||||
},
|
||||
codeSampleMainnet: emptyCodeSample,
|
||||
codeSampleTestnet: emptyCodeSample,
|
||||
codeSampleSignet: emptyCodeSample,
|
||||
codeSampleLiquid: emptyCodeSample,
|
||||
codeSampleLiquidTestnet: emptyCodeSample,
|
||||
codeSampleBisq: emptyCodeSample,
|
||||
codeSampleBisq: {
|
||||
esModule: ['0', '5'],
|
||||
commonJS: ['0', '5'],
|
||||
curl: ['0', '5'],
|
||||
response: `[
|
||||
{
|
||||
"height": 739030,
|
||||
"time": 1654203258000,
|
||||
"hash": "000000000000000000036bc04416ddeec264cbb977a9cd9e454897acb547b601",
|
||||
"previousBlockHash": "00000000000000000000f49261617b589d76e5e70529ea1d4c16f3e19ddcb8ef",
|
||||
"txs": [ ... ],
|
||||
},
|
||||
{
|
||||
"height": 739029,
|
||||
"time": 1654203236000,
|
||||
"hash": "00000000000000000000f49261617b589d76e5e70529ea1d4c16f3e19ddcb8ef",
|
||||
"previousBlockHash": "00000000000000000008dd87e9486cd0d71c5d84e452432bab33c2a0cbaa31ce",
|
||||
"txs": [ ... ],
|
||||
},
|
||||
{
|
||||
"height": 739025,
|
||||
"time": 1654199569000,
|
||||
"hash": "000000000000000000021e9ce82dec208af75807f92a9b1d9dae91f2b4d40e24",
|
||||
"previousBlockHash": "00000000000000000002db644c025a76464b466d25900402452b07213b30c40b",
|
||||
"txs": [ ... ]
|
||||
},
|
||||
{
|
||||
"height": 739023,
|
||||
"time": 1654198597000,
|
||||
"hash": "0000000000000000000702ce10250a46bea4155ca7acb869f3ea92c1e3a68bc5",
|
||||
"previousBlockHash": "00000000000000000002b3d6c1adc5676262ded84181982f88dbd357b9f9d1ec",
|
||||
"txs": [ ... ]
|
||||
},
|
||||
{
|
||||
"height": 739020,
|
||||
"time": 1654197263000,
|
||||
"hash": "000000000000000000046eb46ad941028381d3534c35658f9c80de0641dbbb31",
|
||||
"previousBlockHash": "000000000000000000073f1c49b4c4895f3fa6b866d1e21ab8b22f3f9318b42f",
|
||||
"txs": [ ... ]
|
||||
}
|
||||
]`
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6070,7 +6107,7 @@ export const faqData = [
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "looking-up-fee-estimates",
|
||||
title: "How can I look up fee estimates?",
|
||||
answer: "<p>See real-time fee estimates on <a href='/'>the main dashboard</a>.</p><p>Low priority is suggested for confirmation within 6 blocks (~1 hour), Medium priority is suggested for confirmation within 3 blocks (~30 minutes), and High priority is suggested for confirmation in the next block (~10 minutes).</p>"
|
||||
answer: "<p>See real-time fee estimates on <a href='/'>the main dashboard</a>.</p><p>Here is an overview of Mempool's feerate suggestions:</p><ul> <li><b>High Priority.</b> This figure is the median feerate of transactions in the <a href='/mempool-block/0'>first mempool block</a>. Consider using this feerate if you want confirmation as soon as possible.</li><li><b>Medium Priority.</b> This figure is the average of the median feerate of the <a href='/mempool-block/0'>first mempool block</a> and the median feerate of the <a href='/mempool-block/1'>second mempool block</a>.</li><li><b>Low Priority.</b> This figure is the average of the Medium Priority feerate and the median feerate of the <a href='/mempool-block/2'>third mempool block</a>. Consider using this feerate if you want confirmation soon but don't need it particularly quickly.</li><li><b>No Priority.</b> This figure is either 2x the minimum feerate, or the Low Priority feerate (whichever is lower). Consider using this feerate if you are in no rush and don't mind if confirmation takes a while.</li></ul><p>In all cases, the suggested feerate is adjusted lower if any of the mempool blocks involved in the calculation are not full (example: if there is only 1 mempool block that's less than half-full, Mempool will suggest a feerate of 1 sat/vB—not the median feerate of transactions in the block).</p><p>Mempool blocks use feerates, transaction sizes, and other metrics to <b>forecast</b> which transactions will be in future blocks. Actual blocks will turn out to be different: miners have their own views of the mempool, their own algorithms for determining which transactions to include in a block, etc.</p><p>Ultimately, the Bitcoin network is not perfectly predictable, so fee estimation cannot be perfectly precise.</p><p><b>Use Mempool's feerate suggestions as a guide, and understand that they do not guarantee transaction confirmation in any period of time.</b></p>"
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
|
||||
@@ -14,6 +14,7 @@ import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs-
|
||||
import { GraphsComponent } from '../components/graphs/graphs.component';
|
||||
import { StatisticsComponent } from '../components/statistics/statistics.component';
|
||||
import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component';
|
||||
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
|
||||
import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component';
|
||||
import { PoolComponent } from '../components/pool/pool.component';
|
||||
import { TelevisionComponent } from '../components/television/television.component';
|
||||
@@ -40,6 +41,7 @@ import { CommonModule } from '@angular/common';
|
||||
BlockFeeRatesGraphComponent,
|
||||
BlockSizesWeightsGraphComponent,
|
||||
FeeDistributionGraphComponent,
|
||||
MempoolBlockOverviewComponent,
|
||||
IncomingTransactionsGraphComponent,
|
||||
MempoolGraphComponent,
|
||||
LbtcPegsGraphComponent,
|
||||
|
||||
@@ -21,9 +21,11 @@ export interface WebsocketResponse {
|
||||
loadingIndicators?: ILoadingIndicators;
|
||||
backendInfo?: IBackendInfo;
|
||||
da?: DifficultyAdjustment;
|
||||
fees?: Recommendedfees;
|
||||
'track-tx'?: string;
|
||||
'track-address'?: string;
|
||||
'track-asset'?: string;
|
||||
'track-mempool-block'?: number;
|
||||
'watch-mempool'?: boolean;
|
||||
'track-bisq-market'?: string;
|
||||
}
|
||||
@@ -43,6 +45,16 @@ export interface MempoolBlock {
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface MempoolBlockWithTransactions extends MempoolBlock {
|
||||
transactionIds: string[];
|
||||
transactions: TransactionStripped[];
|
||||
}
|
||||
|
||||
export interface MempoolBlockDelta {
|
||||
added: TransactionStripped[],
|
||||
removed: string[],
|
||||
}
|
||||
|
||||
export interface MempoolInfo {
|
||||
loaded: boolean; // (boolean) True if the mempool is fully loaded
|
||||
size: number; // (numeric) Current tx count
|
||||
@@ -65,3 +77,11 @@ export interface IBackendInfo {
|
||||
gitCommit: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface Recommendedfees {
|
||||
fastestFee: number;
|
||||
halfHourFee: number;
|
||||
hourFee: number;
|
||||
minimumFee: number;
|
||||
economyFee: number;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
|
||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { IBackendInfo, MempoolBlock, MempoolInfo, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
|
||||
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
|
||||
import { BlockExtended, DifficultyAdjustment, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
import { Router, NavigationStart } from '@angular/router';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
@@ -80,6 +80,8 @@ export class StateService {
|
||||
bsqPrice$ = new ReplaySubject<number>(1);
|
||||
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
|
||||
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
|
||||
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
|
||||
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
|
||||
txReplaced$ = new Subject<ReplacedTransaction>();
|
||||
utxoSpent$ = new Subject<object>();
|
||||
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
|
||||
@@ -90,6 +92,7 @@ export class StateService {
|
||||
previousRetarget$ = new ReplaySubject<number>(1);
|
||||
backendInfo$ = new ReplaySubject<IBackendInfo>(1);
|
||||
loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1);
|
||||
recommendedFees$ = new ReplaySubject<Recommendedfees>(1);
|
||||
|
||||
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ export class WebsocketService {
|
||||
private lastWant: string | null = null;
|
||||
private isTrackingTx = false;
|
||||
private trackingTxId: string;
|
||||
private isTrackingMempoolBlock = false;
|
||||
private trackingMempoolBlock: number;
|
||||
private latestGitCommit = '';
|
||||
private onlineCheckTimeout: number;
|
||||
private onlineCheckTimeoutTwo: number;
|
||||
@@ -102,6 +104,9 @@ export class WebsocketService {
|
||||
if (this.isTrackingTx) {
|
||||
this.startMultiTrackTransaction(this.trackingTxId);
|
||||
}
|
||||
if (this.isTrackingMempoolBlock) {
|
||||
this.startTrackMempoolBlock(this.trackingMempoolBlock);
|
||||
}
|
||||
this.stateService.connectionState$.next(2);
|
||||
}
|
||||
|
||||
@@ -157,6 +162,17 @@ export class WebsocketService {
|
||||
this.websocketSubject.next({ 'track-asset': 'stop' });
|
||||
}
|
||||
|
||||
startTrackMempoolBlock(block: number) {
|
||||
this.websocketSubject.next({ 'track-mempool-block': block });
|
||||
this.isTrackingMempoolBlock = true
|
||||
this.trackingMempoolBlock = block
|
||||
}
|
||||
|
||||
stopTrackMempoolBlock() {
|
||||
this.websocketSubject.next({ 'track-mempool-block': -1 });
|
||||
this.isTrackingMempoolBlock = false
|
||||
}
|
||||
|
||||
startTrackBisqMarket(market: string) {
|
||||
this.websocketSubject.next({ 'track-bisq-market': market });
|
||||
}
|
||||
@@ -263,6 +279,10 @@ export class WebsocketService {
|
||||
this.stateService.difficultyAdjustment$.next(response.da);
|
||||
}
|
||||
|
||||
if (response.fees) {
|
||||
this.stateService.recommendedFees$.next(response.fees);
|
||||
}
|
||||
|
||||
if (response.backendInfo) {
|
||||
this.stateService.backendInfo$.next(response.backendInfo);
|
||||
|
||||
@@ -289,6 +309,16 @@ export class WebsocketService {
|
||||
});
|
||||
}
|
||||
|
||||
if (response['projected-block-transactions']) {
|
||||
if (response['projected-block-transactions'].index == this.trackingMempoolBlock) {
|
||||
if (response['projected-block-transactions'].blockTransactions) {
|
||||
this.stateService.mempoolBlockTransactions$.next(response['projected-block-transactions'].blockTransactions);
|
||||
} else if (response['projected-block-transactions'].delta) {
|
||||
this.stateService.mempoolBlockDelta$.next(response['projected-block-transactions'].delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (response['live-2h-chart']) {
|
||||
this.stateService.live2Chart$.next(response['live-2h-chart']);
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ import { RewardStatsComponent } from '../components/reward-stats/reward-stats.co
|
||||
import { DataCyDirective } from '../data-cy.directive';
|
||||
import { LoadingIndicatorComponent } from '../components/loading-indicator/loading-indicator.component';
|
||||
import { IndexingProgressComponent } from '../components/indexing-progress/indexing-progress.component';
|
||||
import { SvgImagesComponent } from '../components/svg-images/svg-images.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -136,6 +137,7 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index
|
||||
RewardStatsComponent,
|
||||
LoadingIndicatorComponent,
|
||||
IndexingProgressComponent,
|
||||
SvgImagesComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -228,6 +230,7 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index
|
||||
RewardStatsComponent,
|
||||
LoadingIndicatorComponent,
|
||||
IndexingProgressComponent,
|
||||
SvgImagesComponent,
|
||||
]
|
||||
})
|
||||
export class SharedModule {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user