Compare commits
4 Commits
orangesurf
...
mononaut/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7569c6a394 | ||
|
|
d67285c683 | ||
|
|
2b7ac32c22 | ||
|
|
df596ab5bf |
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["18", "20"]
|
||||
node: ["16", "17", "18", "20"]
|
||||
flavor: ["dev", "prod"]
|
||||
fail-fast: false
|
||||
runs-on: "ubuntu-latest"
|
||||
@@ -27,17 +27,8 @@ jobs:
|
||||
node-version: ${{ matrix.node }}
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Read rust-toolchain file from repository
|
||||
id: gettoolchain
|
||||
run: echo "::set-output name=toolchain::$(cat rust-toolchain)"
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}
|
||||
|
||||
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
|
||||
# Latest version available on this commit is 1.71.1
|
||||
# Commit date is Aug 3, 2023
|
||||
uses: dtolnay/rust-toolchain@f361669954a8ecfc00a3443f35f9ac8e610ffc06
|
||||
with:
|
||||
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
|
||||
- name: Install 1.63.x Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@1.63
|
||||
|
||||
- name: Install
|
||||
if: ${{ matrix.flavor == 'dev'}}
|
||||
@@ -67,7 +58,7 @@ jobs:
|
||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["18", "20"]
|
||||
node: ["16", "17", "18", "20"]
|
||||
flavor: ["dev", "prod"]
|
||||
fail-fast: false
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
2
.github/workflows/cypress.yml
vendored
2
.github/workflows/cypress.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 18
|
||||
cache: "npm"
|
||||
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
|
||||
|
||||
|
||||
19
.github/workflows/get_backend_hash.yml
vendored
19
.github/workflows/get_backend_hash.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: 'Print backend hashes'
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
print-backend-sha:
|
||||
runs-on: 'ubuntu-latest'
|
||||
name: Print backend hashes
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: repo
|
||||
|
||||
- name: Run script
|
||||
working-directory: repo
|
||||
run: |
|
||||
chmod +x ./scripts/get_backend_hash.sh
|
||||
sh ./scripts/get_backend_hash.sh
|
||||
8
.github/workflows/on-tag.yml
vendored
8
.github/workflows/on-tag.yml
vendored
@@ -68,17 +68,17 @@ jobs:
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Init repo for Dockerization
|
||||
run: docker/init.sh "$TAG"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v2
|
||||
id: qemu
|
||||
|
||||
- name: Setup Docker buildx action
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v2
|
||||
id: buildx
|
||||
|
||||
- name: Available platforms
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
|
||||
--output "type=registry" ./${{ matrix.service }}/ \
|
||||
|
||||
47
GNUmakefile
Executable file
47
GNUmakefile
Executable file
@@ -0,0 +1,47 @@
|
||||
# If you see pwd_unknown showing up check permissions
|
||||
PWD ?= pwd_unknown
|
||||
|
||||
# DATABASE DEPLOY FOLDER CONFIG - default ./data
|
||||
ifeq ($(data),)
|
||||
DATA := data
|
||||
export DATA
|
||||
else
|
||||
DATA := $(data)
|
||||
export DATA
|
||||
endif
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo ''
|
||||
@echo ''
|
||||
@echo ' Usage: make [COMMAND]'
|
||||
@echo ''
|
||||
@echo ' make all # build init mempool and electrs'
|
||||
@echo ' make init # setup some useful configs'
|
||||
@echo ' make mempool # build q dockerized mempool.space'
|
||||
@echo ' make electrs # build a docker electrs image'
|
||||
@echo ''
|
||||
|
||||
.PHONY: init
|
||||
init:
|
||||
@echo ''
|
||||
mkdir -p $(DATA) $(DATA)/mysql $(DATA)/mysql/data
|
||||
#REF: https://github.com/mempool/mempool/blob/master/docker/README.md
|
||||
cat docker/docker-compose.yml > docker-compose.yml
|
||||
cat backend/mempool-config.sample.json > backend/mempool-config.json
|
||||
.PHONY: mempool
|
||||
mempool: init
|
||||
@echo ''
|
||||
docker-compose up --force-recreate --always-recreate-deps
|
||||
@echo ''
|
||||
.PHONY: electrs
|
||||
electrum:
|
||||
#REF: https://hub.docker.com/r/beli/electrum
|
||||
@echo ''
|
||||
docker build -f docker/electrum/Dockerfile .
|
||||
@echo ''
|
||||
.PHONY: all
|
||||
all: init
|
||||
make mempool
|
||||
#######################
|
||||
-include Makefile
|
||||
@@ -23,7 +23,6 @@ Mempool can be conveniently installed on the following full-node distros:
|
||||
- [RoninDojo](https://code.samourai.io/ronindojo/RoninDojo)
|
||||
- [myNode](https://github.com/mynodebtc/mynode)
|
||||
- [Start9](https://github.com/Start9Labs/embassy-os)
|
||||
- [nix-bitcoin](https://github.com/fort-nix/nix-bitcoin/blob/a1eacce6768ca4894f365af8f79be5bbd594e1c3/examples/configuration.nix#L129)
|
||||
|
||||
**We highly recommend you deploy your own Mempool instance this way.** No matter which option you pick, you'll be able to get your own fully-sovereign instance of Mempool up quickly without needing to fiddle with any settings.
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ Install dependencies with `npm` and build the backend:
|
||||
|
||||
```
|
||||
cd backend
|
||||
npm install --no-install-links # npm@9.4.2 and later can omit the --no-install-links
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
|
||||
@@ -40,9 +40,7 @@
|
||||
"PORT": 8332,
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool",
|
||||
"TIMEOUT": 60000,
|
||||
"COOKIE": false,
|
||||
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
|
||||
"TIMEOUT": 60000
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "127.0.0.1",
|
||||
@@ -52,10 +50,7 @@
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "http://127.0.0.1:3000",
|
||||
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
|
||||
"BATCH_QUERY_BASE_SIZE": 1000,
|
||||
"RETRY_UNIX_SOCKET_AFTER": 30000,
|
||||
"REQUEST_TIMEOUT": 10000,
|
||||
"FALLBACK_TIMEOUT": 5000,
|
||||
"FALLBACK": []
|
||||
},
|
||||
"SECOND_CORE_RPC": {
|
||||
@@ -63,9 +58,7 @@
|
||||
"PORT": 8332,
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool",
|
||||
"TIMEOUT": 60000,
|
||||
"COOKIE": false,
|
||||
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
|
||||
"TIMEOUT": 60000
|
||||
},
|
||||
"DATABASE": {
|
||||
"ENABLED": true,
|
||||
@@ -75,8 +68,7 @@
|
||||
"DATABASE": "mempool",
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool",
|
||||
"TIMEOUT": 180000,
|
||||
"PID_DIR": ""
|
||||
"TIMEOUT": 180000
|
||||
},
|
||||
"SYSLOG": {
|
||||
"ENABLED": true,
|
||||
@@ -133,11 +125,6 @@
|
||||
"BISQ_URL": "https://bisq.markets/api",
|
||||
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": false,
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
|
||||
"BATCH_QUERY_BASE_SIZE": 5000
|
||||
},
|
||||
"REPLICATION": {
|
||||
"ENABLED": false,
|
||||
"AUDIT": false,
|
||||
|
||||
656
backend/package-lock.json
generated
656
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -38,12 +38,12 @@
|
||||
"rust-build": "cd rust-gbt && npm run build-release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/core": "^7.21.3",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.6.1",
|
||||
"axios": "~1.4.0",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.2.0",
|
||||
"crypto-js": "~4.1.1",
|
||||
"express": "~4.18.2",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.6.0",
|
||||
@@ -55,7 +55,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/core": "^7.21.3",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/express": "^4.17.17",
|
||||
|
||||
@@ -6,6 +6,8 @@ authors = ["mononaut"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[workspace]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
|
||||
@@ -41,9 +41,7 @@
|
||||
"PORT": 15,
|
||||
"USERNAME": "__CORE_RPC_USERNAME__",
|
||||
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": 1000,
|
||||
"COOKIE": false,
|
||||
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
|
||||
"TIMEOUT": 1000
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "__ELECTRUM_HOST__",
|
||||
@@ -53,10 +51,7 @@
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "__ESPLORA_REST_API_URL__",
|
||||
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
|
||||
"BATCH_QUERY_BASE_SIZE": 1000,
|
||||
"RETRY_UNIX_SOCKET_AFTER": 888,
|
||||
"REQUEST_TIMEOUT": 10000,
|
||||
"FALLBACK_TIMEOUT": 5000,
|
||||
"FALLBACK": []
|
||||
},
|
||||
"SECOND_CORE_RPC": {
|
||||
@@ -64,9 +59,7 @@
|
||||
"PORT": 17,
|
||||
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
|
||||
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": 2000,
|
||||
"COOKIE": false,
|
||||
"COOKIE_PATH": "__SECOND_CORE_RPC_COOKIE_PATH__"
|
||||
"TIMEOUT": 2000
|
||||
},
|
||||
"DATABASE": {
|
||||
"ENABLED": false,
|
||||
@@ -76,7 +69,6 @@
|
||||
"DATABASE": "__DATABASE_DATABASE__",
|
||||
"USERNAME": "__DATABASE_USERNAME__",
|
||||
"PASSWORD": "__DATABASE_PASSWORD__",
|
||||
"PID_DIR": "__DATABASE_PID_FILE__",
|
||||
"TIMEOUT": 3000
|
||||
},
|
||||
"SYSLOG": {
|
||||
@@ -141,7 +133,6 @@
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": false,
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
|
||||
"BATCH_QUERY_BASE_SIZE": 5000
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +55,7 @@ describe('Mempool Backend Config', () => {
|
||||
expect(config.ESPLORA).toStrictEqual({
|
||||
REST_API_URL: 'http://127.0.0.1:3000',
|
||||
UNIX_SOCKET_PATH: null,
|
||||
BATCH_QUERY_BASE_SIZE: 1000,
|
||||
RETRY_UNIX_SOCKET_AFTER: 30000,
|
||||
REQUEST_TIMEOUT: 10000,
|
||||
FALLBACK_TIMEOUT: 5000,
|
||||
FALLBACK: [],
|
||||
});
|
||||
|
||||
@@ -67,9 +64,7 @@ describe('Mempool Backend Config', () => {
|
||||
PORT: 8332,
|
||||
USERNAME: 'mempool',
|
||||
PASSWORD: 'mempool',
|
||||
TIMEOUT: 60000,
|
||||
COOKIE: false,
|
||||
COOKIE_PATH: '/bitcoin/.cookie'
|
||||
TIMEOUT: 60000
|
||||
});
|
||||
|
||||
expect(config.SECOND_CORE_RPC).toStrictEqual({
|
||||
@@ -77,9 +72,7 @@ describe('Mempool Backend Config', () => {
|
||||
PORT: 8332,
|
||||
USERNAME: 'mempool',
|
||||
PASSWORD: 'mempool',
|
||||
TIMEOUT: 60000,
|
||||
COOKIE: false,
|
||||
COOKIE_PATH: '/bitcoin/.cookie'
|
||||
TIMEOUT: 60000
|
||||
});
|
||||
|
||||
expect(config.DATABASE).toStrictEqual({
|
||||
@@ -91,7 +84,6 @@ describe('Mempool Backend Config', () => {
|
||||
USERNAME: 'mempool',
|
||||
PASSWORD: 'mempool',
|
||||
TIMEOUT: 180000,
|
||||
PID_DIR: ''
|
||||
});
|
||||
|
||||
expect(config.SYSLOG).toStrictEqual({
|
||||
@@ -145,8 +137,7 @@ describe('Mempool Backend Config', () => {
|
||||
|
||||
expect(config.REDIS).toStrictEqual({
|
||||
ENABLED: false,
|
||||
UNIX_SOCKET_PATH: '',
|
||||
BATCH_QUERY_BASE_SIZE: 5000,
|
||||
UNIX_SOCKET_PATH: ''
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ class Audit {
|
||||
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
|
||||
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
||||
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 0, similarity: 1 };
|
||||
}
|
||||
|
||||
const matches: string[] = []; // present in both mined block and template
|
||||
@@ -144,12 +144,7 @@ class Audit {
|
||||
|
||||
const numCensored = Object.keys(isCensored).length;
|
||||
const numMatches = matches.length - 1; // adjust for coinbase tx
|
||||
let score = 0;
|
||||
if (numMatches <= 0 && numCensored <= 0) {
|
||||
score = 1;
|
||||
} else if (numMatches > 0) {
|
||||
score = (numMatches / (numMatches + numCensored));
|
||||
}
|
||||
const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
|
||||
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,9 +3,8 @@ import { IEsploraApi } from './esplora-api.interface';
|
||||
export interface AbstractBitcoinApi {
|
||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
||||
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
||||
$getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
|
||||
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
|
||||
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number);
|
||||
$getAllMempoolTransactions(lastTxid: string);
|
||||
$getTransactionHex(txId: string): Promise<string>;
|
||||
$getBlockHeightTip(): Promise<number>;
|
||||
$getBlockHashTip(): Promise<string>;
|
||||
@@ -24,8 +23,6 @@ export interface AbstractBitcoinApi {
|
||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
||||
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
|
||||
|
||||
startHealthChecks(): void;
|
||||
}
|
||||
@@ -35,5 +32,4 @@ export interface BitcoinRpcCredentials {
|
||||
user: string;
|
||||
pass: string;
|
||||
timeout: number;
|
||||
cookie?: string;
|
||||
}
|
||||
|
||||
@@ -60,24 +60,11 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
});
|
||||
}
|
||||
|
||||
async $getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
|
||||
const txs: IEsploraApi.Transaction[] = [];
|
||||
for (const txid of txids) {
|
||||
try {
|
||||
const tx = await this.$getRawTransaction(txid, false, true);
|
||||
txs.push(tx);
|
||||
} catch (err) {
|
||||
// skip failures
|
||||
}
|
||||
}
|
||||
return txs;
|
||||
}
|
||||
|
||||
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
|
||||
throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.');
|
||||
}
|
||||
|
||||
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
|
||||
$getAllMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
|
||||
throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.');
|
||||
|
||||
}
|
||||
@@ -211,19 +198,6 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
return outspends;
|
||||
}
|
||||
|
||||
async $getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
|
||||
return this.$getBatchedOutspends(txId);
|
||||
}
|
||||
|
||||
async $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]> {
|
||||
const outspends: IEsploraApi.Outspend[] = [];
|
||||
for (const outpoint of outpoints) {
|
||||
const outspend = await this.$getOutspend(outpoint.txid, outpoint.vout);
|
||||
outspends.push(outspend);
|
||||
}
|
||||
return outspends;
|
||||
}
|
||||
|
||||
$getEstimatedHashrate(blockHeight: number): Promise<number> {
|
||||
// 120 is the default block span in Core
|
||||
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
|
||||
|
||||
@@ -8,7 +8,6 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
|
||||
user: config.CORE_RPC.USERNAME,
|
||||
pass: config.CORE_RPC.PASSWORD,
|
||||
timeout: config.CORE_RPC.TIMEOUT,
|
||||
cookie: config.CORE_RPC.COOKIE ? config.CORE_RPC.COOKIE_PATH : undefined,
|
||||
};
|
||||
|
||||
export default new bitcoin.Client(nodeRpcCredentials);
|
||||
|
||||
@@ -8,7 +8,6 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
|
||||
user: config.SECOND_CORE_RPC.USERNAME,
|
||||
pass: config.SECOND_CORE_RPC.PASSWORD,
|
||||
timeout: config.SECOND_CORE_RPC.TIMEOUT,
|
||||
cookie: config.SECOND_CORE_RPC.COOKIE ? config.SECOND_CORE_RPC.COOKIE_PATH : undefined,
|
||||
};
|
||||
|
||||
export default new bitcoin.Client(nodeRpcCredentials);
|
||||
|
||||
@@ -24,6 +24,7 @@ class BitcoinRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.$getCpfpInfo)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees)
|
||||
@@ -111,7 +112,6 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'txs/outspends', this.$getBatchedOutspends)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock)
|
||||
@@ -174,20 +174,24 @@ class BitcoinRoutes {
|
||||
res.json(times);
|
||||
}
|
||||
|
||||
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
|
||||
const txids_csv = req.query.txids;
|
||||
if (!txids_csv || typeof txids_csv !== 'string') {
|
||||
res.status(500).send('Invalid txids format');
|
||||
private async $getBatchedOutspends(req: Request, res: Response) {
|
||||
if (!Array.isArray(req.query.txId)) {
|
||||
res.status(500).send('Not an array');
|
||||
return;
|
||||
}
|
||||
const txids = txids_csv.split(',');
|
||||
if (txids.length > 50) {
|
||||
if (req.query.txId.length > 50) {
|
||||
res.status(400).send('Too many txids requested');
|
||||
return;
|
||||
}
|
||||
const txIds: string[] = [];
|
||||
for (const _txId in req.query.txId) {
|
||||
if (typeof req.query.txId[_txId] === 'string') {
|
||||
txIds.push(req.query.txId[_txId].toString());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
|
||||
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txIds);
|
||||
res.json(batchedOutspends);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
@@ -247,7 +251,7 @@ class BitcoinRoutes {
|
||||
|
||||
private async getTransaction(req: Request, res: Response) {
|
||||
try {
|
||||
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true);
|
||||
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
|
||||
res.json(transaction);
|
||||
} catch (e) {
|
||||
let statusCode = 500;
|
||||
@@ -474,7 +478,7 @@ class BitcoinRoutes {
|
||||
}
|
||||
|
||||
let nextHash = startFromHash;
|
||||
for (let i = 0; i < 15 && nextHash; i++) {
|
||||
for (let i = 0; i < 10 && nextHash; i++) {
|
||||
const localBlock = blocks.getBlocks().find((b) => b.id === nextHash);
|
||||
if (localBlock) {
|
||||
returnBlocks.push(localBlock);
|
||||
@@ -573,9 +577,7 @@ class BitcoinRoutes {
|
||||
}
|
||||
|
||||
try {
|
||||
// electrum expects scripthashes in little-endian
|
||||
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
|
||||
const addressData = await bitcoinApi.$getScriptHash(electrumScripthash);
|
||||
const addressData = await bitcoinApi.$getScriptHash(req.params.scripthash);
|
||||
res.json(addressData);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
@@ -592,13 +594,11 @@ class BitcoinRoutes {
|
||||
}
|
||||
|
||||
try {
|
||||
// electrum expects scripthashes in little-endian
|
||||
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
|
||||
let lastTxId: string = '';
|
||||
if (req.query.after_txid && typeof req.query.after_txid === 'string') {
|
||||
lastTxId = req.query.after_txid;
|
||||
}
|
||||
const transactions = await bitcoinApi.$getScriptHashTransactions(electrumScripthash, lastTxId);
|
||||
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.scripthash, lastTxId);
|
||||
res.json(transactions);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
|
||||
@@ -6,7 +6,6 @@ export namespace IEsploraApi {
|
||||
size: number;
|
||||
weight: number;
|
||||
fee: number;
|
||||
sigops?: number;
|
||||
vin: Vin[];
|
||||
vout: Vout[];
|
||||
status: Status;
|
||||
|
||||
@@ -8,9 +8,8 @@ import logger from '../../logger';
|
||||
interface FailoverHost {
|
||||
host: string,
|
||||
rtts: number[],
|
||||
rtt: number,
|
||||
rtt: number
|
||||
failures: number,
|
||||
latestHeight?: number,
|
||||
socket?: boolean,
|
||||
outOfSync?: boolean,
|
||||
unreachable?: boolean,
|
||||
@@ -76,9 +75,9 @@ class FailoverRouter {
|
||||
|
||||
const results = await Promise.allSettled(this.hosts.map(async (host) => {
|
||||
if (host.socket) {
|
||||
return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT });
|
||||
return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: 2000 });
|
||||
} else {
|
||||
return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
|
||||
return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: 2000 });
|
||||
}
|
||||
}));
|
||||
const maxHeight = results.reduce((max, result) => Math.max(max, result.status === 'fulfilled' ? result.value?.data || 0 : 0), 0);
|
||||
@@ -93,7 +92,6 @@ class FailoverRouter {
|
||||
host.rtts.unshift(rtt);
|
||||
host.rtts.slice(0, 5);
|
||||
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
|
||||
host.latestHeight = height;
|
||||
if (height == null || isNaN(height) || (maxHeight - height > 2)) {
|
||||
host.outOfSync = true;
|
||||
} else {
|
||||
@@ -101,23 +99,22 @@ class FailoverRouter {
|
||||
}
|
||||
host.unreachable = false;
|
||||
} else {
|
||||
host.outOfSync = true;
|
||||
host.unreachable = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.sortHosts();
|
||||
|
||||
logger.debug(`Tomahawk ranking:\n${this.hosts.map((host, index) => this.formatRanking(index, host, this.activeHost, maxHeight)).join('\n')}`);
|
||||
logger.debug(`Tomahawk ranking: ${this.hosts.map(host => '\navg rtt ' + Math.round(host.rtt).toString().padStart(5, ' ') + ' | reachable? ' + (!host.unreachable || false).toString().padStart(5, ' ') + ' | in sync? ' + (!host.outOfSync || false).toString().padStart(5, ' ') + ` | ${host.host}`).join('')}`);
|
||||
|
||||
// switch if the current host is out of sync or significantly slower than the next best alternative
|
||||
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) {
|
||||
if (this.activeHost.unreachable) {
|
||||
logger.warn(`🚨🚨🚨 Unable to reach ${this.activeHost.host}, failing over to next best alternative 🚨🚨🚨`);
|
||||
logger.warn(`Unable to reach ${this.activeHost.host}, failing over to next best alternative`);
|
||||
} else if (this.activeHost.outOfSync) {
|
||||
logger.warn(`🚨🚨🚨 ${this.activeHost.host} has fallen behind, failing over to next best alternative 🚨🚨🚨`);
|
||||
logger.warn(`${this.activeHost.host} has fallen behind, failing over to next best alternative`);
|
||||
} else {
|
||||
logger.debug(`🛠️ ${this.activeHost.host} is no longer the best esplora host 🛠️`);
|
||||
logger.debug(`${this.activeHost.host} is no longer the best esplora host`);
|
||||
}
|
||||
this.electHost();
|
||||
}
|
||||
@@ -125,11 +122,6 @@ class FailoverRouter {
|
||||
this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval);
|
||||
}
|
||||
|
||||
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
|
||||
const heightStatus = host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅');
|
||||
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : ' - '} ${host.unreachable ? '🔥' : '✅'} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
|
||||
}
|
||||
|
||||
// sort hosts by connection quality, and update default fallback
|
||||
private sortHosts(): void {
|
||||
// sort by connection quality
|
||||
@@ -164,7 +156,7 @@ class FailoverRouter {
|
||||
private addFailure(host: FailoverHost): FailoverHost {
|
||||
host.failures++;
|
||||
if (host.failures > 5 && this.multihost) {
|
||||
logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`);
|
||||
logger.warn(`Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative`);
|
||||
this.electHost();
|
||||
return this.activeHost;
|
||||
} else {
|
||||
@@ -176,15 +168,12 @@ class FailoverRouter {
|
||||
let axiosConfig;
|
||||
let url;
|
||||
if (host.socket) {
|
||||
axiosConfig = { socketPath: host.host, timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType };
|
||||
axiosConfig = { socketPath: host.host, timeout: 10000, responseType };
|
||||
url = path;
|
||||
} else {
|
||||
axiosConfig = { timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType };
|
||||
axiosConfig = { timeout: 10000, responseType };
|
||||
url = host.host + path;
|
||||
}
|
||||
if (data?.params) {
|
||||
axiosConfig.params = data.params;
|
||||
}
|
||||
return (method === 'post'
|
||||
? this.requestConnection.post<T>(url, data, axiosConfig)
|
||||
: this.requestConnection.get<T>(url, axiosConfig)
|
||||
@@ -192,8 +181,7 @@ class FailoverRouter {
|
||||
.catch((e) => {
|
||||
let fallbackHost = this.fallbackHost;
|
||||
if (e?.response?.status !== 404) {
|
||||
logger.warn(`esplora request failed ${e?.response?.status} ${host.host}${path}`);
|
||||
logger.warn(e instanceof Error ? e.message : e);
|
||||
logger.warn(`esplora request failed ${e?.response?.status || 500} ${host.host}${path}`);
|
||||
fallbackHost = this.addFailure(host);
|
||||
}
|
||||
if (retry && e?.code === 'ECONNREFUSED' && this.multihost) {
|
||||
@@ -205,8 +193,8 @@ class FailoverRouter {
|
||||
});
|
||||
}
|
||||
|
||||
public async $get<T>(path, responseType = 'json', params: any = null): Promise<T> {
|
||||
return this.$query<T>('get', path, params ? { params } : null, responseType);
|
||||
public async $get<T>(path, responseType = 'json'): Promise<T> {
|
||||
return this.$query<T>('get', path, null, responseType);
|
||||
}
|
||||
|
||||
public async $post<T>(path, data: any, responseType = 'json'): Promise<T> {
|
||||
@@ -225,16 +213,12 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txId);
|
||||
}
|
||||
|
||||
async $getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
|
||||
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/txs', txids, 'json');
|
||||
}
|
||||
|
||||
async $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
|
||||
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/mempool/txs', txids, 'json');
|
||||
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/mempool/txs', txids, 'json');
|
||||
}
|
||||
|
||||
async $getAllMempoolTransactions(lastSeenTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
|
||||
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''), 'json', max_txs ? { max_txs } : null);
|
||||
async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
|
||||
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
|
||||
}
|
||||
|
||||
$getTransactionHex(txId: string): Promise<string> {
|
||||
@@ -254,7 +238,7 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
}
|
||||
|
||||
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
|
||||
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/block/' + hash + '/txs');
|
||||
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/block/' + hash + '/txs');
|
||||
}
|
||||
|
||||
$getBlockHash(height: number): Promise<string> {
|
||||
@@ -306,16 +290,13 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
return this.failoverRouter.$get<IEsploraApi.Outspend[]>('/tx/' + txId + '/outspends');
|
||||
}
|
||||
|
||||
async $getBatchedOutspends(txids: string[]): Promise<IEsploraApi.Outspend[][]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async $getBatchedOutspendsInternal(txids: string[]): Promise<IEsploraApi.Outspend[][]> {
|
||||
return this.failoverRouter.$post<IEsploraApi.Outspend[][]>('/internal/txs/outspends/by-txid', txids, 'json');
|
||||
}
|
||||
|
||||
async $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]> {
|
||||
return this.failoverRouter.$post<IEsploraApi.Outspend[]>('/internal/txs/outspends/by-outpoint', outpoints.map(out => `${out.txid}:${out.vout}`), 'json');
|
||||
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
|
||||
const outspends: IEsploraApi.Outspend[][] = [];
|
||||
for (const tx of txId) {
|
||||
const outspend = await this.$getOutspends(tx);
|
||||
outspends.push(outspend);
|
||||
}
|
||||
return outspends;
|
||||
}
|
||||
|
||||
public startHealthChecks(): void {
|
||||
|
||||
@@ -29,6 +29,10 @@ import websocketHandler from './websocket-handler';
|
||||
import redisCache from './redis-cache';
|
||||
import rbfCache from './rbf-cache';
|
||||
import { calcBitsDifference } from './difficulty-adjustment';
|
||||
import os from 'os';
|
||||
import { Worker } from 'worker_threads';
|
||||
import path from 'path';
|
||||
|
||||
|
||||
class Blocks {
|
||||
private blocks: BlockExtended[] = [];
|
||||
@@ -81,7 +85,6 @@ class Blocks {
|
||||
private async $getTransactionsExtended(
|
||||
blockHash: string,
|
||||
blockHeight: number,
|
||||
blockTime: number,
|
||||
onlyCoinbase: boolean,
|
||||
txIds: string[] | null = null,
|
||||
quiet: boolean = false,
|
||||
@@ -102,12 +105,6 @@ class Blocks {
|
||||
if (!onlyCoinbase) {
|
||||
for (const txid of txIds) {
|
||||
if (mempool[txid]) {
|
||||
mempool[txid].status = {
|
||||
confirmed: true,
|
||||
block_height: blockHeight,
|
||||
block_hash: blockHash,
|
||||
block_time: blockTime,
|
||||
};
|
||||
transactionMap[txid] = mempool[txid];
|
||||
foundInMempool++;
|
||||
totalFound++;
|
||||
@@ -413,6 +410,8 @@ class Blocks {
|
||||
return;
|
||||
}
|
||||
|
||||
const workerPool: Worker[] = [];
|
||||
|
||||
try {
|
||||
// Get all indexed block hash
|
||||
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
|
||||
@@ -427,39 +426,67 @@ class Blocks {
|
||||
let newlyIndexed = 0;
|
||||
let totalIndexed = indexedBlockSummariesHashesArray.length;
|
||||
let indexedThisRun = 0;
|
||||
let timer = Date.now() / 1000;
|
||||
const startedAt = Date.now() / 1000;
|
||||
let timer = Date.now();
|
||||
const startedAt = Date.now();
|
||||
|
||||
for (const block of indexedBlocks) {
|
||||
if (indexedBlockSummariesHashes[block.hash] === true) {
|
||||
continue;
|
||||
}
|
||||
const blocksToIndex = indexedBlocks.filter(block => !indexedBlockSummariesHashes[block.hash]);
|
||||
|
||||
// Logging
|
||||
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||
if (elapsedSeconds > 5) {
|
||||
const runningFor = (Date.now() / 1000) - startedAt;
|
||||
const blockPerSeconds = indexedThisRun / elapsedSeconds;
|
||||
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
|
||||
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
|
||||
timer = Date.now() / 1000;
|
||||
indexedThisRun = 0;
|
||||
}
|
||||
if (!blocksToIndex.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const numWorkers = Math.max(1, os.cpus().length - 1);
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
workerPool.push(new Worker(path.resolve(__dirname, '../index-workers/block-summary-worker.js')));
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
|
||||
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// This function assigns a task to a worker
|
||||
const assignTask = (worker: Worker): boolean => {
|
||||
if (blocksToIndex.length === 0) {
|
||||
return false;
|
||||
} else {
|
||||
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
|
||||
worker.postMessage(blocksToIndex.shift());
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Logging
|
||||
const handleResult = (height: number): void => {
|
||||
indexedThisRun++;
|
||||
totalIndexed++;
|
||||
newlyIndexed++;
|
||||
const elapsed = Date.now() - timer;
|
||||
if (elapsed > 5000) {
|
||||
const runningFor = Date.now() - startedAt;
|
||||
const blockPerSeconds = indexedThisRun / (elapsed / 1000);
|
||||
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
|
||||
logger.debug(`Indexing block summary for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${(runningFor / 1000).toFixed(2)} seconds`, logger.tags.mining);
|
||||
timer = Date.now();
|
||||
indexedThisRun = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Start a task on each worker
|
||||
for (const worker of workerPool) {
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
worker.removeAllListeners();
|
||||
worker.on('message', (result) => {
|
||||
// Handle the result, then assign a new task to the worker
|
||||
handleResult(result);
|
||||
if (!assignTask(worker)) {
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
worker.on('error', reject);
|
||||
if (!assignTask(worker)) {
|
||||
resolve();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (newlyIndexed > 0) {
|
||||
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining);
|
||||
} else {
|
||||
@@ -468,6 +495,14 @@ class Blocks {
|
||||
} catch (e) {
|
||||
logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
|
||||
throw e;
|
||||
} finally {
|
||||
for (const worker of workerPool) {
|
||||
if (worker) {
|
||||
// clean up the workers
|
||||
worker.removeAllListeners();
|
||||
worker.terminate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,6 +599,7 @@ class Blocks {
|
||||
* [INDEXING] Index all blocks metadata for the mining dashboard
|
||||
*/
|
||||
public async $generateBlockDatabase(): Promise<boolean> {
|
||||
const workerPool: Worker[] = [];
|
||||
try {
|
||||
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||
let currentBlockHeight = blockchainInfo.blocks;
|
||||
@@ -582,12 +618,18 @@ class Blocks {
|
||||
let totalIndexed = await blocksRepository.$blockCountBetweenHeight(currentBlockHeight, lastBlockToIndex);
|
||||
let indexedThisRun = 0;
|
||||
let newlyIndexed = 0;
|
||||
const startedAt = Date.now() / 1000;
|
||||
let timer = Date.now() / 1000;
|
||||
const startedAt = Date.now();
|
||||
let timer = Date.now();
|
||||
|
||||
if (currentBlockHeight >= lastBlockToIndex) {
|
||||
const numWorkers = Math.max(1, os.cpus().length - 1);
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
workerPool.push(new Worker(path.resolve(__dirname, '../index-workers/block-worker.js')));
|
||||
}
|
||||
}
|
||||
|
||||
while (currentBlockHeight >= lastBlockToIndex) {
|
||||
const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1);
|
||||
|
||||
const missingBlockHeights: number[] = await blocksRepository.$getMissingBlocksBetweenHeights(
|
||||
currentBlockHeight, endBlock);
|
||||
if (missingBlockHeights.length <= 0) {
|
||||
@@ -597,33 +639,65 @@ class Blocks {
|
||||
|
||||
logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`, logger.tags.mining);
|
||||
|
||||
for (const blockHeight of missingBlockHeights) {
|
||||
if (blockHeight < lastBlockToIndex) {
|
||||
break;
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// This function assigns a task to a worker
|
||||
const assignTask = (worker: Worker): boolean => {
|
||||
if (missingBlockHeights.length === 0) {
|
||||
return false;
|
||||
} else {
|
||||
worker.postMessage({ height: missingBlockHeights.shift() });
|
||||
return true;
|
||||
}
|
||||
++indexedThisRun;
|
||||
++totalIndexed;
|
||||
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
||||
const runningFor = (Date.now() / 1000) - startedAt;
|
||||
const blockPerSeconds = indexedThisRun / elapsedSeconds;
|
||||
};
|
||||
|
||||
const handleResult = (height: number): void => {
|
||||
indexedThisRun++;
|
||||
totalIndexed++;
|
||||
newlyIndexed++;
|
||||
const elapsed = Date.now() - timer;
|
||||
if (elapsed > 5000 || height === lastBlockToIndex) {
|
||||
const runningFor = Date.now() - startedAt;
|
||||
const blockPerSeconds = indexedThisRun / (elapsed / 1000);
|
||||
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
|
||||
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress.toFixed(2)}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
|
||||
timer = Date.now() / 1000;
|
||||
logger.debug(`Indexing block #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress.toFixed(2)}%) | elapsed: ${(runningFor / 1000).toFixed(2)} seconds`, logger.tags.mining);
|
||||
timer = Date.now();
|
||||
indexedThisRun = 0;
|
||||
loadingIndicators.setProgress('block-indexing', progress, false);
|
||||
}
|
||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, block.timestamp, true, null, true);
|
||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||
};
|
||||
|
||||
newlyIndexed++;
|
||||
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
||||
// Start a task on each worker
|
||||
for (const worker of workerPool) {
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
worker.removeAllListeners();
|
||||
worker.on('message', (result) => {
|
||||
// Handle the result, then assign a new task to the worker
|
||||
handleResult(result);
|
||||
if (!assignTask(worker)) {
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
worker.on('error', reject);
|
||||
if (!assignTask(worker)) {
|
||||
resolve();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
currentBlockHeight -= chunkSize;
|
||||
}
|
||||
|
||||
for (const worker of workerPool) {
|
||||
if (worker) {
|
||||
// clean up the workers
|
||||
worker.removeAllListeners();
|
||||
worker.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
if (newlyIndexed > 0) {
|
||||
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining);
|
||||
} else {
|
||||
@@ -634,6 +708,14 @@ class Blocks {
|
||||
logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
||||
loadingIndicators.setProgress('block-indexing', 100);
|
||||
throw e;
|
||||
} finally {
|
||||
for (const worker of workerPool) {
|
||||
if (worker) {
|
||||
// clean up the workers
|
||||
worker.removeAllListeners();
|
||||
worker.terminate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await BlocksRepository.$validateChain();
|
||||
@@ -708,7 +790,7 @@ class Blocks {
|
||||
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
|
||||
const block = BitcoinApi.convertBlock(verboseBlock);
|
||||
const txIds: string[] = verboseBlock.tx.map(tx => tx.txid);
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, block.timestamp, false, txIds, false, true) as MempoolTransactionExtended[];
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, txIds, false, true) as MempoolTransactionExtended[];
|
||||
|
||||
// fill in missing transaction fee data from verboseBlock
|
||||
for (let i = 0; i < transactions.length; i++) {
|
||||
@@ -761,13 +843,8 @@ class Blocks {
|
||||
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
|
||||
|
||||
if (!fastForwarded) {
|
||||
let lastestPriceId;
|
||||
try {
|
||||
lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
|
||||
} catch (e) {
|
||||
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
|
||||
if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
|
||||
await blocksRepository.$saveBlockPrices([{
|
||||
height: blockExtended.height,
|
||||
@@ -776,7 +853,9 @@ class Blocks {
|
||||
this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`);
|
||||
} else {
|
||||
logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
|
||||
indexer.scheduleSingleTask('blocksPrices', 10000);
|
||||
setTimeout(() => {
|
||||
indexer.runSingleTask('blocksPrices');
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// Save blocks summary for visualization if it's enabled
|
||||
@@ -900,7 +979,7 @@ class Blocks {
|
||||
|
||||
const blockHash = await bitcoinApi.$getBlockHash(height);
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, block.timestamp, true);
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
|
||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||
|
||||
if (Common.indexingEnabled()) {
|
||||
@@ -912,7 +991,7 @@ class Blocks {
|
||||
|
||||
public async $indexStaleBlock(hash: string): Promise<BlockExtended> {
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
|
||||
const transactions = await this.$getTransactionsExtended(hash, block.height, block.timestamp, true);
|
||||
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
|
||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||
|
||||
blockExtended.canonical = await bitcoinApi.$getBlockHash(block.height);
|
||||
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 66;
|
||||
private static currentVersion = 65;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -553,11 +553,6 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
|
||||
await this.updateToSchemaVersion(65);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 66) {
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD min_fee FLOAT UNSIGNED DEFAULT NULL');
|
||||
await this.updateToSchemaVersion(66);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -252,11 +252,7 @@ class DiskCache {
|
||||
}
|
||||
|
||||
if (rbfData?.rbf) {
|
||||
rbfCache.load({
|
||||
txs: rbfData.rbf.txs.map(([txid, entry]) => ({ value: entry })),
|
||||
trees: rbfData.rbf.trees,
|
||||
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
|
||||
});
|
||||
rbfCache.load(rbfData.rbf);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse rbf cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
|
||||
@@ -80,13 +80,7 @@ class ChannelsApi {
|
||||
|
||||
public async $searchChannelsById(search: string): Promise<any[]> {
|
||||
try {
|
||||
// restrict search to valid id/short_id prefix formats
|
||||
let searchStripped = search.match(/[0-9]+[0-9x]*/)?.[0] || '';
|
||||
if (!searchStripped.length) {
|
||||
return [];
|
||||
}
|
||||
// add wildcard to search by prefix
|
||||
searchStripped += '%';
|
||||
const searchStripped = search.replace(/[^0-9x]/g, '') + '%';
|
||||
const query = `SELECT id, short_id, capacity, status FROM channels WHERE id LIKE ? OR short_id LIKE ? LIMIT 10`;
|
||||
const [rows]: any = await DB.query(query, [searchStripped, searchStripped]);
|
||||
return rows;
|
||||
|
||||
@@ -42,12 +42,6 @@ class NodesRoutes {
|
||||
switch (config.MEMPOOL.NETWORK) {
|
||||
case 'testnet':
|
||||
nodesList = [
|
||||
'0259db43b4e4ac0ff12a805f2d81e521253ba2317f6739bc611d8e2fa156d64256',
|
||||
'0352b9944b9a52bd2116c91f1ba70c4ef851ac5ba27e1b20f1d92da3ade010dd10',
|
||||
'03424f5a7601eaa47482cb17100b31a84a04d14fb44b83a57eeceffd8e299878e3',
|
||||
'032850492ee61a5f7006a2fda6925e4b4ec3782f2b6de2ff0e439ef5a38c3b2470',
|
||||
'022c80bace98831c44c32fb69755f2b353434e0ee9e7fbda29507f7ef8abea1421',
|
||||
'02c3559c833e6f99f9ca05fe503e0b4e7524dea9121344edfd3e811101e0c28680',
|
||||
'032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b',
|
||||
'025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7',
|
||||
'0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55',
|
||||
@@ -70,12 +64,6 @@ class NodesRoutes {
|
||||
break;
|
||||
case 'signet':
|
||||
nodesList = [
|
||||
'029fe3621fc0c6e08056a14b868f8fb9acca1aa28a129512f6cea0f0d7654d9f92',
|
||||
'02f60cd7a3a4f1c953dd9554a6ebd51a34f8b10b8124b7fc43a0b381139b55c883',
|
||||
'03cbbf581774700865eebd1be42d022bc004ba30881274ab304e088a25d70e773d',
|
||||
'0243348cb3741cfe2d8485fa8375c29c7bc7cbb67577c363cb6987a5e5fd0052cc',
|
||||
'02cb73e631af44bee600d80f8488a9194c9dc5c7590e575c421a070d1be05bc8e9',
|
||||
'0306f55ee631aa1e2cd4d9b2bfcbc14404faec5c541cef8b2e6f779061029d09c4',
|
||||
'03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956',
|
||||
'033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de',
|
||||
'02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781',
|
||||
@@ -98,12 +86,6 @@ class NodesRoutes {
|
||||
break;
|
||||
default:
|
||||
nodesList = [
|
||||
'02b12b889fe3c943cb05645921040ef13d6d397a2e7a4ad000e28500c505ff26d6',
|
||||
'0302240ac9d71b39617cbde2764837ec3d6198bd6074b15b75d2ff33108e89d2e1',
|
||||
'03364a8ace313376e5e4b68c954e287c6388e16df9e9fdbaf0363ecac41105cbf6',
|
||||
'03229ab4b7f692753e094b93df90530150680f86b535b5183b0cffd75b3df583fc',
|
||||
'03a696eb7acde991c1be97a58a9daef416659539ae462b897f5e9ae361f990228e',
|
||||
'0248bf26cf3a63ab8870f34dc0ec9e6c8c6288cdba96ba3f026f34ec0f13ac4055',
|
||||
'03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61',
|
||||
'03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437',
|
||||
'03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144',
|
||||
|
||||
@@ -3,31 +3,21 @@ import { Common } from './common';
|
||||
import mempool from './mempool';
|
||||
import projectedBlocks from './mempool-blocks';
|
||||
|
||||
interface RecommendedFees {
|
||||
fastestFee: number,
|
||||
halfHourFee: number,
|
||||
hourFee: number,
|
||||
economyFee: number,
|
||||
minimumFee: number,
|
||||
}
|
||||
|
||||
class FeeApi {
|
||||
constructor() { }
|
||||
|
||||
defaultFee = Common.isLiquid() ? 0.1 : 1;
|
||||
minimumIncrement = Common.isLiquid() ? 0.1 : 1;
|
||||
|
||||
public getRecommendedFee(): RecommendedFees {
|
||||
public getRecommendedFee() {
|
||||
const pBlocks = projectedBlocks.getMempoolBlocks();
|
||||
const mPool = mempool.getMempoolInfo();
|
||||
const minimumFee = this.roundUpToNearest(mPool.mempoolminfee * 100000, this.minimumIncrement);
|
||||
const defaultMinFee = Math.max(minimumFee, this.defaultFee);
|
||||
const minimumFee = Math.ceil(mPool.mempoolminfee * 100000);
|
||||
|
||||
if (!pBlocks.length) {
|
||||
return {
|
||||
'fastestFee': defaultMinFee,
|
||||
'halfHourFee': defaultMinFee,
|
||||
'hourFee': defaultMinFee,
|
||||
'fastestFee': this.defaultFee,
|
||||
'halfHourFee': this.defaultFee,
|
||||
'hourFee': this.defaultFee,
|
||||
'economyFee': minimumFee,
|
||||
'minimumFee': minimumFee,
|
||||
};
|
||||
@@ -37,15 +27,11 @@ class FeeApi {
|
||||
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;
|
||||
|
||||
// explicitly enforce a minimum of ceil(mempoolminfee) on all recommendations.
|
||||
// simply rounding up recommended rates is insufficient, as the purging rate
|
||||
// can exceed the median rate of projected blocks in some extreme scenarios
|
||||
// (see https://bitcoin.stackexchange.com/a/120024)
|
||||
return {
|
||||
'fastestFee': Math.max(minimumFee, firstMedianFee),
|
||||
'halfHourFee': Math.max(minimumFee, secondMedianFee),
|
||||
'hourFee': Math.max(minimumFee, thirdMedianFee),
|
||||
'economyFee': Math.max(minimumFee, Math.min(2 * minimumFee, thirdMedianFee)),
|
||||
'fastestFee': firstMedianFee,
|
||||
'halfHourFee': secondMedianFee,
|
||||
'hourFee': thirdMedianFee,
|
||||
'economyFee': Math.min(2 * minimumFee, thirdMedianFee),
|
||||
'minimumFee': minimumFee,
|
||||
};
|
||||
}
|
||||
@@ -59,11 +45,7 @@ class FeeApi {
|
||||
const multiplier = (pBlock.blockVSize - 500000) / 500000;
|
||||
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
|
||||
}
|
||||
return this.roundUpToNearest(useFee, this.minimumIncrement);
|
||||
}
|
||||
|
||||
private roundUpToNearest(value: number, nearest: number): number {
|
||||
return Math.ceil(value / nearest) * nearest;
|
||||
return Math.ceil(useFee);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,13 +44,9 @@ export enum FeatureBits {
|
||||
KeysendOptional = 55,
|
||||
ScriptEnforcedLeaseRequired = 2022,
|
||||
ScriptEnforcedLeaseOptional = 2023,
|
||||
SimpleTaprootChannelsRequiredFinal = 80,
|
||||
SimpleTaprootChannelsOptionalFinal = 81,
|
||||
SimpleTaprootChannelsRequiredStaging = 180,
|
||||
SimpleTaprootChannelsOptionalStaging = 181,
|
||||
MaxBolt11Feature = 5114,
|
||||
};
|
||||
|
||||
|
||||
export const FeaturesMap = new Map<FeatureBits, string>([
|
||||
[FeatureBits.DataLossProtectRequired, 'data-loss-protect'],
|
||||
[FeatureBits.DataLossProtectOptional, 'data-loss-protect'],
|
||||
@@ -89,10 +85,6 @@ export const FeaturesMap = new Map<FeatureBits, string>([
|
||||
[FeatureBits.ZeroConfOptional, 'zero-conf'],
|
||||
[FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'],
|
||||
[FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'],
|
||||
[FeatureBits.SimpleTaprootChannelsRequiredFinal, 'taproot-channels'],
|
||||
[FeatureBits.SimpleTaprootChannelsOptionalFinal, 'taproot-channels'],
|
||||
[FeatureBits.SimpleTaprootChannelsRequiredStaging, 'taproot-channels-staging'],
|
||||
[FeatureBits.SimpleTaprootChannelsOptionalStaging, 'taproot-channels-staging'],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@ class MemoryCache {
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
this.cache = this.cache.filter((cache) => cache.expires > (new Date()));
|
||||
this.cache = this.cache.filter((cache) => cache.expires < (new Date()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -451,7 +451,6 @@ class MempoolBlocks {
|
||||
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
|
||||
for (const [txid, rate] of rates) {
|
||||
if (txid in mempool) {
|
||||
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
|
||||
mempool[txid].effectiveFeePerVsize = rate;
|
||||
mempool[txid].cpfpChecked = false;
|
||||
}
|
||||
@@ -495,9 +494,6 @@ class MempoolBlocks {
|
||||
}
|
||||
}
|
||||
});
|
||||
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
|
||||
}
|
||||
}
|
||||
@@ -535,21 +531,12 @@ class MempoolBlocks {
|
||||
|
||||
const acceleration = accelerations[txid];
|
||||
if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||
if (!mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
mempoolTx.acceleration = true;
|
||||
for (const ancestor of mempoolTx.ancestors || []) {
|
||||
if (!mempool[ancestor.txid].acceleration) {
|
||||
mempool[ancestor.txid].cpfpDirty = true;
|
||||
}
|
||||
mempool[ancestor.txid].acceleration = true;
|
||||
isAccelerated[ancestor.txid] = true;
|
||||
}
|
||||
} else {
|
||||
if (mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
delete mempoolTx.acceleration;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class Mempool {
|
||||
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
||||
private spendMap = new Map<string, MempoolTransactionExtended>();
|
||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
||||
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
|
||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
|
||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
||||
@@ -94,7 +94,7 @@ class Mempool {
|
||||
logger.debug(`Migrating ${Object.keys(this.mempoolCache).length} transactions from disk cache to Redis cache`);
|
||||
}
|
||||
for (const txid of Object.keys(this.mempoolCache)) {
|
||||
if (!this.mempoolCache[txid].adjustedVsize || this.mempoolCache[txid].sigops == null || this.mempoolCache[txid].effectiveFeePerVsize == null) {
|
||||
if (!this.mempoolCache[txid].sigops || this.mempoolCache[txid].effectiveFeePerVsize == null) {
|
||||
this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]);
|
||||
}
|
||||
if (this.mempoolCache[txid].order == null) {
|
||||
@@ -126,7 +126,7 @@ class Mempool {
|
||||
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
|
||||
while (!done) {
|
||||
try {
|
||||
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid, config.ESPLORA.BATCH_QUERY_BASE_SIZE);
|
||||
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid);
|
||||
if (result) {
|
||||
for (const tx of result) {
|
||||
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
|
||||
@@ -235,7 +235,7 @@ class Mempool {
|
||||
|
||||
if (!loaded) {
|
||||
const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]);
|
||||
const sliceLength = config.ESPLORA.BATCH_QUERY_BASE_SIZE;
|
||||
const sliceLength = 10000;
|
||||
for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) {
|
||||
const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false);
|
||||
|
||||
@@ -15,13 +15,6 @@ import bitcoinApi from '../bitcoin/bitcoin-api-factory';
|
||||
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||
import database from '../../database';
|
||||
|
||||
interface DifficultyBlock {
|
||||
timestamp: number,
|
||||
height: number,
|
||||
bits: number,
|
||||
difficulty: number,
|
||||
}
|
||||
|
||||
class Mining {
|
||||
private blocksPriceIndexingRunning = false;
|
||||
public lastHashrateIndexingDate: number | null = null;
|
||||
@@ -428,7 +421,6 @@ class Mining {
|
||||
indexedHeights[height] = true;
|
||||
}
|
||||
|
||||
// gets {time, height, difficulty, bits} of blocks in ascending order of height
|
||||
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
|
||||
let currentDifficulty = genesisBlock.difficulty;
|
||||
@@ -444,45 +436,41 @@ class Mining {
|
||||
});
|
||||
}
|
||||
|
||||
if (!blocks?.length) {
|
||||
// no blocks in database yet
|
||||
return;
|
||||
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
|
||||
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
|
||||
currentBits = oldestConsecutiveBlock.bits;
|
||||
currentDifficulty = oldestConsecutiveBlock.difficulty;
|
||||
}
|
||||
|
||||
const oldestConsecutiveBlock = this.getOldestConsecutiveBlock(blocks);
|
||||
|
||||
currentBits = oldestConsecutiveBlock.bits;
|
||||
currentDifficulty = oldestConsecutiveBlock.difficulty;
|
||||
|
||||
let totalBlockChecked = 0;
|
||||
let timer = new Date().getTime() / 1000;
|
||||
|
||||
for (const block of blocks) {
|
||||
// skip until the first block after the oldest consecutive block
|
||||
if (block.height <= oldestConsecutiveBlock.height) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// difficulty has changed between two consecutive blocks!
|
||||
if (block.bits !== currentBits) {
|
||||
// skip if already indexed
|
||||
if (indexedHeights[block.height] !== true) {
|
||||
let adjustment = block.difficulty / currentDifficulty;
|
||||
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
|
||||
|
||||
await DifficultyAdjustmentsRepository.$saveAdjustments({
|
||||
time: block.time,
|
||||
height: block.height,
|
||||
difficulty: block.difficulty,
|
||||
adjustment: adjustment,
|
||||
});
|
||||
|
||||
totalIndexed++;
|
||||
if (indexedHeights[block.height] === true) { // Already indexed
|
||||
if (block.height >= oldestConsecutiveBlock.height) {
|
||||
currentDifficulty = block.difficulty;
|
||||
currentBits = block.bits;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// update the current difficulty
|
||||
currentDifficulty = block.difficulty;
|
||||
currentBits = block.bits;
|
||||
}
|
||||
|
||||
let adjustment = block.difficulty / currentDifficulty;
|
||||
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
|
||||
|
||||
await DifficultyAdjustmentsRepository.$saveAdjustments({
|
||||
time: block.time,
|
||||
height: block.height,
|
||||
difficulty: block.difficulty,
|
||||
adjustment: adjustment,
|
||||
});
|
||||
|
||||
totalIndexed++;
|
||||
if (block.height >= oldestConsecutiveBlock.height) {
|
||||
currentDifficulty = block.difficulty;
|
||||
currentBits = block.bits;
|
||||
}
|
||||
}
|
||||
|
||||
totalBlockChecked++;
|
||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||
@@ -645,17 +633,6 @@ class Mining {
|
||||
default: return 86400 * scale;
|
||||
}
|
||||
}
|
||||
|
||||
// Finds the oldest block in a consecutive chain back from the tip
|
||||
// assumes `blocks` is sorted in ascending height order
|
||||
private getOldestConsecutiveBlock(blocks: DifficultyBlock[]): DifficultyBlock {
|
||||
for (let i = blocks.length - 1; i > 0; i--) {
|
||||
if ((blocks[i].height - blocks[i - 1].height) > 1) {
|
||||
return blocks[i];
|
||||
}
|
||||
}
|
||||
return blocks[0];
|
||||
}
|
||||
}
|
||||
|
||||
export default new Mining();
|
||||
|
||||
@@ -2,7 +2,6 @@ import config from "../config";
|
||||
import logger from "../logger";
|
||||
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import { IEsploraApi } from "./bitcoin/esplora-api.interface";
|
||||
import { Common } from "./common";
|
||||
import redisCache from "./redis-cache";
|
||||
|
||||
@@ -54,10 +53,7 @@ class RbfCache {
|
||||
private expiring: Map<string, number> = new Map();
|
||||
private cacheQueue: CacheEvent[] = [];
|
||||
|
||||
private evictionCount = 0;
|
||||
private staleCount = 0;
|
||||
|
||||
constructor() {
|
||||
public init(): void {
|
||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
||||
}
|
||||
|
||||
@@ -249,7 +245,6 @@ class RbfCache {
|
||||
|
||||
// flag a transaction as removed from the mempool
|
||||
public evict(txid: string, fast: boolean = false): void {
|
||||
this.evictionCount++;
|
||||
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
||||
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
||||
this.addExpiration(txid, expiryTime);
|
||||
@@ -277,23 +272,18 @@ class RbfCache {
|
||||
this.remove(txid);
|
||||
}
|
||||
}
|
||||
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire (${this.evictionCount} newly expired)`);
|
||||
this.evictionCount = 0;
|
||||
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire`);
|
||||
}
|
||||
|
||||
// remove a transaction & all previous versions from the cache
|
||||
private remove(txid): void {
|
||||
// don't remove a transaction if a newer version remains in the mempool
|
||||
if (!this.replacedBy.has(txid)) {
|
||||
const root = this.treeMap.get(txid);
|
||||
const replaces = this.replaces.get(txid);
|
||||
this.replaces.delete(txid);
|
||||
this.treeMap.delete(txid);
|
||||
this.removeTx(txid);
|
||||
this.removeExpiration(txid);
|
||||
if (root === txid) {
|
||||
this.removeTree(txid);
|
||||
}
|
||||
for (const tx of (replaces || [])) {
|
||||
// recursively remove prior versions from the cache
|
||||
this.replacedBy.delete(tx);
|
||||
@@ -369,27 +359,18 @@ class RbfCache {
|
||||
}
|
||||
|
||||
public async load({ txs, trees, expiring }): Promise<void> {
|
||||
try {
|
||||
txs.forEach(txEntry => {
|
||||
this.txs.set(txEntry.value.txid, txEntry.value);
|
||||
});
|
||||
this.staleCount = 0;
|
||||
for (const deflatedTree of trees) {
|
||||
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||
}
|
||||
expiring.forEach(expiringEntry => {
|
||||
if (this.txs.has(expiringEntry.key)) {
|
||||
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
|
||||
}
|
||||
});
|
||||
this.staleCount = 0;
|
||||
await this.checkTrees();
|
||||
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
|
||||
this.cleanup();
|
||||
|
||||
} catch (e) {
|
||||
logger.err('failed to restore RBF cache: ' + (e instanceof Error ? e.message : e));
|
||||
txs.forEach(txEntry => {
|
||||
this.txs.set(txEntry.key, txEntry.value);
|
||||
});
|
||||
for (const deflatedTree of trees) {
|
||||
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||
}
|
||||
expiring.forEach(expiringEntry => {
|
||||
if (this.txs.has(expiringEntry.key)) {
|
||||
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
|
||||
}
|
||||
});
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
exportTree(tree: RbfTree, deflated: any = null) {
|
||||
@@ -417,11 +398,29 @@ class RbfCache {
|
||||
const treeInfo = deflated[txid];
|
||||
const replaces: RbfTree[] = [];
|
||||
|
||||
// if the root tx is unknown, remove this tree and return early
|
||||
if (root === txid && !txs.has(txid)) {
|
||||
this.staleCount++;
|
||||
this.removeTree(deflated.key);
|
||||
return;
|
||||
// check if any transactions in this tree have already been confirmed
|
||||
mined = mined || treeInfo.mined;
|
||||
let exists = mined;
|
||||
if (!mined) {
|
||||
try {
|
||||
const apiTx = await bitcoinApi.$getRawTransaction(txid);
|
||||
if (apiTx) {
|
||||
exists = true;
|
||||
}
|
||||
if (apiTx?.status?.confirmed) {
|
||||
mined = true;
|
||||
treeInfo.txMined = true;
|
||||
this.evict(txid, true);
|
||||
}
|
||||
} catch (e) {
|
||||
// most transactions do not exist
|
||||
}
|
||||
}
|
||||
|
||||
// if the root tx is not in the mempool or the blockchain
|
||||
// evict this tree as soon as possible
|
||||
if (root === txid && !exists) {
|
||||
this.evict(txid, true);
|
||||
}
|
||||
|
||||
// recursively reconstruct child trees
|
||||
@@ -459,60 +458,6 @@ class RbfCache {
|
||||
return tree;
|
||||
}
|
||||
|
||||
private async checkTrees(): Promise<void> {
|
||||
const found: { [txid: string]: boolean } = {};
|
||||
const txids = Array.from(this.txs.values()).map(tx => tx.txid).filter(txid => {
|
||||
return !this.expiring.has(txid) && !this.getRbfTree(txid)?.mined;
|
||||
});
|
||||
|
||||
const processTxs = (txs: IEsploraApi.Transaction[]): void => {
|
||||
for (const tx of txs) {
|
||||
found[tx.txid] = true;
|
||||
if (tx.status?.confirmed) {
|
||||
const tree = this.getRbfTree(tx.txid);
|
||||
if (tree) {
|
||||
this.setTreeMined(tree, tx.txid);
|
||||
tree.mined = true;
|
||||
this.evict(tx.txid, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
let processedCount = 0;
|
||||
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 40);
|
||||
for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) {
|
||||
const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
processedCount += slice.length;
|
||||
try {
|
||||
const txs = await bitcoinApi.$getRawTransactions(slice);
|
||||
processTxs(txs);
|
||||
logger.debug(`fetched and processed ${processedCount} of ${txids.length} cached rbf transactions (${(processedCount / txids.length * 100).toFixed(2)}%)`);
|
||||
} catch (err) {
|
||||
logger.err(`failed to fetch or process ${slice.length} cached rbf transactions`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const txs: IEsploraApi.Transaction[] = [];
|
||||
for (const txid of txids) {
|
||||
try {
|
||||
const tx = await bitcoinApi.$getRawTransaction(txid, true, false);
|
||||
txs.push(tx);
|
||||
} catch (err) {
|
||||
// some 404s are expected, so continue quietly
|
||||
}
|
||||
}
|
||||
processTxs(txs);
|
||||
}
|
||||
|
||||
for (const txid of txids) {
|
||||
if (!found[txid]) {
|
||||
this.evict(txid, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getLatestRbfSummary(): ReplacementInfo[] {
|
||||
const rbfList = this.getRbfTrees(false);
|
||||
return rbfList.slice(0, 6).map(rbfTree => {
|
||||
|
||||
@@ -23,24 +23,21 @@ class RedisCache {
|
||||
private cacheQueue: MempoolTransactionExtended[] = [];
|
||||
private txFlushLimit: number = 10000;
|
||||
|
||||
constructor() {
|
||||
if (config.REDIS.ENABLED) {
|
||||
const redisConfig = {
|
||||
socket: {
|
||||
path: config.REDIS.UNIX_SOCKET_PATH
|
||||
},
|
||||
database: NetworkDB[config.MEMPOOL.NETWORK],
|
||||
};
|
||||
this.client = createClient(redisConfig);
|
||||
this.client.on('error', (e) => {
|
||||
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
|
||||
});
|
||||
this.$ensureConnected();
|
||||
}
|
||||
}
|
||||
|
||||
private async $ensureConnected(): Promise<void> {
|
||||
if (!this.connected && config.REDIS.ENABLED) {
|
||||
if (!this.client) {
|
||||
const redisConfig = {
|
||||
socket: {
|
||||
path: config.REDIS.UNIX_SOCKET_PATH
|
||||
},
|
||||
database: NetworkDB[config.MEMPOOL.NETWORK],
|
||||
};
|
||||
this.client = createClient(redisConfig);
|
||||
this.client.on('error', (e) => {
|
||||
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
|
||||
});
|
||||
}
|
||||
|
||||
return this.client.connect().then(async () => {
|
||||
this.connected = true;
|
||||
logger.info(`Redis client connected`);
|
||||
@@ -122,9 +119,8 @@ class RedisCache {
|
||||
async $removeTransactions(transactions: string[]) {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE;
|
||||
for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) {
|
||||
const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) {
|
||||
const slice = transactions.slice(i * 10000, (i + 1) * 10000);
|
||||
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
|
||||
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
|
||||
}
|
||||
@@ -220,7 +216,7 @@ class RedisCache {
|
||||
await memPool.$setMempool(loadedMempool);
|
||||
await rbfCache.load({
|
||||
txs: rbfTxs,
|
||||
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
|
||||
trees: rbfTrees.map(loadedTree => loadedTree.value),
|
||||
expiring: rbfExpirations,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ class StatisticsApi {
|
||||
mempool_byte_weight,
|
||||
fee_data,
|
||||
total_fee,
|
||||
min_fee,
|
||||
vsize_1,
|
||||
vsize_2,
|
||||
vsize_3,
|
||||
@@ -55,7 +54,7 @@ class StatisticsApi {
|
||||
vsize_1800,
|
||||
vsize_2000
|
||||
)
|
||||
VALUES (NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
VALUES (NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)`;
|
||||
const [result]: any = await DB.query(query);
|
||||
return result.insertId;
|
||||
@@ -74,7 +73,6 @@ class StatisticsApi {
|
||||
mempool_byte_weight,
|
||||
fee_data,
|
||||
total_fee,
|
||||
min_fee,
|
||||
vsize_1,
|
||||
vsize_2,
|
||||
vsize_3,
|
||||
@@ -114,7 +112,7 @@ class StatisticsApi {
|
||||
vsize_1800,
|
||||
vsize_2000
|
||||
)
|
||||
VALUES (${statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
VALUES (${statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
const params: (string | number)[] = [
|
||||
@@ -124,7 +122,6 @@ class StatisticsApi {
|
||||
statistics.mempool_byte_weight,
|
||||
statistics.fee_data,
|
||||
statistics.total_fee,
|
||||
statistics.min_fee,
|
||||
statistics.vsize_1,
|
||||
statistics.vsize_2,
|
||||
statistics.vsize_3,
|
||||
@@ -174,9 +171,7 @@ class StatisticsApi {
|
||||
private getQueryForDaysAvg(div: number, interval: string) {
|
||||
return `SELECT
|
||||
UNIX_TIMESTAMP(added) as added,
|
||||
CAST(avg(unconfirmed_transactions) as DOUBLE) as unconfirmed_transactions,
|
||||
CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second,
|
||||
CAST(avg(min_fee) as DOUBLE) as min_fee,
|
||||
CAST(avg(vsize_1) as DOUBLE) as vsize_1,
|
||||
CAST(avg(vsize_2) as DOUBLE) as vsize_2,
|
||||
CAST(avg(vsize_3) as DOUBLE) as vsize_3,
|
||||
@@ -224,9 +219,7 @@ class StatisticsApi {
|
||||
private getQueryForDays(div: number, interval: string) {
|
||||
return `SELECT
|
||||
UNIX_TIMESTAMP(added) as added,
|
||||
CAST(avg(unconfirmed_transactions) as DOUBLE) as unconfirmed_transactions,
|
||||
CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second,
|
||||
CAST(avg(min_fee) as DOUBLE) as min_fee,
|
||||
vsize_1,
|
||||
vsize_2,
|
||||
vsize_3,
|
||||
@@ -408,11 +401,9 @@ class StatisticsApi {
|
||||
return statistic.map((s) => {
|
||||
return {
|
||||
added: s.added,
|
||||
count: s.unconfirmed_transactions,
|
||||
vbytes_per_second: s.vbytes_per_second,
|
||||
mempool_byte_weight: s.mempool_byte_weight,
|
||||
total_fee: s.total_fee,
|
||||
min_fee: s.min_fee,
|
||||
vsizes: [
|
||||
s.vsize_1,
|
||||
s.vsize_2,
|
||||
|
||||
@@ -89,9 +89,6 @@ class Statistics {
|
||||
}
|
||||
});
|
||||
|
||||
// get minFee and convert to sats/vb
|
||||
const minFee = memPool.getMempoolInfo().mempoolminfee * 100000;
|
||||
|
||||
try {
|
||||
const insertId = await statisticsApi.$create({
|
||||
added: 'NOW()',
|
||||
@@ -101,7 +98,6 @@ class Statistics {
|
||||
mempool_byte_weight: totalWeight,
|
||||
total_fee: totalFee,
|
||||
fee_data: '',
|
||||
min_fee: minFee,
|
||||
vsize_1: weightVsizeFees['1'] || 0,
|
||||
vsize_2: weightVsizeFees['2'] || 0,
|
||||
vsize_3: weightVsizeFees['3'] || 0,
|
||||
|
||||
@@ -5,7 +5,6 @@ import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import logger from '../logger';
|
||||
import config from '../config';
|
||||
import pLimit from '../utils/p-limit';
|
||||
|
||||
class TransactionUtils {
|
||||
constructor() { }
|
||||
@@ -75,12 +74,8 @@ class TransactionUtils {
|
||||
|
||||
public async $getMempoolTransactionsExtended(txids: string[], addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended[]> {
|
||||
if (forceCore || config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
const limiter = pLimit(8); // Run 8 requests at a time
|
||||
const results = await Promise.allSettled(txids.map(
|
||||
txid => limiter(() => this.$getMempoolTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore))
|
||||
));
|
||||
return results.filter(reply => reply.status === 'fulfilled')
|
||||
.map(r => (r as PromiseFulfilledResult<MempoolTransactionExtended>).value);
|
||||
const results = await Promise.allSettled(txids.map(txid => this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore, true)));
|
||||
return (results.filter(r => r.status === 'fulfilled') as PromiseFulfilledResult<MempoolTransactionExtended>[]).map(r => r.value);
|
||||
} else {
|
||||
const transactions = await bitcoinApi.$getMempoolTransactions(txids);
|
||||
return transactions.map(transaction => {
|
||||
@@ -116,7 +111,7 @@ class TransactionUtils {
|
||||
public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended {
|
||||
const vsize = Math.ceil(transaction.weight / 4);
|
||||
const fractionalVsize = (transaction.weight / 4);
|
||||
let sigops = Common.isLiquid() ? 0 : (transaction.sigops != null ? transaction.sigops : this.countSigops(transaction));
|
||||
const sigops = !Common.isLiquid() ? this.countSigops(transaction) : 0;
|
||||
// https://github.com/bitcoin/bitcoin/blob/e9262ea32a6e1d364fb7974844fadc36f931f8c6/src/policy/policy.cpp#L295-L298
|
||||
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
|
||||
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
||||
@@ -155,7 +150,7 @@ class TransactionUtils {
|
||||
sigops += 20 * (script.match(/OP_CHECKMULTISIG/g)?.length || 0);
|
||||
} else {
|
||||
// in redeem scripts and witnesses, worth N if preceded by OP_N, 20 otherwise
|
||||
const matches = script.matchAll(/(?:OP_(?:PUSHNUM_)?(\d+))? OP_CHECKMULTISIG/g);
|
||||
const matches = script.matchAll(/(?:OP_(\d+))? OP_CHECKMULTISIG/g);
|
||||
for (const match of matches) {
|
||||
const n = parseInt(match[1]);
|
||||
if (Number.isInteger(n)) {
|
||||
@@ -189,12 +184,6 @@ class TransactionUtils {
|
||||
sigops += this.countScriptSigops(bitcoinjs.script.toASM(Buffer.from(input.witness[input.witness.length - 1], 'hex')), false, true);
|
||||
}
|
||||
break;
|
||||
|
||||
case input.prevout.scriptpubkey_type === 'p2sh':
|
||||
if (input.inner_redeemscript_asm) {
|
||||
sigops += this.countScriptSigops(input.inner_redeemscript_asm);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,13 +94,9 @@ class WebsocketHandler {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.wss.on('connection', (client: WebSocket, req) => {
|
||||
this.wss.on('connection', (client: WebSocket) => {
|
||||
this.numConnected++;
|
||||
client['remoteAddress'] = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown';
|
||||
client.on('error', (e) => {
|
||||
logger.info(`websocket client error from ${client['remoteAddress']}: ` + (e instanceof Error ? e.message : e));
|
||||
client.close();
|
||||
});
|
||||
client.on('error', logger.info);
|
||||
client.on('close', () => {
|
||||
this.numDisconnected++;
|
||||
});
|
||||
@@ -286,8 +282,7 @@ class WebsocketHandler {
|
||||
client.send(serializedResponse);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug(`Error parsing websocket message from ${client['remoteAddress']}: ` + (e instanceof Error ? e.message : e));
|
||||
client.close();
|
||||
logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -491,7 +486,6 @@ class WebsocketHandler {
|
||||
|
||||
// pre-compute address transactions
|
||||
const addressCache = this.makeAddressCache(newTransactions);
|
||||
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
||||
|
||||
this.wss.clients.forEach(async (client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
@@ -532,15 +526,11 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (client['track-address']) {
|
||||
const newTransactions = Array.from(addressCache[client['track-address']]?.values() || []);
|
||||
const removedTransactions = Array.from(removedAddressCache[client['track-address']]?.values() || []);
|
||||
const foundTransactions = Array.from(addressCache[client['track-address']]?.values() || []);
|
||||
// txs may be missing prevouts in non-esplora backends
|
||||
// so fetch the full transactions now
|
||||
const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(newTransactions) : newTransactions;
|
||||
const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(foundTransactions) : foundTransactions;
|
||||
|
||||
if (removedTransactions.length) {
|
||||
response['address-removed-transactions'] = JSON.stringify(removedTransactions);
|
||||
}
|
||||
if (fullTransactions.length) {
|
||||
response['address-transactions'] = JSON.stringify(fullTransactions);
|
||||
}
|
||||
@@ -582,7 +572,7 @@ class WebsocketHandler {
|
||||
response['utxoSpent'] = JSON.stringify(outspends);
|
||||
}
|
||||
|
||||
const rbfReplacedBy = rbfChanges.map[client['track-tx']] ? rbfCache.getReplacedBy(client['track-tx']) : false;
|
||||
const rbfReplacedBy = rbfCache.getReplacedBy(client['track-tx']);
|
||||
if (rbfReplacedBy) {
|
||||
response['rbfTransaction'] = JSON.stringify({
|
||||
txid: rbfReplacedBy,
|
||||
@@ -596,25 +586,13 @@ class WebsocketHandler {
|
||||
|
||||
const mempoolTx = newMempool[trackTxid];
|
||||
if (mempoolTx && mempoolTx.position) {
|
||||
const positionData = {
|
||||
response['txPosition'] = JSON.stringify({
|
||||
txid: trackTxid,
|
||||
position: {
|
||||
...mempoolTx.position,
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
}
|
||||
};
|
||||
if (mempoolTx.cpfpDirty) {
|
||||
positionData['cpfp'] = {
|
||||
ancestors: mempoolTx.ancestors,
|
||||
bestDescendant: mempoolTx.bestDescendant || null,
|
||||
descendants: mempoolTx.descendants || null,
|
||||
effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null,
|
||||
sigops: mempoolTx.sigops,
|
||||
adjustedVsize: mempoolTx.adjustedVsize,
|
||||
acceleration: mempoolTx.acceleration
|
||||
};
|
||||
}
|
||||
response['txPosition'] = JSON.stringify(positionData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,10 +43,7 @@ interface IConfig {
|
||||
ESPLORA: {
|
||||
REST_API_URL: string;
|
||||
UNIX_SOCKET_PATH: string | void | null;
|
||||
BATCH_QUERY_BASE_SIZE: number;
|
||||
RETRY_UNIX_SOCKET_AFTER: number;
|
||||
REQUEST_TIMEOUT: number;
|
||||
FALLBACK_TIMEOUT: number;
|
||||
FALLBACK: string[];
|
||||
};
|
||||
LIGHTNING: {
|
||||
@@ -79,8 +76,6 @@ interface IConfig {
|
||||
USERNAME: string;
|
||||
PASSWORD: string;
|
||||
TIMEOUT: number;
|
||||
COOKIE: boolean;
|
||||
COOKIE_PATH: string;
|
||||
};
|
||||
SECOND_CORE_RPC: {
|
||||
HOST: string;
|
||||
@@ -88,8 +83,6 @@ interface IConfig {
|
||||
USERNAME: string;
|
||||
PASSWORD: string;
|
||||
TIMEOUT: number;
|
||||
COOKIE: boolean;
|
||||
COOKIE_PATH: string;
|
||||
};
|
||||
DATABASE: {
|
||||
ENABLED: boolean;
|
||||
@@ -100,7 +93,6 @@ interface IConfig {
|
||||
USERNAME: string;
|
||||
PASSWORD: string;
|
||||
TIMEOUT: number;
|
||||
PID_DIR: string;
|
||||
};
|
||||
SYSLOG: {
|
||||
ENABLED: boolean;
|
||||
@@ -152,7 +144,6 @@ interface IConfig {
|
||||
REDIS: {
|
||||
ENABLED: boolean;
|
||||
UNIX_SOCKET_PATH: string;
|
||||
BATCH_QUERY_BASE_SIZE: number;
|
||||
},
|
||||
}
|
||||
|
||||
@@ -197,10 +188,7 @@ const defaults: IConfig = {
|
||||
'ESPLORA': {
|
||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||
'UNIX_SOCKET_PATH': null,
|
||||
'BATCH_QUERY_BASE_SIZE': 1000,
|
||||
'RETRY_UNIX_SOCKET_AFTER': 30000,
|
||||
'REQUEST_TIMEOUT': 10000,
|
||||
'FALLBACK_TIMEOUT': 5000,
|
||||
'FALLBACK': [],
|
||||
},
|
||||
'ELECTRUM': {
|
||||
@@ -214,8 +202,6 @@ const defaults: IConfig = {
|
||||
'USERNAME': 'mempool',
|
||||
'PASSWORD': 'mempool',
|
||||
'TIMEOUT': 60000,
|
||||
'COOKIE': false,
|
||||
'COOKIE_PATH': '/bitcoin/.cookie'
|
||||
},
|
||||
'SECOND_CORE_RPC': {
|
||||
'HOST': '127.0.0.1',
|
||||
@@ -223,8 +209,6 @@ const defaults: IConfig = {
|
||||
'USERNAME': 'mempool',
|
||||
'PASSWORD': 'mempool',
|
||||
'TIMEOUT': 60000,
|
||||
'COOKIE': false,
|
||||
'COOKIE_PATH': '/bitcoin/.cookie'
|
||||
},
|
||||
'DATABASE': {
|
||||
'ENABLED': true,
|
||||
@@ -235,7 +219,6 @@ const defaults: IConfig = {
|
||||
'USERNAME': 'mempool',
|
||||
'PASSWORD': 'mempool',
|
||||
'TIMEOUT': 180000,
|
||||
'PID_DIR': '',
|
||||
},
|
||||
'SYSLOG': {
|
||||
'ENABLED': true,
|
||||
@@ -306,7 +289,6 @@ const defaults: IConfig = {
|
||||
'REDIS': {
|
||||
'ENABLED': false,
|
||||
'UNIX_SOCKET_PATH': '',
|
||||
'BATCH_QUERY_BASE_SIZE': 5000,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import config from './config';
|
||||
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
|
||||
import { LogLevel } from './logger';
|
||||
import logger from './logger';
|
||||
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
class DB {
|
||||
constructor() {
|
||||
@@ -34,7 +30,7 @@ import { execSync } from 'child_process';
|
||||
}
|
||||
|
||||
public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
|
||||
OkPacket[] | ResultSetHeader>(query, params?, errorLogLevel: LogLevel | 'silent' = 'debug', connection?: PoolConnection): Promise<[T, FieldPacket[]]>
|
||||
OkPacket[] | ResultSetHeader>(query, params?, connection?: PoolConnection): Promise<[T, FieldPacket[]]>
|
||||
{
|
||||
this.checkDBFlag();
|
||||
let hardTimeout;
|
||||
@@ -56,38 +52,19 @@ import { execSync } from 'child_process';
|
||||
}).then(result => {
|
||||
resolve(result);
|
||||
}).catch(error => {
|
||||
if (errorLogLevel !== 'silent') {
|
||||
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
|
||||
}
|
||||
reject(error);
|
||||
}).finally(() => {
|
||||
clearTimeout(timer);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const pool = await this.getPool();
|
||||
return pool.query(query, params);
|
||||
} catch (e) {
|
||||
if (errorLogLevel !== 'silent') {
|
||||
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async $rollbackAtomic(connection: PoolConnection): Promise<void> {
|
||||
try {
|
||||
await connection.rollback();
|
||||
await connection.release();
|
||||
} catch (e) {
|
||||
logger.warn('Failed to rollback incomplete db transaction: ' + (e instanceof Error ? e.message : e));
|
||||
const pool = await this.getPool();
|
||||
return pool.query(query, params);
|
||||
}
|
||||
}
|
||||
|
||||
public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
|
||||
OkPacket[] | ResultSetHeader>(queries: { query, params }[], errorLogLevel: LogLevel | 'silent' = 'debug'): Promise<[T, FieldPacket[]][]>
|
||||
OkPacket[] | ResultSetHeader>(queries: { query, params }[]): Promise<[T, FieldPacket[]][]>
|
||||
{
|
||||
const pool = await this.getPool();
|
||||
const connection = await pool.getConnection();
|
||||
@@ -96,7 +73,7 @@ import { execSync } from 'child_process';
|
||||
|
||||
const results: [T, FieldPacket[]][] = [];
|
||||
for (const query of queries) {
|
||||
const result = await this.query(query.query, query.params, errorLogLevel, connection) as [T, FieldPacket[]];
|
||||
const result = await this.query(query.query, query.params, connection) as [T, FieldPacket[]];
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
@@ -104,8 +81,9 @@ import { execSync } from 'child_process';
|
||||
|
||||
return results;
|
||||
} catch (e) {
|
||||
logger.warn('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
|
||||
this.$rollbackAtomic(connection);
|
||||
logger.err('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
|
||||
connection.rollback();
|
||||
connection.release();
|
||||
throw e;
|
||||
} finally {
|
||||
connection.release();
|
||||
@@ -123,50 +101,6 @@ import { execSync } from 'child_process';
|
||||
}
|
||||
}
|
||||
|
||||
public getPidLock(): boolean {
|
||||
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
|
||||
this.enforcePidLock(filePath);
|
||||
fs.writeFileSync(filePath, `${process.pid}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
private enforcePidLock(filePath: string): void {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const pid = parseInt(fs.readFileSync(filePath, 'utf-8'));
|
||||
if (pid === process.pid) {
|
||||
logger.warn('PID file already exists for this process');
|
||||
return;
|
||||
}
|
||||
|
||||
let cmd;
|
||||
try {
|
||||
cmd = execSync(`ps -p ${pid} -o args=`);
|
||||
} catch (e) {
|
||||
logger.warn(`Stale PID file at ${filePath}, but no process running on that PID ${pid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd && cmd.toString()?.includes('node')) {
|
||||
const msg = `Another mempool nodejs process is already running on PID ${pid}`;
|
||||
logger.err(msg);
|
||||
throw new Error(msg);
|
||||
} else {
|
||||
logger.warn(`Stale PID file at ${filePath}, but the PID ${pid} does not belong to a running mempool instance`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public releasePidLock(): void {
|
||||
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const pid = parseInt(fs.readFileSync(filePath, 'utf-8'));
|
||||
// only release our own pid file
|
||||
if (pid === process.pid) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getPool(): Promise<Pool> {
|
||||
if (this.pool === null) {
|
||||
this.pool = createPool(this.poolConfig);
|
||||
|
||||
38
backend/src/index-workers/block-summary-worker.ts
Normal file
38
backend/src/index-workers/block-summary-worker.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { parentPort } from 'worker_threads';
|
||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||
import blocks from '../api/blocks';
|
||||
import config from '../config';
|
||||
import transactionUtils from '../api/transaction-utils';
|
||||
import bitcoinClient from '../api/bitcoin/bitcoin-client';
|
||||
|
||||
if (parentPort) {
|
||||
parentPort.on('message', async ({ hash, height }) => {
|
||||
if (hash != null && height != null) {
|
||||
await indexBlockSummary(hash, height);
|
||||
}
|
||||
|
||||
if (parentPort) {
|
||||
parentPort.postMessage(height);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function indexBlockSummary(hash: string, height: number): Promise<void> {
|
||||
let txs;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
} else {
|
||||
const block = await bitcoinClient.getBlock(hash, 2);
|
||||
txs = block.tx.map(tx => {
|
||||
tx.fee = Math.round(tx.fee * 100_000_000);
|
||||
tx.vout.forEach((vout) => {
|
||||
vout.value = Math.round(vout.value * 100000000);
|
||||
});
|
||||
tx.vsize = Math.round(tx.weight / 4); // required for backwards compatibility
|
||||
return tx;
|
||||
});
|
||||
}
|
||||
|
||||
const cpfpSummary = await blocks.$indexCPFP(hash, height, txs);
|
||||
await blocks.$getStrippedBlockTransactions(hash, true, true, cpfpSummary, height); // This will index the block summary
|
||||
}
|
||||
25
backend/src/index-workers/block-worker.ts
Normal file
25
backend/src/index-workers/block-worker.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { parentPort } from 'worker_threads';
|
||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||
import blocksRepository from '../repositories/BlocksRepository';
|
||||
import blocks from '../api/blocks';
|
||||
import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
|
||||
|
||||
if (parentPort) {
|
||||
parentPort.on('message', async (params) => {
|
||||
if (params.height != null) {
|
||||
await indexBlock(params.height);
|
||||
}
|
||||
|
||||
if (parentPort) {
|
||||
parentPort.postMessage(params.height);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function indexBlock(blockHeight: number): Promise<void> {
|
||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
|
||||
const transactions = await blocks['$getTransactionsExtended'](blockHash, block.height, true, null, true);
|
||||
const blockExtended = await blocks['$getBlockExtended'](block, transactions);
|
||||
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import { AxiosError } from 'axios';
|
||||
import v8 from 'v8';
|
||||
import { formatBytes, getBytesUnit } from './utils/format';
|
||||
import redisCache from './api/redis-cache';
|
||||
import rbfCache from './api/rbf-cache';
|
||||
|
||||
class Server {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
@@ -91,24 +92,11 @@ class Server {
|
||||
async startServer(worker = false): Promise<void> {
|
||||
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
|
||||
|
||||
// Register cleanup listeners for exit events
|
||||
['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'].forEach(event => {
|
||||
process.on(event, () => { this.onExit(event); });
|
||||
});
|
||||
process.on('uncaughtException', (error) => {
|
||||
this.onUnhandledException('uncaughtException', error);
|
||||
});
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
this.onUnhandledException('unhandledRejection', reason);
|
||||
});
|
||||
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
bitcoinApi.startHealthChecks();
|
||||
}
|
||||
|
||||
if (config.DATABASE.ENABLED) {
|
||||
DB.getPidLock();
|
||||
|
||||
await DB.checkDbConnection();
|
||||
try {
|
||||
if (process.env.npm_config_reindex_blocks === 'true') { // Re-index requests
|
||||
@@ -120,6 +108,8 @@ class Server {
|
||||
}
|
||||
}
|
||||
|
||||
rbfCache.init();
|
||||
|
||||
this.app
|
||||
.use((req: Request, res: Response, next: NextFunction) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
@@ -206,7 +196,7 @@ class Server {
|
||||
}
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1);
|
||||
if (numHandledBlocks === 0) {
|
||||
await memPool.$updateMempool(newMempool, pollRate);
|
||||
}
|
||||
@@ -319,19 +309,6 @@ class Server {
|
||||
this.lastHeapLogTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
onExit(exitEvent, code = 0): void {
|
||||
logger.debug(`onExit for signal: ${exitEvent}`);
|
||||
if (config.DATABASE.ENABLED) {
|
||||
DB.releasePidLock();
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
onUnhandledException(type, error): void {
|
||||
console.error(`${type}:`, error);
|
||||
this.onExit(type, 1);
|
||||
}
|
||||
}
|
||||
|
||||
((): Server => new Server())();
|
||||
|
||||
@@ -15,18 +15,11 @@ export interface CoreIndex {
|
||||
best_block_height: number;
|
||||
}
|
||||
|
||||
type TaskName = 'blocksPrices' | 'coinStatsIndex';
|
||||
|
||||
class Indexer {
|
||||
private runIndexer = true;
|
||||
private indexerRunning = false;
|
||||
private tasksRunning: { [key in TaskName]?: boolean; } = {};
|
||||
private tasksScheduled: { [key in TaskName]?: NodeJS.Timeout; } = {};
|
||||
private coreIndexes: CoreIndex[] = [];
|
||||
|
||||
public indexerIsRunning(): boolean {
|
||||
return this.indexerRunning;
|
||||
}
|
||||
runIndexer = true;
|
||||
indexerRunning = false;
|
||||
tasksRunning: string[] = [];
|
||||
coreIndexes: CoreIndex[] = [];
|
||||
|
||||
/**
|
||||
* Check which core index is available for indexing
|
||||
@@ -76,69 +69,33 @@ class Indexer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* schedules a single task to run in `timeout` ms
|
||||
* only one task of each type may be scheduled
|
||||
*
|
||||
* @param {TaskName} task - the type of task
|
||||
* @param {number} timeout - delay in ms
|
||||
* @param {boolean} replace - `true` replaces any already scheduled task (works like a debounce), `false` ignores subsequent requests (works like a throttle)
|
||||
*/
|
||||
public scheduleSingleTask(task: TaskName, timeout: number = 10000, replace = false): void {
|
||||
if (this.tasksScheduled[task]) {
|
||||
if (!replace) { //throttle
|
||||
return;
|
||||
} else { // debounce
|
||||
clearTimeout(this.tasksScheduled[task]);
|
||||
}
|
||||
}
|
||||
this.tasksScheduled[task] = setTimeout(async () => {
|
||||
try {
|
||||
await this.runSingleTask(task);
|
||||
} catch (e) {
|
||||
logger.err(`Unexpected error in scheduled task ${task}: ` + (e instanceof Error ? e.message : e));
|
||||
} finally {
|
||||
clearTimeout(this.tasksScheduled[task]);
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a single task immediately
|
||||
*
|
||||
* (use `scheduleSingleTask` instead to queue a task to run after some timeout)
|
||||
*/
|
||||
public async runSingleTask(task: TaskName): Promise<void> {
|
||||
if (!Common.indexingEnabled() || this.tasksRunning[task]) {
|
||||
public async runSingleTask(task: 'blocksPrices' | 'coinStatsIndex'): Promise<void> {
|
||||
if (!Common.indexingEnabled()) {
|
||||
return;
|
||||
}
|
||||
this.tasksRunning[task] = true;
|
||||
|
||||
switch (task) {
|
||||
case 'blocksPrices': {
|
||||
if (!['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
let lastestPriceId;
|
||||
try {
|
||||
lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
} catch (e) {
|
||||
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
|
||||
} if (priceUpdater.historyInserted === false || lastestPriceId === null) {
|
||||
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
|
||||
this.scheduleSingleTask(task, 10000);
|
||||
} else {
|
||||
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
|
||||
await mining.$indexBlockPrices();
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case 'coinStatsIndex': {
|
||||
logger.debug(`Indexing coinStatsIndex now`);
|
||||
await mining.$indexCoinStatsIndex();
|
||||
} break;
|
||||
if (task === 'blocksPrices' && !this.tasksRunning.includes(task) && !['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
this.tasksRunning.push(task);
|
||||
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||
if (priceUpdater.historyInserted === false || lastestPriceId === null) {
|
||||
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
|
||||
setTimeout(() => {
|
||||
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
|
||||
this.runSingleTask('blocksPrices');
|
||||
}, 10000);
|
||||
} else {
|
||||
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
|
||||
await mining.$indexBlockPrices();
|
||||
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
|
||||
}
|
||||
}
|
||||
|
||||
this.tasksRunning[task] = false;
|
||||
if (task === 'coinStatsIndex' && !this.tasksRunning.includes(task)) {
|
||||
this.tasksRunning.push(task);
|
||||
logger.debug(`Indexing coinStatsIndex now`);
|
||||
await mining.$indexCoinStatsIndex();
|
||||
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
|
||||
}
|
||||
}
|
||||
|
||||
public async $run(): Promise<void> {
|
||||
|
||||
@@ -157,6 +157,4 @@ class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
export type LogLevel = 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
|
||||
|
||||
export default new Logger();
|
||||
|
||||
@@ -104,7 +104,6 @@ export interface MempoolTransactionExtended extends TransactionExtended {
|
||||
adjustedFeePerVsize: number;
|
||||
inputs?: number[];
|
||||
lastBoosted?: number;
|
||||
cpfpDirty?: boolean;
|
||||
}
|
||||
|
||||
export interface AuditTransaction {
|
||||
@@ -300,7 +299,6 @@ export interface Statistic {
|
||||
total_fee: number;
|
||||
mempool_byte_weight: number;
|
||||
fee_data: string;
|
||||
min_fee: number;
|
||||
|
||||
vsize_1: number;
|
||||
vsize_2: number;
|
||||
@@ -347,7 +345,6 @@ export interface OptimizedStatistic {
|
||||
vbytes_per_second: number;
|
||||
total_fee: number;
|
||||
mempool_byte_weight: number;
|
||||
min_fee: number;
|
||||
vsizes: number[];
|
||||
}
|
||||
|
||||
|
||||
@@ -541,7 +541,7 @@ class BlocksRepository {
|
||||
*/
|
||||
public async $getBlocksDifficulty(): Promise<object[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks ORDER BY height ASC`);
|
||||
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks`);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
|
||||
@@ -14,7 +14,7 @@ class NodesSocketsRepository {
|
||||
await DB.query(`
|
||||
INSERT INTO nodes_sockets(public_key, socket, type)
|
||||
VALUE (?, ?, ?)
|
||||
`, [socket.publicKey, socket.addr, socket.network], 'silent');
|
||||
`, [socket.publicKey, socket.addr, socket.network]);
|
||||
} catch (e: any) {
|
||||
if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this
|
||||
logger.err(`Cannot save node socket (${[socket.publicKey, socket.addr, socket.network]}) into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
var http = require('http')
|
||||
var https = require('https')
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
var JsonRPC = function (opts) {
|
||||
// @ts-ignore
|
||||
@@ -56,13 +55,7 @@ JsonRPC.prototype.call = function (method, params) {
|
||||
}
|
||||
|
||||
// use HTTP auth if user and password set
|
||||
if (this.opts.cookie) {
|
||||
if (!this.cachedCookie) {
|
||||
this.cachedCookie = readFileSync(this.opts.cookie).toString();
|
||||
}
|
||||
// @ts-ignore
|
||||
requestOptions.auth = this.cachedCookie;
|
||||
} else if (this.opts.user && this.opts.pass) {
|
||||
if (this.opts.user && this.opts.pass) {
|
||||
// @ts-ignore
|
||||
requestOptions.auth = this.opts.user + ':' + this.opts.pass
|
||||
}
|
||||
@@ -100,7 +93,7 @@ JsonRPC.prototype.call = function (method, params) {
|
||||
reject(err)
|
||||
})
|
||||
|
||||
request.on('response', (response) => {
|
||||
request.on('response', function (response) {
|
||||
clearTimeout(reqTimeout)
|
||||
|
||||
// We need to buffer the response chunks in a nonblocking way.
|
||||
@@ -111,7 +104,7 @@ JsonRPC.prototype.call = function (method, params) {
|
||||
// When all the responses are finished, we decode the JSON and
|
||||
// depending on whether it's got a result or an error, we call
|
||||
// emitSuccess or emitError on the promise.
|
||||
response.on('end', () => {
|
||||
response.on('end', function () {
|
||||
var err
|
||||
|
||||
if (cbCalled) return
|
||||
@@ -120,14 +113,6 @@ JsonRPC.prototype.call = function (method, params) {
|
||||
try {
|
||||
var decoded = JSON.parse(buffer)
|
||||
} catch (e) {
|
||||
// if we authenticated using a cookie and it failed, read the cookie file again
|
||||
if (
|
||||
response.statusCode === 401 /* Unauthorized */ &&
|
||||
this.opts.cookie
|
||||
) {
|
||||
this.cachedCookie = undefined;
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
err = new Error('Invalid params, response status code: ' + response.statusCode)
|
||||
err.code = -32602
|
||||
|
||||
@@ -15,6 +15,8 @@ class ForensicsService {
|
||||
txCache: { [txid: string]: IEsploraApi.Transaction } = {};
|
||||
tempCached: string[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
public async $startService(): Promise<void> {
|
||||
logger.info('Starting lightning network forensics service');
|
||||
|
||||
@@ -64,138 +66,93 @@ class ForensicsService {
|
||||
*/
|
||||
|
||||
public async $runClosedChannelsForensics(onlyNewChannels: boolean = false): Promise<void> {
|
||||
// Only Esplora backend can retrieve spent transaction outputs
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
return;
|
||||
}
|
||||
|
||||
let progress = 0;
|
||||
|
||||
try {
|
||||
logger.debug(`Started running closed channel forensics...`);
|
||||
let allChannels;
|
||||
let channels;
|
||||
if (onlyNewChannels) {
|
||||
allChannels = await channelsApi.$getClosedChannelsWithoutReason();
|
||||
channels = await channelsApi.$getClosedChannelsWithoutReason();
|
||||
} else {
|
||||
allChannels = await channelsApi.$getUnresolvedClosedChannels();
|
||||
channels = await channelsApi.$getUnresolvedClosedChannels();
|
||||
}
|
||||
|
||||
let progress = 0;
|
||||
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 10);
|
||||
// process batches of 1000 channels
|
||||
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
|
||||
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
|
||||
let allOutspends: IEsploraApi.Outspend[][] = [];
|
||||
const forceClosedChannels: { channel: any, cachedSpends: string[] }[] = [];
|
||||
|
||||
// fetch outspends in bulk
|
||||
for (const channel of channels) {
|
||||
let reason = 0;
|
||||
let resolvedForceClose = false;
|
||||
// Only Esplora backend can retrieve spent transaction outputs
|
||||
const cached: string[] = [];
|
||||
try {
|
||||
const outspendTxids = channels.map(channel => channel.closing_transaction_id);
|
||||
allOutspends = await bitcoinApi.$getBatchedOutspendsInternal(outspendTxids);
|
||||
logger.info(`Fetched outspends for ${allOutspends.length} txs from esplora for LN forensics`);
|
||||
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
|
||||
} catch (e) {
|
||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/internal/txs/outspends/by-txid'}. Reason ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
// fetch spending transactions in bulk and load into txCache
|
||||
const newSpendingTxids: { [txid: string]: boolean } = {};
|
||||
for (const outspends of allOutspends) {
|
||||
let outspends: IEsploraApi.Outspend[] | undefined;
|
||||
try {
|
||||
outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id);
|
||||
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
|
||||
} catch (e) {
|
||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
|
||||
continue;
|
||||
}
|
||||
const lightningScriptReasons: number[] = [];
|
||||
for (const outspend of outspends) {
|
||||
if (outspend.spent && outspend.txid) {
|
||||
newSpendingTxids[outspend.txid] = true;
|
||||
let spendingTx = await this.fetchTransaction(outspend.txid);
|
||||
if (!spendingTx) {
|
||||
continue;
|
||||
}
|
||||
cached.push(spendingTx.txid);
|
||||
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
||||
lightningScriptReasons.push(lightningScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
const allOutspendTxs = await this.fetchTransactions(
|
||||
allOutspends.flatMap(outspends =>
|
||||
outspends
|
||||
.filter(outspend => outspend.spent && outspend.txid)
|
||||
.map(outspend => outspend.txid)
|
||||
)
|
||||
);
|
||||
logger.info(`Fetched ${allOutspendTxs.length} out-spending txs from esplora for LN forensics`);
|
||||
|
||||
// process each outspend
|
||||
for (const [index, channel] of channels.entries()) {
|
||||
let reason = 0;
|
||||
const cached: string[] = [];
|
||||
try {
|
||||
const outspends = allOutspends[index];
|
||||
if (!outspends || !outspends.length) {
|
||||
// outspends are missing
|
||||
const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
|
||||
if (filteredReasons.length) {
|
||||
if (filteredReasons.some((r) => r === 2 || r === 4)) {
|
||||
reason = 3;
|
||||
} else {
|
||||
reason = 2;
|
||||
resolvedForceClose = true;
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
We can detect a commitment transaction (force close) by reading Sequence and Locktime
|
||||
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
|
||||
*/
|
||||
let closingTx = await this.fetchTransaction(channel.closing_transaction_id, true);
|
||||
if (!closingTx) {
|
||||
continue;
|
||||
}
|
||||
const lightningScriptReasons: number[] = [];
|
||||
for (const outspend of outspends) {
|
||||
if (outspend.spent && outspend.txid) {
|
||||
const spendingTx = this.txCache[outspend.txid];
|
||||
if (!spendingTx) {
|
||||
continue;
|
||||
}
|
||||
cached.push(spendingTx.txid);
|
||||
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
||||
lightningScriptReasons.push(lightningScript);
|
||||
}
|
||||
cached.push(closingTx.txid);
|
||||
const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
|
||||
const locktimeHex: string = closingTx.locktime.toString(16);
|
||||
if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
|
||||
reason = 2; // Here we can't be sure if it's a penalty or not
|
||||
} else {
|
||||
reason = 1;
|
||||
}
|
||||
const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
|
||||
if (filteredReasons.length) {
|
||||
if (filteredReasons.some((r) => r === 2 || r === 4)) {
|
||||
// Force closed with penalty
|
||||
reason = 3;
|
||||
} else {
|
||||
// Force closed without penalty
|
||||
reason = 2;
|
||||
await DB.query(`UPDATE channels SET closing_resolved = ? WHERE id = ?`, [true, channel.id]);
|
||||
}
|
||||
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
|
||||
// clean up cached transactions
|
||||
}
|
||||
if (reason) {
|
||||
logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
|
||||
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
|
||||
if (reason === 2 && resolvedForceClose) {
|
||||
await DB.query(`UPDATE channels SET closing_resolved = ? WHERE id = ?`, [true, channel.id]);
|
||||
}
|
||||
if (reason !== 2 || resolvedForceClose) {
|
||||
cached.forEach(txid => {
|
||||
delete this.txCache[txid];
|
||||
});
|
||||
} else {
|
||||
forceClosedChannels.push({ channel, cachedSpends: cached });
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// fetch force-closing transactions in bulk
|
||||
const closingTxs = await this.fetchTransactions(forceClosedChannels.map(x => x.channel.closing_transaction_id));
|
||||
logger.info(`Fetched ${closingTxs.length} closing txs from esplora for LN forensics`);
|
||||
|
||||
// process channels with no lightning script reasons
|
||||
for (const { channel, cachedSpends } of forceClosedChannels) {
|
||||
const closingTx = this.txCache[channel.closing_transaction_id];
|
||||
if (!closingTx) {
|
||||
// no channel close transaction found yet
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
We can detect a commitment transaction (force close) by reading Sequence and Locktime
|
||||
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
|
||||
*/
|
||||
const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
|
||||
const locktimeHex: string = closingTx.locktime.toString(16);
|
||||
let reason;
|
||||
if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
|
||||
// Force closed, but we can't be sure if it's a penalty or not
|
||||
reason = 2;
|
||||
} else {
|
||||
// Mutually closed
|
||||
reason = 1;
|
||||
// clean up cached transactions
|
||||
delete this.txCache[closingTx.txid];
|
||||
for (const txid of cachedSpends) {
|
||||
delete this.txCache[txid];
|
||||
}
|
||||
}
|
||||
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
|
||||
} catch (e) {
|
||||
logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
|
||||
progress += channels.length;
|
||||
++progress;
|
||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||
if (elapsedSeconds > 10) {
|
||||
logger.debug(`Updating channel closed channel forensics ${progress}/${allChannels.length}`);
|
||||
logger.debug(`Updating channel closed channel forensics ${progress}/${channels.length}`);
|
||||
this.loggerTimer = new Date().getTime() / 1000;
|
||||
}
|
||||
}
|
||||
@@ -263,11 +220,8 @@ class ForensicsService {
|
||||
logger.debug(`Started running open channel forensics...`);
|
||||
const channels = await channelsApi.$getChannelsWithoutSourceChecked();
|
||||
|
||||
// preload open channel transactions
|
||||
await this.fetchTransactions(channels.map(channel => channel.transaction_id), true);
|
||||
|
||||
for (const openChannel of channels) {
|
||||
const openTx = this.txCache[openChannel.transaction_id];
|
||||
let openTx = await this.fetchTransaction(openChannel.transaction_id, true);
|
||||
if (!openTx) {
|
||||
continue;
|
||||
}
|
||||
@@ -322,7 +276,7 @@ class ForensicsService {
|
||||
|
||||
// Check if a channel open tx input spends the result of a swept channel close output
|
||||
private async $attributeSweptChannelCloses(openChannel: ILightningApi.Channel, input: IEsploraApi.Vin): Promise<void> {
|
||||
const sweepTx = await this.fetchTransaction(input.txid, true);
|
||||
let sweepTx = await this.fetchTransaction(input.txid, true);
|
||||
if (!sweepTx) {
|
||||
logger.err(`couldn't find input transaction for channel forensics ${openChannel.channel_id} ${input.txid}`);
|
||||
return;
|
||||
@@ -381,7 +335,7 @@ class ForensicsService {
|
||||
|
||||
if (matched && !ambiguous) {
|
||||
// fetch closing channel transaction and perform forensics on the outputs
|
||||
const prevChannelTx = await this.fetchTransaction(input.txid, true);
|
||||
let prevChannelTx = await this.fetchTransaction(input.txid, true);
|
||||
let outspends: IEsploraApi.Outspend[] | undefined;
|
||||
try {
|
||||
outspends = await bitcoinApi.$getOutspends(input.txid);
|
||||
@@ -401,17 +355,17 @@ class ForensicsService {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// preload outspend transactions
|
||||
await this.fetchTransactions(outspends.filter(o => o.spent && o.txid).map(o => o.txid), true);
|
||||
|
||||
for (let i = 0; i < outspends?.length; i++) {
|
||||
const outspend = outspends[i];
|
||||
const output = prevChannel.outputs[i];
|
||||
if (outspend.spent && outspend.txid) {
|
||||
const spendingTx = this.txCache[outspend.txid];
|
||||
if (spendingTx) {
|
||||
output.type = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
||||
try {
|
||||
const spendingTx = await this.fetchTransaction(outspend.txid, true);
|
||||
if (spendingTx) {
|
||||
output.type = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
} else {
|
||||
output.type = 0;
|
||||
@@ -476,36 +430,13 @@ class ForensicsService {
|
||||
}
|
||||
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
|
||||
} catch (e) {
|
||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + txid}. Reason ${e instanceof Error ? e.message : e}`);
|
||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + txid + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
|
||||
// fetches a batch of transactions and adds them to the txCache
|
||||
// the returned list of txs does *not* preserve ordering or number
|
||||
async fetchTransactions(txids, temp: boolean = false): Promise<(IEsploraApi.Transaction | null)[]> {
|
||||
// deduplicate txids
|
||||
const uniqueTxids = [...new Set<string>(txids)];
|
||||
// filter out any transactions we already have in the cache
|
||||
const needToFetch: string[] = uniqueTxids.filter(txid => !this.txCache[txid]);
|
||||
try {
|
||||
const txs = await bitcoinApi.$getRawTransactions(needToFetch);
|
||||
for (const tx of txs) {
|
||||
this.txCache[tx.txid] = tx;
|
||||
if (temp) {
|
||||
this.tempCached.push(tx.txid);
|
||||
}
|
||||
}
|
||||
await Common.sleep$(config.LIGHTNING.FORENSICS_RATE_LIMIT);
|
||||
} catch (e) {
|
||||
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/txs'}. Reason ${e instanceof Error ? e.message : e}`);
|
||||
return [];
|
||||
}
|
||||
return txids.map(txid => this.txCache[txid]);
|
||||
}
|
||||
|
||||
clearTempCache(): void {
|
||||
for (const txid of this.tempCached) {
|
||||
delete this.txCache[txid];
|
||||
|
||||
@@ -288,32 +288,22 @@ class NetworkSyncService {
|
||||
}
|
||||
logger.debug(`${log}`, logger.tags.ln);
|
||||
|
||||
const allChannels = await channelsApi.$getChannelsByStatus([0, 1]);
|
||||
|
||||
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 2);
|
||||
// process batches of 5000 channels
|
||||
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
|
||||
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||
const outspends = await bitcoinApi.$getOutSpendsByOutpoint(channels.map(channel => {
|
||||
return { txid: channel.transaction_id, vout: channel.transaction_vout };
|
||||
}));
|
||||
|
||||
for (const [index, channel] of channels.entries()) {
|
||||
const spendingTx = outspends[index];
|
||||
if (spendingTx.spent === true && spendingTx.status?.confirmed === true) {
|
||||
// logger.debug(`Marking channel: ${channel.id} as closed.`, logger.tags.ln);
|
||||
await DB.query(`UPDATE channels SET status = 2, closing_date = FROM_UNIXTIME(?) WHERE id = ?`,
|
||||
[spendingTx.status.block_time, channel.id]);
|
||||
if (spendingTx.txid && !channel.closing_transaction_id) {
|
||||
await DB.query(`UPDATE channels SET closing_transaction_id = ? WHERE id = ?`, [spendingTx.txid, channel.id]);
|
||||
}
|
||||
const channels = await channelsApi.$getChannelsByStatus([0, 1]);
|
||||
for (const channel of channels) {
|
||||
const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
|
||||
if (spendingTx.spent === true && spendingTx.status?.confirmed === true) {
|
||||
logger.debug(`Marking channel: ${channel.id} as closed.`, logger.tags.ln);
|
||||
await DB.query(`UPDATE channels SET status = 2, closing_date = FROM_UNIXTIME(?) WHERE id = ?`,
|
||||
[spendingTx.status.block_time, channel.id]);
|
||||
if (spendingTx.txid && !channel.closing_transaction_id) {
|
||||
await DB.query(`UPDATE channels SET closing_transaction_id = ? WHERE id = ?`, [spendingTx.txid, channel.id]);
|
||||
}
|
||||
}
|
||||
|
||||
progress += channels.length;
|
||||
++progress;
|
||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
|
||||
logger.debug(`Checking if channel has been closed ${progress}/${allChannels.length}`, logger.tags.ln);
|
||||
logger.debug(`Checking if channel has been closed ${progress}/${channels.length}`, logger.tags.ln);
|
||||
this.loggerTimer = new Date().getTime() / 1000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
software and associated documentation files (the "Software"), to deal in the Software
|
||||
without restriction, including without limitation the rights to use, copy, modify,
|
||||
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies
|
||||
or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
How it works:
|
||||
`this._head` is an instance of `Node` which keeps track of its current value and nests
|
||||
another instance of `Node` that keeps the value that comes after it. When a value is
|
||||
provided to `.enqueue()`, the code needs to iterate through `this._head`, going deeper
|
||||
and deeper to find the last value. However, iterating through every single item is slow.
|
||||
This problem is solved by saving a reference to the last value as `this._tail` so that
|
||||
it can reference it to add a new value.
|
||||
*/
|
||||
|
||||
class Node {
|
||||
value;
|
||||
next;
|
||||
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
class Queue {
|
||||
private _head;
|
||||
private _tail;
|
||||
private _size;
|
||||
|
||||
constructor() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
enqueue(value) {
|
||||
const node = new Node(value);
|
||||
|
||||
if (this._head) {
|
||||
this._tail.next = node;
|
||||
this._tail = node;
|
||||
} else {
|
||||
this._head = node;
|
||||
this._tail = node;
|
||||
}
|
||||
|
||||
this._size++;
|
||||
}
|
||||
|
||||
dequeue() {
|
||||
const current = this._head;
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._head = this._head.next;
|
||||
this._size--;
|
||||
return current.value;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._head = undefined;
|
||||
this._tail = undefined;
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
let current = this._head;
|
||||
|
||||
while (current) {
|
||||
yield current.value;
|
||||
current = current.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface LimitFunction {
|
||||
readonly activeCount: number;
|
||||
readonly pendingCount: number;
|
||||
clearQueue: () => void;
|
||||
<Arguments extends unknown[], ReturnType>(
|
||||
fn: (...args: Arguments) => PromiseLike<ReturnType> | ReturnType,
|
||||
...args: Arguments
|
||||
): Promise<ReturnType>;
|
||||
}
|
||||
|
||||
export default function pLimit(concurrency: number): LimitFunction {
|
||||
if (
|
||||
!(
|
||||
(Number.isInteger(concurrency) ||
|
||||
concurrency === Number.POSITIVE_INFINITY) &&
|
||||
concurrency > 0
|
||||
)
|
||||
) {
|
||||
throw new TypeError('Expected `concurrency` to be a number from 1 and up');
|
||||
}
|
||||
|
||||
const queue = new Queue();
|
||||
let activeCount = 0;
|
||||
|
||||
const next = () => {
|
||||
activeCount--;
|
||||
|
||||
if (queue.size > 0) {
|
||||
queue.dequeue()();
|
||||
}
|
||||
};
|
||||
|
||||
const run = async (fn, resolve, args) => {
|
||||
activeCount++;
|
||||
|
||||
const result = (async () => fn(...args))();
|
||||
|
||||
resolve(result);
|
||||
|
||||
try {
|
||||
await result;
|
||||
} catch {}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
const enqueue = (fn, resolve, args) => {
|
||||
queue.enqueue(run.bind(undefined, fn, resolve, args));
|
||||
|
||||
(async () => {
|
||||
// This function needs to wait until the next microtask before comparing
|
||||
// `activeCount` to `concurrency`, because `activeCount` is updated asynchronously
|
||||
// when the run function is dequeued and called. The comparison in the if-statement
|
||||
// needs to happen asynchronously as well to get an up-to-date value for `activeCount`.
|
||||
await Promise.resolve();
|
||||
|
||||
if (activeCount < concurrency && queue.size > 0) {
|
||||
queue.dequeue()();
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const generator = (fn, ...args) =>
|
||||
new Promise((resolve) => {
|
||||
enqueue(fn, resolve, args);
|
||||
});
|
||||
|
||||
Object.defineProperties(generator, {
|
||||
activeCount: {
|
||||
get: () => activeCount,
|
||||
},
|
||||
pendingCount: {
|
||||
get: () => queue.size,
|
||||
},
|
||||
clearQueue: {
|
||||
value: () => {
|
||||
queue.clear();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return generator as any;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of October 23, 2023.
|
||||
|
||||
Signed: TKone7
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of October 23, 2023.
|
||||
|
||||
Signed: fanquake
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
|
||||
|
||||
Signed: fubz
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 16, 2023.
|
||||
|
||||
Signed: ncois
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of September 12, 2023.
|
||||
|
||||
Signed: orange surf
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 15, 2023.
|
||||
|
||||
Signed: shubhamkmr04
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of Oct 13, 2023.
|
||||
|
||||
Signed starius
|
||||
@@ -164,9 +164,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
"PORT": 8332,
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool",
|
||||
"TIMEOUT": 60000,
|
||||
"COOKIE": false,
|
||||
"COOKIE_PATH": ""
|
||||
"TIMEOUT": 60000
|
||||
},
|
||||
```
|
||||
|
||||
@@ -179,8 +177,6 @@ Corresponding `docker-compose.yml` overrides:
|
||||
CORE_RPC_USERNAME: ""
|
||||
CORE_RPC_PASSWORD: ""
|
||||
CORE_RPC_TIMEOUT: 60000
|
||||
CORE_RPC_COOKIE: false
|
||||
CORE_RPC_COOKIE_PATH: ""
|
||||
...
|
||||
```
|
||||
|
||||
@@ -235,9 +231,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
"PORT": 8332,
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool",
|
||||
"TIMEOUT": 60000,
|
||||
"COOKIE": false,
|
||||
"COOKIE_PATH": ""
|
||||
"TIMEOUT": 60000
|
||||
},
|
||||
```
|
||||
|
||||
@@ -250,8 +244,6 @@ Corresponding `docker-compose.yml` overrides:
|
||||
SECOND_CORE_RPC_USERNAME: ""
|
||||
SECOND_CORE_RPC_PASSWORD: ""
|
||||
SECOND_CORE_RPC_TIMEOUT: ""
|
||||
SECOND_CORE_RPC_COOKIE: false
|
||||
SECOND_CORE_RPC_COOKIE_PATH: ""
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20.8.0-buster-slim AS builder
|
||||
FROM node:16.16.0-buster-slim AS builder
|
||||
|
||||
ARG commitHash
|
||||
ENV MEMPOOL_COMMIT_HASH=${commitHash}
|
||||
@@ -17,7 +17,7 @@ ENV PATH="/root/.cargo/bin:$PATH"
|
||||
RUN npm install --omit=dev --omit=optional
|
||||
RUN npm run package
|
||||
|
||||
FROM node:20.8.0-buster-slim
|
||||
FROM node:16.16.0-buster-slim
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
|
||||
@@ -41,9 +41,7 @@
|
||||
"PORT": __CORE_RPC_PORT__,
|
||||
"USERNAME": "__CORE_RPC_USERNAME__",
|
||||
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": __CORE_RPC_TIMEOUT__,
|
||||
"COOKIE": __CORE_RPC_COOKIE__,
|
||||
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
|
||||
"TIMEOUT": __CORE_RPC_TIMEOUT__
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "__ELECTRUM_HOST__",
|
||||
@@ -53,10 +51,7 @@
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "__ESPLORA_REST_API_URL__",
|
||||
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
|
||||
"BATCH_QUERY_BASE_SIZE": __ESPLORA_BATCH_QUERY_BASE_SIZE__,
|
||||
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
|
||||
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
|
||||
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
|
||||
"FALLBACK": __ESPLORA_FALLBACK__
|
||||
},
|
||||
"SECOND_CORE_RPC": {
|
||||
@@ -64,9 +59,7 @@
|
||||
"PORT": __SECOND_CORE_RPC_PORT__,
|
||||
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
|
||||
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__,
|
||||
"COOKIE": __SECOND_CORE_RPC_COOKIE__,
|
||||
"COOKIE_PATH": "__SECOND_CORE_RPC_COOKIE_PATH__"
|
||||
"TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__
|
||||
},
|
||||
"DATABASE": {
|
||||
"ENABLED": __DATABASE_ENABLED__,
|
||||
@@ -76,8 +69,7 @@
|
||||
"DATABASE": "__DATABASE_DATABASE__",
|
||||
"USERNAME": "__DATABASE_USERNAME__",
|
||||
"PASSWORD": "__DATABASE_PASSWORD__",
|
||||
"TIMEOUT": __DATABASE_TIMEOUT__,
|
||||
"PID_DIR": "__DATABASE_PID_DIR__"
|
||||
"TIMEOUT": __DATABASE_TIMEOUT__
|
||||
},
|
||||
"SYSLOG": {
|
||||
"ENABLED": __SYSLOG_ENABLED__,
|
||||
@@ -147,7 +139,6 @@
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": __REDIS_ENABLED__,
|
||||
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
|
||||
"BATCH_QUERY_BASE_SIZE": __REDIS_BATCH_QUERY_BASE_SIZE__
|
||||
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,6 @@ __CORE_RPC_PORT__=${CORE_RPC_PORT:=8332}
|
||||
__CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool}
|
||||
__CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
|
||||
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
|
||||
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
|
||||
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
|
||||
|
||||
# ELECTRUM
|
||||
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
|
||||
@@ -54,10 +52,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
|
||||
# ESPLORA
|
||||
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
|
||||
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
|
||||
__ESPLORA_BATCH_QUERY_BASE_SIZE__=${ESPLORA_BATCH_QUERY_BASE_SIZE:=1000}
|
||||
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
|
||||
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
|
||||
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
|
||||
__ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]}
|
||||
|
||||
# SECOND_CORE_RPC
|
||||
@@ -66,8 +61,6 @@ __SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332}
|
||||
__SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool}
|
||||
__SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool}
|
||||
__SECOND_CORE_RPC_TIMEOUT__=${SECOND_CORE_RPC_TIMEOUT:=60000}
|
||||
__SECOND_CORE_RPC_COOKIE__=${SECOND_CORE_RPC_COOKIE:=false}
|
||||
__SECOND_CORE_RPC_COOKIE_PATH__=${SECOND_CORE_RPC_COOKIE_PATH:=""}
|
||||
|
||||
# DATABASE
|
||||
__DATABASE_ENABLED__=${DATABASE_ENABLED:=true}
|
||||
@@ -78,7 +71,6 @@ __DATABASE_DATABASE__=${DATABASE_DATABASE:=mempool}
|
||||
__DATABASE_USERNAME__=${DATABASE_USERNAME:=mempool}
|
||||
__DATABASE_PASSWORD__=${DATABASE_PASSWORD:=mempool}
|
||||
__DATABASE_TIMEOUT__=${DATABASE_TIMEOUT:=180000}
|
||||
__DATABASE_PID_DIR__=${DATABASE_PID_DIR:=""}
|
||||
|
||||
# SYSLOG
|
||||
__SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false}
|
||||
@@ -147,9 +139,8 @@ __MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
|
||||
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||
|
||||
# REDIS
|
||||
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
|
||||
__REDIS_ENABLED__=${REDIS_ENABLED:=true}
|
||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
|
||||
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
|
||||
|
||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||
|
||||
@@ -194,8 +185,6 @@ sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_USERNAME__!${__CORE_RPC_USERNAME__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json
|
||||
sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
|
||||
|
||||
sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
|
||||
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json
|
||||
@@ -203,10 +192,7 @@ sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config
|
||||
|
||||
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_BATCH_QUERY_BASE_SIZE__!${__ESPLORA_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
|
||||
sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json
|
||||
|
||||
sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
|
||||
@@ -214,8 +200,6 @@ sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config
|
||||
sed -i "s!__SECOND_CORE_RPC_USERNAME__!${__SECOND_CORE_RPC_USERNAME__}!g" mempool-config.json
|
||||
sed -i "s!__SECOND_CORE_RPC_PASSWORD__!${__SECOND_CORE_RPC_PASSWORD__}!g" mempool-config.json
|
||||
sed -i "s!__SECOND_CORE_RPC_TIMEOUT__!${__SECOND_CORE_RPC_TIMEOUT__}!g" mempool-config.json
|
||||
sed -i "s!__SECOND_CORE_RPC_COOKIE__!${__SECOND_CORE_RPC_COOKIE__}!g" mempool-config.json
|
||||
sed -i "s!__SECOND_CORE_RPC_COOKIE_PATH__!${__SECOND_CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
|
||||
|
||||
sed -i "s!__DATABASE_ENABLED__!${__DATABASE_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_HOST__!${__DATABASE_HOST__}!g" mempool-config.json
|
||||
@@ -225,7 +209,6 @@ sed -i "s!__DATABASE_DATABASE__!${__DATABASE_DATABASE__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_USERNAME__!${__DATABASE_USERNAME__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_PASSWORD__!${__DATABASE_PASSWORD__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_TIMEOUT__!${__DATABASE_TIMEOUT__}!g" mempool-config.json
|
||||
sed -i "s!__DATABASE_PID_DIR__!${__DATABASE_PID_DIR__}!g" mempool-config.json
|
||||
|
||||
sed -i "s!__SYSLOG_ENABLED__!${__SYSLOG_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__SYSLOG_HOST__!${__SYSLOG_HOST__}!g" mempool-config.json
|
||||
@@ -291,6 +274,5 @@ sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS_
|
||||
# REDIS
|
||||
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||
sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
|
||||
|
||||
node /backend/package/index.js
|
||||
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
MYSQL_USER: "mempool"
|
||||
MYSQL_PASSWORD: "mempool"
|
||||
MYSQL_ROOT_PASSWORD: "admin"
|
||||
image: mariadb:10.5.21
|
||||
image: mariadb:10.5.8
|
||||
user: "1000:1000"
|
||||
restart: on-failure
|
||||
stop_grace_period: 1m
|
||||
|
||||
32
docker/electrum/Dockerfile
Normal file
32
docker/electrum/Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
FROM ubuntu:18.04
|
||||
MAINTAINER mempool.space developers
|
||||
EXPOSE 50002
|
||||
|
||||
# runs as UID 1000 GID 1000 inside the container
|
||||
|
||||
ENV VERSION 4.0.9
|
||||
RUN set -x \
|
||||
&& apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends gpg gpg-agent dirmngr \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends wget xpra python3-pyqt5 python3-wheel python3-pip python3-setuptools libsecp256k1-0 libsecp256k1-dev python3-numpy python3-dev build-essential \
|
||||
&& wget -O /tmp/Electrum-${VERSION}.tar.gz https://download.electrum.org/${VERSION}/Electrum-${VERSION}.tar.gz \
|
||||
&& wget -O /tmp/Electrum-${VERSION}.tar.gz.asc https://download.electrum.org/${VERSION}/Electrum-${VERSION}.tar.gz.asc \
|
||||
&& gpg --keyserver keys.gnupg.net --recv-keys 6694D8DE7BE8EE5631BED9502BD5824B7F9470E6 \
|
||||
&& gpg --verify /tmp/Electrum-${VERSION}.tar.gz.asc /tmp/Electrum-${VERSION}.tar.gz \
|
||||
&& pip3 install /tmp/Electrum-${VERSION}.tar.gz \
|
||||
&& test -f /usr/local/bin/electrum \
|
||||
&& rm -vrf /tmp/Electrum-${VERSION}.tar.gz /tmp/Electrum-${VERSION}.tar.gz.asc ${HOME}/.gnupg \
|
||||
&& apt-get purge --autoremove -y python3-wheel python3-pip python3-setuptools python3-dev build-essential libsecp256k1-dev curl gpg gpg-agent dirmngr \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
&& useradd -d /home/mempool -m mempool \
|
||||
&& mkdir /electrum \
|
||||
&& ln -s /electrum /home/mempool/.electrum \
|
||||
&& chown mempool:mempool /electrum
|
||||
|
||||
USER mempool
|
||||
ENV HOME /home/mempool
|
||||
WORKDIR /home/mempool
|
||||
VOLUME /electrum
|
||||
|
||||
CMD ["/usr/bin/xpra", "start", ":100", "--start-child=/usr/local/bin/electrum", "--bind-tcp=0.0.0.0:50002","--daemon=yes", "--notifications=no", "--mdns=no", "--pulseaudio=no", "--html=off", "--speaker=disabled", "--microphone=disabled", "--webcam=no", "--printing=no", "--dbus-launch=", "--exit-with-children"]
|
||||
ENTRYPOINT ["electrum"]
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20.8.0-buster-slim AS builder
|
||||
FROM node:16.16.0-buster-slim AS builder
|
||||
|
||||
ARG commitHash
|
||||
ENV DOCKER_COMMIT_HASH=${commitHash}
|
||||
@@ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.24.0-alpine
|
||||
FROM nginx:1.17.8-alpine
|
||||
|
||||
WORKDIR /patch
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ __AUDIT__=${AUDIT:=false}
|
||||
__MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__ACCELERATOR__=${ACCELERATOR:=false}
|
||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
||||
|
||||
# Export as environment variables to be used by envsubst
|
||||
@@ -66,7 +65,6 @@ export __AUDIT__
|
||||
export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __ACCELERATOR__
|
||||
export __HISTORICAL_PRICE__
|
||||
|
||||
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
|
||||
|
||||
1833
frontend/package-lock.json
generated
1833
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@
|
||||
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
|
||||
"start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
|
||||
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
|
||||
"sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'",
|
||||
"sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources'",
|
||||
"sync-assets-dev": "node sync-assets.js 'src/resources/'",
|
||||
"generate-config": "node generate-config.js",
|
||||
"build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js && npm run build-mempool-bisq-js",
|
||||
@@ -74,9 +74,9 @@
|
||||
"@angular/platform-server": "^16.2.2",
|
||||
"@angular/router": "^16.2.2",
|
||||
"@fortawesome/angular-fontawesome": "~0.13.0",
|
||||
"@fortawesome/fontawesome-common-types": "~6.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "~6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "~6.5.1",
|
||||
"@fortawesome/fontawesome-common-types": "~6.4.0",
|
||||
"@fortawesome/fontawesome-svg-core": "~6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "~6.4.0",
|
||||
"@mempool/mempool.js": "2.3.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^15.1.0",
|
||||
"@types/qrcode": "~1.5.0",
|
||||
@@ -85,12 +85,13 @@
|
||||
"clipboard": "^2.0.11",
|
||||
"domino": "^2.1.6",
|
||||
"echarts": "~5.4.3",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"lightweight-charts": "~3.8.0",
|
||||
"ngx-echarts": "~16.2.0",
|
||||
"ngx-echarts": "~16.0.0",
|
||||
"ngx-infinite-scroll": "^16.0.0",
|
||||
"qrcode": "1.5.1",
|
||||
"rxjs": "~7.8.1",
|
||||
"tinyify": "^3.1.0",
|
||||
"tinyify": "^4.0.0",
|
||||
"tlite": "^0.1.9",
|
||||
"tslib": "~2.6.0",
|
||||
"zone.js": "~0.13.1"
|
||||
@@ -110,10 +111,10 @@
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.6.0",
|
||||
"cypress-fail-on-console-error": "~5.0.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
"cypress": "^12.17.2",
|
||||
"cypress-fail-on-console-error": "~4.0.3",
|
||||
"cypress-wait-until": "^2.0.0",
|
||||
"mock-socket": "~9.2.1",
|
||||
"start-server-and-test": "~2.0.0"
|
||||
},
|
||||
"scarfSettings": {
|
||||
|
||||
@@ -112,14 +112,6 @@ PROXY_CONFIG.push(...[
|
||||
"^/testnet": ""
|
||||
},
|
||||
},
|
||||
{
|
||||
context: ['/api/v1/services/**'],
|
||||
target: `http://localhost:9000`,
|
||||
secure: false,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
},
|
||||
{
|
||||
context: ['/api/v1/**'],
|
||||
target: `http://127.0.0.1:8999`,
|
||||
|
||||
@@ -112,14 +112,6 @@ PROXY_CONFIG.push(...[
|
||||
"^/testnet": ""
|
||||
},
|
||||
},
|
||||
{
|
||||
context: ['/api/v1/services/**'],
|
||||
target: `http://localhost:9000`,
|
||||
secure: false,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
},
|
||||
{
|
||||
context: ['/api/v1/**'],
|
||||
target: `http://localhost:8999`,
|
||||
|
||||
@@ -95,14 +95,6 @@ if (configContent && configContent.BASE_MODULE === 'bisq') {
|
||||
}
|
||||
|
||||
PROXY_CONFIG.push(...[
|
||||
{
|
||||
context: ['/api/v1/services/**'],
|
||||
target: `http://localhost:9000`,
|
||||
secure: false,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
},
|
||||
{
|
||||
context: ['/api/v1/**'],
|
||||
target: `http://localhost:8999`,
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy'
|
||||
import { BlockViewComponent } from './components/block-view/block-view.component';
|
||||
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
|
||||
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
import { TransactionComponent } from './components/transaction/transaction.component';
|
||||
import { BlockComponent } from './components/block/block.component';
|
||||
import { ClockComponent } from './components/clock/clock.component';
|
||||
import { AddressComponent } from './components/address/address.component';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
import { AboutComponent } from './components/about/about.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
|
||||
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
|
||||
import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component';
|
||||
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
||||
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
|
||||
import { BlocksList } from './components/blocks-list/blocks-list.component';
|
||||
import { RbfList } from './components/rbf-list/rbf-list.component';
|
||||
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
|
||||
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
|
||||
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
|
||||
import { AssetsComponent } from './components/assets/assets.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
|
||||
import { CalculatorComponent } from './components/calculator/calculator.component';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
@@ -18,13 +35,95 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
component: MasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'mining/blocks',
|
||||
redirectTo: 'blocks',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: BlocksList,
|
||||
},
|
||||
{
|
||||
path: 'rbf',
|
||||
component: RbfList,
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
component: PrivacyPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'trademark-policy',
|
||||
component: TrademarkPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
component: StartComponent,
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
component: StartComponent,
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: BlockComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'lightning',
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
|
||||
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
@@ -33,8 +132,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
@@ -53,13 +151,88 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
component: MasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: BlocksList,
|
||||
},
|
||||
{
|
||||
path: 'rbf',
|
||||
component: RbfList,
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
component: PrivacyPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'trademark-policy',
|
||||
component: TrademarkPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: BlockComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'lightning',
|
||||
data: { networks: ['bitcoin'] },
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
@@ -68,8 +241,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
@@ -80,13 +252,97 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
component: MasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'mining/blocks',
|
||||
redirectTo: 'blocks',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: BlocksList,
|
||||
},
|
||||
{
|
||||
path: 'rbf',
|
||||
component: RbfList,
|
||||
},
|
||||
{
|
||||
path: 'tools/calculator',
|
||||
component: CalculatorComponent
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
component: PrivacyPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'trademark-policy',
|
||||
component: TrademarkPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: BlockComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'lightning',
|
||||
data: { networks: ['bitcoin'] },
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
@@ -117,18 +373,6 @@ let routes: Routes = [
|
||||
path: 'clock/:mode/:index',
|
||||
component: ClockComponent,
|
||||
},
|
||||
{
|
||||
path: 'view/block/:id',
|
||||
component: BlockViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'view/mempool-block/:index',
|
||||
component: MempoolBlockViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'view/blocks',
|
||||
component: EightBlocksComponent,
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
@@ -136,8 +380,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
@@ -148,6 +391,7 @@ let routes: Routes = [
|
||||
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') {
|
||||
routes = [{
|
||||
path: '',
|
||||
component: BisqMasterPageComponent,
|
||||
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
|
||||
}];
|
||||
}
|
||||
@@ -160,13 +404,105 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
data: { preload: true },
|
||||
component: LiquidMasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: BlocksList,
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
component: PrivacyPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'trademark-policy',
|
||||
component: TrademarkPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: BlockComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
data: { networks: ['liquid'] },
|
||||
component: AssetsNavComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'all',
|
||||
data: { networks: ['liquid'] },
|
||||
component: AssetsComponent,
|
||||
},
|
||||
{
|
||||
path: 'asset/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetComponent
|
||||
},
|
||||
{
|
||||
path: 'group/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetGroupComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'all'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
@@ -175,8 +511,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
@@ -187,13 +522,110 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
data: { preload: true },
|
||||
component: LiquidMasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: BlocksList,
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
component: PrivacyPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'trademark-policy',
|
||||
component: TrademarkPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: BlockComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
data: { networks: ['liquid'] },
|
||||
component: AssetsNavComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'featured',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetsFeaturedComponent,
|
||||
},
|
||||
{
|
||||
path: 'all',
|
||||
data: { networks: ['liquid'] },
|
||||
component: AssetsComponent,
|
||||
},
|
||||
{
|
||||
path: 'asset/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetComponent
|
||||
},
|
||||
{
|
||||
path: 'group/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetGroupComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'featured'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
@@ -215,8 +647,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
|
||||
@@ -41,7 +41,6 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
|
||||
document.body.scrollTo(0, 0);
|
||||
this.addressString = params.get('id') || '';
|
||||
this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.address:See current balance, pending transactions, and history of confirmed transactions for BSQ address ${this.addressString}:INTERPOLATION:.`);
|
||||
|
||||
return this.bisqApiService.getAddress$(this.addressString)
|
||||
.pipe(
|
||||
|
||||
@@ -69,7 +69,6 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
|
||||
this.location.replaceState(
|
||||
this.router.createUrlTree(['/bisq/block/', hash]).toString()
|
||||
);
|
||||
this.seoService.updateCanonical(this.location.path());
|
||||
return this.bisqApiService.getBlock$(this.blockHash)
|
||||
.pipe(catchError(this.caughtHttpError.bind(this)));
|
||||
}),
|
||||
@@ -89,7 +88,6 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
|
||||
this.isLoading = false;
|
||||
this.blockHeight = block.height;
|
||||
this.seoService.setTitle($localize`:@@bisq-block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.hash}:BLOCK_HASH:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.block:See all BSQ transactions in Bitcoin block ${block.height}:BLOCK_HEIGHT: (block hash ${block.hash}:BLOCK_HASH:).`);
|
||||
this.block = block;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ export class BisqBlocksComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.blocks:See a list of recent Bitcoin blocks with BSQ transactions, total BSQ sent per block, and more.`);
|
||||
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
|
||||
this.loadingItems = Array(this.itemsPerPage);
|
||||
if (document.body.clientWidth < 670) {
|
||||
|
||||
@@ -29,8 +29,7 @@ export class BisqDashboardComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project™. See Bisq market prices, trading activity, and more.`);
|
||||
this.seoService.setTitle(`Markets`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
|
||||
|
||||
@@ -34,7 +34,6 @@ export class BisqMainDashboardComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.resetTitle();
|
||||
this.seoService.resetDescription();
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.usdPrice$ = this.stateService.conversions$.asObservable().pipe(
|
||||
|
||||
@@ -48,8 +48,7 @@ export class BisqMarketComponent implements OnInit, OnDestroy {
|
||||
map(([markets, routeParams]) => {
|
||||
const pair = routeParams.get('pair');
|
||||
const pairUpperCase = pair.replace('_', '/').toUpperCase();
|
||||
this.seoService.setTitle($localize`:@@meta.title.bisq.market:Bisq market: ${pairUpperCase}`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.market:See price history, current buy/sell offers, and latest trades for the ${pairUpperCase} market on Bisq.`);
|
||||
this.seoService.setTitle(`Bisq market: ${pairUpperCase}`);
|
||||
|
||||
return {
|
||||
pair: pairUpperCase,
|
||||
|
||||
@@ -26,7 +26,6 @@ export class BisqStatsComponent implements OnInit {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.stats:See high-level stats on the BSQ economy: supply metrics, number of addresses, BSQ price, market cap, and more.`);
|
||||
this.stateService.bsqPrice$
|
||||
.subscribe((bsqPrice) => {
|
||||
this.price = bsqPrice;
|
||||
|
||||
@@ -48,7 +48,6 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
|
||||
document.body.scrollTo(0, 0);
|
||||
this.txId = params.get('id') || '';
|
||||
this.seoService.setTitle($localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.transaction:See inputs, outputs, transaction type, burnt amount, and more for transaction with txid ${this.txId}:INTERPOLATION:.`);
|
||||
if (history.state.data) {
|
||||
return of(history.state.data);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,6 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy {
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.transactions:See recent BSQ transactions: amount, txid, associated Bitcoin block, transaction type, and more.`);
|
||||
|
||||
this.radioGroupForm = this.formBuilder.group({
|
||||
txTypes: [this.txTypesDefaultChecked],
|
||||
|
||||
@@ -27,11 +27,9 @@ import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/auto
|
||||
import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe';
|
||||
import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive';
|
||||
import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component';
|
||||
import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
BisqMasterPageComponent,
|
||||
BisqTransactionsComponent,
|
||||
BisqTransactionComponent,
|
||||
BisqBlockComponent,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component';
|
||||
import { AboutComponent } from '../components/about/about.component';
|
||||
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
|
||||
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
|
||||
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
|
||||
@@ -10,83 +10,78 @@ import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
|
||||
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
|
||||
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
|
||||
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
|
||||
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
|
||||
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: BisqMasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: BisqMainDashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'markets',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqDashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'transactions',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqTransactionsComponent
|
||||
},
|
||||
{
|
||||
path: 'market/:pair',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqMarketComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqTransactionComponent
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
children: [],
|
||||
component: BisqBlocksComponent
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqBlockComponent,
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqAddressComponent,
|
||||
},
|
||||
{
|
||||
path: 'stats',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqStatsComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
loadChildren: () => import('../components/about/about.module').then(m => m.AboutModule),
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
loadChildren: () => import('../components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
path: '',
|
||||
component: BisqMainDashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'markets',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqDashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'transactions',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqTransactionsComponent
|
||||
},
|
||||
{
|
||||
path: 'market/:pair',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqMarketComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqTransactionComponent
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
children: [],
|
||||
component: BisqBlocksComponent
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqBlockComponent,
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqAddressComponent,
|
||||
},
|
||||
{
|
||||
path: 'stats',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqStatsComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: MasterPageComponent,
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
|
||||
data: { preload: true },
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class BitcoinGraphsRoutingModule { }
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
BitcoinGraphsRoutingModule,
|
||||
],
|
||||
})
|
||||
export class BitcoinGraphsModule { }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ const witnessSize = (vin: Vin) => vin.witness ? vin.witness.reduce((S, w) => S +
|
||||
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
|
||||
|
||||
// Power of ten wrapper
|
||||
export function selectPowerOfTen(val: number, multiplier = 1): { divider: number, unit: string } {
|
||||
export function selectPowerOfTen(val: number): { divider: number, unit: string } {
|
||||
const powerOfTen = {
|
||||
exa: Math.pow(10, 18),
|
||||
peta: Math.pow(10, 15),
|
||||
@@ -236,17 +236,17 @@ export function selectPowerOfTen(val: number, multiplier = 1): { divider: number
|
||||
};
|
||||
|
||||
let selectedPowerOfTen: { divider: number, unit: string };
|
||||
if (val < powerOfTen.kilo * multiplier) {
|
||||
if (val < powerOfTen.kilo) {
|
||||
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
|
||||
} else if (val < powerOfTen.mega * multiplier) {
|
||||
} else if (val < powerOfTen.mega) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' };
|
||||
} else if (val < powerOfTen.giga * multiplier) {
|
||||
} else if (val < powerOfTen.giga) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
|
||||
} else if (val < powerOfTen.tera * multiplier) {
|
||||
} else if (val < powerOfTen.tera) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
|
||||
} else if (val < powerOfTen.peta * multiplier) {
|
||||
} else if (val < powerOfTen.peta) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.tera, unit: 'T' };
|
||||
} else if (val < powerOfTen.exa * multiplier) {
|
||||
} else if (val < powerOfTen.exa) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
|
||||
} else {
|
||||
selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' };
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<div id="become-sponsor-container">
|
||||
<div class="become-sponsor community">
|
||||
<p style="font-weight: 700; font-size: 18px;">If you're an individual...</p>
|
||||
<a href="https://mempool.space/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button" (click)="onSponsorClick($event)">Become a Community Sponsor</a>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Exclusive swag</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Your avatar on the About page</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> And more coming soon :)</p>
|
||||
</div>
|
||||
<div class="become-sponsor enterprise">
|
||||
<p style="font-weight: 700; font-size: 18px;">If you're a business...</p>
|
||||
<a href="https://mempool.space/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button" (click)="onEnterpriseClick($event)">Become an Enterprise Sponsor</a>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Increased API limits</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Co-branded instance</p>
|
||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,45 +0,0 @@
|
||||
#become-sponsor-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin: 68px auto;
|
||||
}
|
||||
|
||||
.become-sponsor {
|
||||
background-color: #1d1f31;
|
||||
border-radius: 16px;
|
||||
padding: 12px 20px;
|
||||
width: 400px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.become-sponsor a {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#become-sponsor-container .btn {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
#become-sponsor-container .ng-fa-icon {
|
||||
color: #2ecc71;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#become-sponsor-container .sponsor-feature {
|
||||
text-align: left;
|
||||
width: 250px;
|
||||
margin: 12px auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
||||
#become-sponsor-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about-sponsors',
|
||||
templateUrl: './about-sponsors.component.html',
|
||||
styleUrls: ['./about-sponsors.component.scss'],
|
||||
})
|
||||
export class AboutSponsorsComponent {
|
||||
constructor(private enterpriseService: EnterpriseService) {
|
||||
}
|
||||
|
||||
onSponsorClick(e): boolean {
|
||||
this.enterpriseService.goal(5);
|
||||
return true;
|
||||
}
|
||||
|
||||
onEnterpriseClick(e): boolean {
|
||||
this.enterpriseService.goal(6);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,12 @@
|
||||
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">®</span>
|
||||
<img class="logo" src="/resources/mempool-logo-bigger.png" />
|
||||
<div class="version">
|
||||
<span>v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]</span>
|
||||
<span *ngIf="stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE">[{{ stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE }}]</span>
|
||||
v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="about-text">
|
||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ®</ng-template></h5>
|
||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></h5>
|
||||
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
|
||||
</div>
|
||||
|
||||
@@ -32,8 +31,12 @@
|
||||
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
|
||||
</video>
|
||||
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
<app-about-sponsors></app-about-sponsors>
|
||||
<ng-container *ngIf="false && officialMempoolSpace">
|
||||
<h3 class="mt-5">Sponsor the project</h3>
|
||||
<div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px">
|
||||
<a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Community</a>
|
||||
<a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Enterprise</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="enterprise-sponsor" id="enterprise-sponsors">
|
||||
@@ -182,14 +185,14 @@
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
|
||||
<div *ngIf="profiles$ | async as profiles" id="community-sponsors">
|
||||
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
|
||||
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
|
||||
<div class="wrapper">
|
||||
<ng-container>
|
||||
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
|
||||
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
|
||||
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -201,7 +204,7 @@
|
||||
<div class="wrapper">
|
||||
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
|
||||
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
|
||||
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -239,7 +242,7 @@
|
||||
<img class="image" src="/resources/profile/ronindojo.png" />
|
||||
<span>RoninDojo</span>
|
||||
</a>
|
||||
<a href="https://github.com/runcitadel" target="_blank" title="Citadel">
|
||||
<a href="https://github.com/runcitadel/core" target="_blank" title="Citadel">
|
||||
<img class="image" src="/resources/profile/runcitadel.svg" />
|
||||
<span>Citadel</span>
|
||||
</a>
|
||||
@@ -295,9 +298,9 @@
|
||||
<img class="image" src="/resources/profile/blixt.png" />
|
||||
<span>Blixt</span>
|
||||
</a>
|
||||
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS">
|
||||
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="Zeus">
|
||||
<img class="image" src="/resources/profile/zeus.png" />
|
||||
<span>ZEUS</span>
|
||||
<span>Zeus</span>
|
||||
</a>
|
||||
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
|
||||
<img class="image" src="/resources/profile/marina.svg" />
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
.intro {
|
||||
margin: 25px auto 30px;
|
||||
margin-top: 25px;
|
||||
width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { map, share, tap } from 'rxjs/operators';
|
||||
import { ITranslators } from '../../interfaces/node-api.interface';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
@@ -34,7 +33,6 @@ export class AboutComponent implements OnInit {
|
||||
private websocketService: WebsocketService,
|
||||
private seoService: SeoService,
|
||||
public stateService: StateService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private apiService: ApiService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
@@ -45,17 +43,11 @@ export class AboutComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
this.backendInfo$ = this.stateService.backendInfo$;
|
||||
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.about:Learn more about The Mempool Open Source Project®\: enterprise sponsors, individual sponsors, integrations, who contributes, FOSS licensing, and more.`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(
|
||||
tap((profiles: any) => {
|
||||
const scrollToSponsors = this.route.snapshot.fragment === 'community-sponsors';
|
||||
if (scrollToSponsors && !profiles?.whales?.length && !profiles?.chads?.length) {
|
||||
return;
|
||||
} else {
|
||||
this.goToAnchor(scrollToSponsors)
|
||||
}
|
||||
tap(() => {
|
||||
this.goToAnchor()
|
||||
}),
|
||||
share(),
|
||||
)
|
||||
@@ -90,19 +82,11 @@ export class AboutComponent implements OnInit {
|
||||
this.goToAnchor();
|
||||
}
|
||||
|
||||
goToAnchor(scrollToSponsor = false) {
|
||||
if (!scrollToSponsor) {
|
||||
return;
|
||||
}
|
||||
goToAnchor() {
|
||||
setTimeout(() => {
|
||||
if (this.route.snapshot.fragment) {
|
||||
const el = scrollToSponsor ? this.document.getElementById('community-sponsors-anchor') : this.document.getElementById(this.route.snapshot.fragment);
|
||||
if (el) {
|
||||
if (scrollToSponsor) {
|
||||
el.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'center'});
|
||||
} else {
|
||||
el.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
if (this.document.getElementById(this.route.snapshot.fragment)) {
|
||||
this.document.getElementById(this.route.snapshot.fragment).scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
}, 1);
|
||||
@@ -123,14 +107,4 @@ export class AboutComponent implements OnInit {
|
||||
unmutePromoVideo(): void {
|
||||
this.promoVideo.nativeElement.muted = false;
|
||||
}
|
||||
|
||||
onSponsorClick(e): boolean {
|
||||
this.enterpriseService.goal(5);
|
||||
return true;
|
||||
}
|
||||
|
||||
onEnterpriseClick(e): boolean {
|
||||
this.enterpriseService.goal(6);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AboutComponent } from './about.component';
|
||||
import { AboutSponsorsComponent } from './about-sponsors.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AboutComponent,
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class AboutRoutingModule { }
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
AboutRoutingModule,
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [
|
||||
AboutComponent,
|
||||
AboutSponsorsComponent,
|
||||
],
|
||||
exports: [
|
||||
AboutSponsorsComponent,
|
||||
]
|
||||
})
|
||||
export class AboutModule { }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<div class="fee-graph" *ngIf="tx && estimate">
|
||||
<div class="column">
|
||||
<ng-container *ngFor="let bar of bars">
|
||||
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">
|
||||
<div class="fill"></div>
|
||||
<div class="line">
|
||||
<p class="fee-rate">
|
||||
<span class="label">{{ bar.label }}</span>
|
||||
<span class="rate">
|
||||
<app-fee-rate [fee]="bar.rate"></app-fee-rate>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }} {{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
||||
<div class="spacer"></div>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,157 +0,0 @@
|
||||
.fee-graph {
|
||||
height: 100%;
|
||||
min-width: 120px;
|
||||
width: 120px;
|
||||
margin-left: 4em;
|
||||
margin-right: 1.5em;
|
||||
padding-bottom: 63px;
|
||||
|
||||
.column {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: #181b2d;
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.75;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fee {
|
||||
font-size: 0.9em;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
flex-grow: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
left: -4.5em;
|
||||
border-top: dashed white 1.5px;
|
||||
|
||||
.fee-rate {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0.2em;
|
||||
font-size: 0.8em;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
|
||||
.label {
|
||||
margin-right: .2em;
|
||||
}
|
||||
|
||||
.rate .symbol {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tx {
|
||||
.fill {
|
||||
background: #3bcc49;
|
||||
}
|
||||
.line {
|
||||
.fee-rate {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
.fee {
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
z-index: 11;
|
||||
}
|
||||
}
|
||||
|
||||
&.target {
|
||||
.fill {
|
||||
background: #653b9c;
|
||||
}
|
||||
.fee {
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
z-index: 11;
|
||||
}
|
||||
.line .fee-rate {
|
||||
bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.max {
|
||||
cursor: pointer;
|
||||
.line .fee-rate {
|
||||
.label {
|
||||
opacity: 0;
|
||||
}
|
||||
bottom: 2px;
|
||||
}
|
||||
&.active, &:hover {
|
||||
.fill {
|
||||
background: #105fb0;
|
||||
}
|
||||
.line {
|
||||
.fee-rate .label {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fill {
|
||||
z-index: 10;
|
||||
}
|
||||
.line {
|
||||
z-index: 11;
|
||||
}
|
||||
.fee {
|
||||
opacity: 1;
|
||||
z-index: 12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover > .bar:not(:hover) {
|
||||
&.target, &.max {
|
||||
.fee {
|
||||
opacity: 0;
|
||||
}
|
||||
.line .fee-rate .label {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
&.max {
|
||||
.fill {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
|
||||
import { Router } from '@angular/router';
|
||||
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
|
||||
import { tap, switchMap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { AccelerationEstimate, RateOption } from './accelerate-preview.component';
|
||||
|
||||
interface GraphBar {
|
||||
rate: number;
|
||||
style: any;
|
||||
class: 'tx' | 'target' | 'max';
|
||||
label: string;
|
||||
active?: boolean;
|
||||
rateIndex?: number;
|
||||
fee?: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerate-fee-graph',
|
||||
templateUrl: './accelerate-fee-graph.component.html',
|
||||
styleUrls: ['./accelerate-fee-graph.component.scss'],
|
||||
})
|
||||
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
@Input() estimate: AccelerationEstimate;
|
||||
@Input() maxRateOptions: RateOption[] = [];
|
||||
@Input() maxRateIndex: number = 0;
|
||||
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
|
||||
|
||||
bars: GraphBar[] = [];
|
||||
tooltipPosition = { x: 0, y: 0 };
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
initGraph(): void {
|
||||
if (!this.tx || !this.estimate) {
|
||||
return;
|
||||
}
|
||||
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
|
||||
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
|
||||
const baseHeight = baseRate / maxRate;
|
||||
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => {
|
||||
return {
|
||||
rate: option.rate,
|
||||
style: this.getStyle(option.rate, maxRate, baseHeight),
|
||||
class: 'max',
|
||||
label: 'maximum',
|
||||
active: option.index === this.maxRateIndex,
|
||||
rateIndex: option.index,
|
||||
fee: option.fee,
|
||||
}
|
||||
});
|
||||
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
|
||||
bars.push({
|
||||
rate: this.estimate.targetFeeRate,
|
||||
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||
class: 'target',
|
||||
label: 'next block',
|
||||
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||
});
|
||||
}
|
||||
bars.push({
|
||||
rate: baseRate,
|
||||
style: this.getStyle(baseRate, maxRate, 0),
|
||||
class: 'tx',
|
||||
label: '',
|
||||
fee: this.estimate.txSummary.effectiveFee,
|
||||
});
|
||||
this.bars = bars;
|
||||
}
|
||||
|
||||
getStyle(rate, maxRate, base) {
|
||||
const top = (rate / maxRate);
|
||||
return {
|
||||
height: `${(top - base) * 100}%`,
|
||||
bottom: base ? `${base * 100}%` : '0',
|
||||
}
|
||||
}
|
||||
|
||||
onClick(event, bar): void {
|
||||
if (bar.rateIndex != null) {
|
||||
this.setUserBid.emit({ fee: bar.fee, index: bar.rateIndex });
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
<span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span>
|
||||
<div class="row" *ngIf="showSuccess">
|
||||
<div class="col">
|
||||
<div class="alert alert-success">
|
||||
Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span id="mempoolError" class="m-0 p-0 d-block" style="height: 1px;"></span>
|
||||
<div class="row" *ngIf="error">
|
||||
<div class="col">
|
||||
<app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accelerate-cols">
|
||||
<ng-container *ngIf="!isMobile">
|
||||
<app-accelerate-fee-graph
|
||||
[tx]="tx"
|
||||
[estimate]="estimate"
|
||||
[maxRateOptions]="maxRateOptions"
|
||||
[maxRateIndex]="selectFeeRateIndex"
|
||||
(setUserBid)="setUserBid($event)"
|
||||
></app-accelerate-fee-graph>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="estimate">
|
||||
<div [class]="{estimateDisabled: error}">
|
||||
|
||||
<div *ngIf="user && !estimate.hasAccess">
|
||||
<div class="alert alert-mempool">You are currently on the waitlist</div>
|
||||
</div>
|
||||
|
||||
<h5>Your transaction</h5>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<small *ngIf="hasAncestors" class="form-text text-muted mb-2">
|
||||
Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor{{ estimate.txSummary.ancestorCount > 2 ? 's' : ''}}.
|
||||
</small>
|
||||
<table class="table table-borderless table-border table-dark table-accelerator">
|
||||
<tbody>
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
Virtual size
|
||||
</td>
|
||||
<td style="text-align: end;" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info" colspan=3>
|
||||
<i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="item">
|
||||
In-band fees
|
||||
</td>
|
||||
<td style="text-align: end;">
|
||||
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info" colspan=3>
|
||||
<i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<h5>How much more are you willing to pay?</h5>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<small class="form-text text-muted mb-2">
|
||||
Choose the maximum extra transaction fee you're willing to pay to get into the next block.<br>
|
||||
If the estimated next block rate rises beyond this limit, we will automatically cancel your acceleration request.
|
||||
</small>
|
||||
<div class="form-group">
|
||||
<div class="fee-card">
|
||||
<div class="d-flex mb-0">
|
||||
<ng-container *ngFor="let option of maxRateOptions">
|
||||
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)">
|
||||
<span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
|
||||
<span class="rate">~<app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Acceleration summary</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-border table-dark table-accelerator">
|
||||
<tbody>
|
||||
<!-- ESTIMATED FEE -->
|
||||
<ng-container>
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
Next block market rate
|
||||
</td>
|
||||
<td class="amt" style="font-size: 16px">
|
||||
{{ estimate.targetFeeRate | number : '1.0-0' }}
|
||||
</td>
|
||||
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<i><small>Estimated extra fee required</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- MEMPOOL BASE FEE -->
|
||||
<tr>
|
||||
<td class="item">
|
||||
Mempool Accelerator™ fees
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<i><small>mempool.space fee</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
+{{ estimate.mempoolBaseFee | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<i><small>Transaction vsize fee</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
+{{ estimate.vsizeFee | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- NEXT BLOCK ESTIMATE -->
|
||||
<ng-container>
|
||||
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
|
||||
<td class="item">
|
||||
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
|
||||
</td>
|
||||
<td class="amt">
|
||||
<span style="background-color: #5E35B1" class="p-1 pl-0">
|
||||
{{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
|
||||
<td class="info" colspan=3>
|
||||
<i><small>If your tx is accelerated to </small><small>{{ estimate.targetFeeRate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- MAX COST -->
|
||||
<ng-container>
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
|
||||
</td>
|
||||
<td class="amt">
|
||||
<span style="background-color: #105fb0" class="p-1 pl-0">
|
||||
{{ maxCost | number }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1">
|
||||
<app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info" colspan=3>
|
||||
<i><small>If your tx is accelerated to </small><small>~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- USER BALANCE -->
|
||||
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
|
||||
<tr class="group-first group-last" style="border-top: 1px dashed grey">
|
||||
<td class="item">
|
||||
Available balance
|
||||
</td>
|
||||
<td class="amt">
|
||||
{{ estimate.userBalance | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats">sats</span>
|
||||
<span class="fiat ml-1">
|
||||
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- LOGIN CTA -->
|
||||
<ng-container *ngIf="!isLoggedIn()">
|
||||
<tr class="group-first group-last" style="border-top: 1px dashed grey">
|
||||
<td class="item"></td>
|
||||
<td class="amt"></td>
|
||||
<td class="units d-flex">
|
||||
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1">Login</a>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3" *ngIf="isLoggedIn()">
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess">
|
||||
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -1,112 +0,0 @@
|
||||
.fee-card {
|
||||
padding: 15px;
|
||||
background-color: #1d1f31;
|
||||
|
||||
.feerate {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.rate {
|
||||
font-size: 0.9em;
|
||||
.symbol {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-border {
|
||||
border: solid 1px black;
|
||||
background-color: #0c4a87;
|
||||
}
|
||||
|
||||
.feerate.active {
|
||||
background-color: #105fb0 !important;
|
||||
opacity: 1;
|
||||
border: 1px solid #007fff !important;
|
||||
}
|
||||
.feerate:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.estimateDisabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.table-toggle {
|
||||
width: 100%;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.tab {
|
||||
&:first-child {
|
||||
margin-right: 1px;
|
||||
}
|
||||
border: solid 1px black;
|
||||
border-bottom: none;
|
||||
background-color: #323655;
|
||||
border-top-left-radius: 10px !important;
|
||||
border-top-right-radius: 10px !important;
|
||||
}
|
||||
.tab.active {
|
||||
background-color: #5d659d !important;
|
||||
opacity: 1;
|
||||
}
|
||||
.tab:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.table-accelerator {
|
||||
tr {
|
||||
td {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
&.group-first {
|
||||
td {
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
&.group-last {
|
||||
td {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
td {
|
||||
&:first-child {
|
||||
width: 100vw;
|
||||
}
|
||||
&.info {
|
||||
color: #6c757d;
|
||||
white-space: initial;
|
||||
}
|
||||
&.amt {
|
||||
text-align: right;
|
||||
padding-right: 0.2em;
|
||||
}
|
||||
&.units {
|
||||
padding-left: 0.2em;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accelerate-cols {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.item {
|
||||
white-space: initial;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user