Compare commits

..

20 Commits

Author SHA1 Message Date
Mononaut
1037b50851 strip non-essential data from redis cache txs 2023-07-25 16:35:21 +09:00
Mononaut
a3c82e44d9 store redis mempool in sharded json object 2023-07-25 16:12:39 +09:00
Mononaut
78a0059d01 Add network and schema versioning to redis cache 2023-07-25 16:12:39 +09:00
Mononaut
172d4f9dfc Add transactions to Redis cache in manageable batches 2023-07-25 16:12:38 +09:00
Mononaut
97ecc7b90f Fix redis feature merge conflicts 2023-07-25 16:12:38 +09:00
Mononaut
381d2f0b1f Add RBF data to Redis cache 2023-07-25 16:12:38 +09:00
Mononaut
38a79249a3 Implement Redis cache for block and mempool data 2023-07-25 16:12:37 +09:00
softsimon
b58abe4779 Merge pull request #4045 from mempool/mononaut/fast-blocks
Faster blocks
2023-07-25 16:07:52 +09:00
Mononaut
25925751eb refactor $getTransactionsExtended to optimise API requests 2023-07-25 12:09:13 +09:00
Mononaut
0ebfd6f017 Fetch block txs from mempool/electrs in bulk 2023-07-25 10:27:43 +09:00
wiz
81d1c0a4d5 Merge pull request #4043 from mempool/mononaut/mempool-sync-status
Mempool inSync status
2023-07-24 19:39:17 +09:00
Mononaut
36fe5627c7 fix mempool sync skeleton loaders on Core backend 2023-07-24 17:49:34 +09:00
Mononaut
2d463326e0 fix gbt mempool size mismatch bug 2023-07-24 17:22:38 +09:00
Mononaut
a6edfcc272 show mempool skeleton while not inSync 2023-07-24 16:22:35 +09:00
Mononaut
de4265a6d1 More conservative mempool inSync status 2023-07-24 16:22:22 +09:00
softsimon
dc43a81899 Merge pull request #4035 from mempool/mononaut/lightning-balance-bars
Lightning channel balance progress bars
2023-07-24 16:22:20 +09:00
Mononaut
e59c961f25 Add electrs sync progress updates 2023-07-24 14:59:51 +09:00
Mononaut
db715a1dba Switch to batch mempool/txs/:txid endpoint 2023-07-24 14:44:43 +09:00
Mononaut
202d4122b4 load mempool txs in bulk from esplora 2023-07-24 14:44:42 +09:00
Mononaut
a1e05c0c37 Lightning channel balance progress bars 2023-07-23 18:00:24 +09:00
51 changed files with 1121 additions and 448 deletions

View File

@@ -19,6 +19,7 @@
"maxmind": "~4.3.11",
"mysql2": "~3.5.2",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.13.0"
@@ -1555,6 +1556,64 @@
"node": ">= 8"
}
},
"node_modules/@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/client": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz",
"integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==",
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@redis/client/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/@redis/graph": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
"integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/json": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/search": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz",
"integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/time-series": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
"integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
@@ -2718,6 +2777,14 @@
"node": ">=12"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -3678,6 +3745,14 @@
"is-property": "^1.0.2"
}
},
"node_modules/generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
"engines": {
"node": ">= 4"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -6577,6 +6652,19 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
"node_modules/redis": {
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz",
"integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==",
"dependencies": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.5.7",
"@redis/graph": "1.1.0",
"@redis/json": "1.0.4",
"@redis/search": "1.1.2",
"@redis/time-series": "1.0.4"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -8704,6 +8792,53 @@
"fastq": "^1.6.0"
}
},
"@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
"requires": {}
},
"@redis/client": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz",
"integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==",
"requires": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
},
"dependencies": {
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
},
"@redis/graph": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
"integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
"requires": {}
},
"@redis/json": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
"requires": {}
},
"@redis/search": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz",
"integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==",
"requires": {}
},
"@redis/time-series": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
"integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
"requires": {}
},
"@sinclair/typebox": {
"version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
@@ -9604,6 +9739,11 @@
"wrap-ansi": "^7.0.0"
}
},
"cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -10332,6 +10472,11 @@
"is-property": "^1.0.2"
}
},
"generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g=="
},
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -12454,6 +12599,19 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
"redis": {
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz",
"integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==",
"requires": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.5.7",
"@redis/graph": "1.1.0",
"@redis/json": "1.0.4",
"@redis/search": "1.1.2",
"@redis/time-series": "1.0.4"
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",

View File

@@ -47,13 +47,14 @@
"maxmind": "~4.3.11",
"mysql2": "~3.5.2",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.13.0"
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.21.3",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",

View File

@@ -10,6 +10,7 @@
"AUTOMATIC_BLOCK_REINDEXING": false,
"POLL_RATE_MS": 3,
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
"CACHE_ENABLED": true,
"CLEAR_PROTECTION_MINUTES": 4,
"RECOMMENDED_FEE_PERCENTILE": 5,
"BLOCK_WEIGHT_UNITS": 6,
@@ -127,5 +128,9 @@
"AUDIT": false,
"AUDIT_START_HEIGHT": 774000,
"SERVERS": []
},
"REDIS": {
"ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock"
}
}

View File

@@ -23,6 +23,7 @@ describe('Mempool Backend Config', () => {
AUTOMATIC_BLOCK_REINDEXING: false,
POLL_RATE_MS: 2000,
CACHE_DIR: './cache',
CACHE_ENABLED: true,
CLEAR_PROTECTION_MINUTES: 20,
RECOMMENDED_FEE_PERCENTILE: 50,
BLOCK_WEIGHT_UNITS: 4000000,
@@ -127,6 +128,11 @@ describe('Mempool Backend Config', () => {
AUDIT_START_HEIGHT: 774000,
SERVERS: []
});
expect(config.REDIS).toStrictEqual({
ENABLED: false,
UNIX_SOCKET_PATH: ''
});
});
});
@@ -160,6 +166,8 @@ describe('Mempool Backend Config', () => {
expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
expect(config.REDIS).toStrictEqual(fixture.REDIS);
});
});

View File

@@ -3,10 +3,12 @@ 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>;
$getMempoolTransactions(lastTxid: string);
$getTransactionHex(txId: string): Promise<string>;
$getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>;
$getTxIdsForBlock(hash: string): Promise<string[]>;
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]>;
$getBlockHash(height: number): Promise<string>;
$getBlockHeader(hash: string): Promise<string>;
$getBlock(hash: string): Promise<IEsploraApi.Block>;

View File

@@ -5,6 +5,7 @@ import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks';
import mempool from '../mempool';
import { TransactionExtended } from '../../mempool.interfaces';
import transactionUtils from '../transaction-utils';
class BitcoinApi implements AbstractBitcoinApi {
private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
@@ -59,9 +60,20 @@ class BitcoinApi implements AbstractBitcoinApi {
});
}
$getTransactionHex(txId: string): Promise<string> {
return this.$getRawTransaction(txId, true)
.then((tx) => tx.hex || '');
$getMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
return Promise.resolve([]);
}
async $getTransactionHex(txId: string): Promise<string> {
const txInMempool = mempool.getMempool()[txId];
if (txInMempool && txInMempool.hex) {
return txInMempool.hex;
}
return this.bitcoindClient.getRawTransaction(txId, true)
.then((transaction: IBitcoinApi.Transaction) => {
return transaction.hex;
});
}
$getBlockHeightTip(): Promise<number> {
@@ -77,6 +89,10 @@ class BitcoinApi implements AbstractBitcoinApi {
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
}
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getTxsForBlock not supported by the Bitcoin RPC API.');
}
$getRawBlock(hash: string): Promise<Buffer> {
return this.bitcoindClient.getBlock(hash, 0)
.then((raw: string) => Buffer.from(raw, "hex"));
@@ -201,7 +217,7 @@ class BitcoinApi implements AbstractBitcoinApi {
scriptpubkey: vout.scriptPubKey.hex,
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address
: vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.hex) : '',
scriptpubkey_asm: vout.scriptPubKey.asm ? transactionUtils.convertScriptSigAsm(vout.scriptPubKey.hex) : '',
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
};
});
@@ -211,7 +227,7 @@ class BitcoinApi implements AbstractBitcoinApi {
is_coinbase: !!vin.coinbase,
prevout: null,
scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.hex) || '',
scriptsig_asm: vin.scriptSig && transactionUtils.convertScriptSigAsm(vin.scriptSig.hex) || '',
sequence: vin.sequence,
txid: vin.txid || '',
vout: vin.vout || 0,
@@ -283,7 +299,7 @@ class BitcoinApi implements AbstractBitcoinApi {
}
const innerTx = await this.$getRawTransaction(vin.txid, false, false);
vin.prevout = innerTx.vout[vin.vout];
this.addInnerScriptsToVin(vin);
transactionUtils.addInnerScriptsToVin(vin);
}
return transaction;
}
@@ -322,7 +338,7 @@ class BitcoinApi implements AbstractBitcoinApi {
}
const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
this.addInnerScriptsToVin(transaction.vin[i]);
transactionUtils.addInnerScriptsToVin(transaction.vin[i]);
totalIn += innerTx.vout[transaction.vin[i].vout].value;
}
if (lazyPrevouts && transaction.vin.length > 12) {
@@ -334,122 +350,6 @@ class BitcoinApi implements AbstractBitcoinApi {
return transaction;
}
private convertScriptSigAsm(hex: string): string {
const buf = Buffer.from(hex, 'hex');
const b: string[] = [];
let i = 0;
while (i < buf.length) {
const op = buf[i];
if (op >= 0x01 && op <= 0x4e) {
i++;
let push: number;
if (op === 0x4c) {
push = buf.readUInt8(i);
b.push('OP_PUSHDATA1');
i += 1;
} else if (op === 0x4d) {
push = buf.readUInt16LE(i);
b.push('OP_PUSHDATA2');
i += 2;
} else if (op === 0x4e) {
push = buf.readUInt32LE(i);
b.push('OP_PUSHDATA4');
i += 4;
} else {
push = op;
b.push('OP_PUSHBYTES_' + push);
}
const data = buf.slice(i, i + push);
if (data.length !== push) {
break;
}
b.push(data.toString('hex'));
i += data.length;
} else {
if (op === 0x00) {
b.push('OP_0');
} else if (op === 0x4f) {
b.push('OP_PUSHNUM_NEG1');
} else if (op === 0xb1) {
b.push('OP_CLTV');
} else if (op === 0xb2) {
b.push('OP_CSV');
} else if (op === 0xba) {
b.push('OP_CHECKSIGADD');
} else {
const opcode = bitcoinjs.script.toASM([ op ]);
if (opcode && op < 0xfd) {
if (/^OP_(\d+)$/.test(opcode)) {
b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
} else {
b.push(opcode);
}
} else {
b.push('OP_RETURN_' + op);
}
}
i += 1;
}
}
return b.join(' ');
}
private addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
if (!vin.prevout) {
return;
}
if (vin.prevout.scriptpubkey_type === 'p2sh') {
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
if (vin.witness && vin.witness.length > 2) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
}
}
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
}
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
const witnessScript = this.witnessToP2TRScript(vin.witness);
if (witnessScript !== null) {
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
}
}
}
/**
* This function must only be called when we know the witness we are parsing
* is a taproot witness.
* @param witness An array of hex strings that represents the witness stack of
* the input.
* @returns null if the witness is not a script spend, and the hex string of
* the script item if it is a script spend.
*/
private witnessToP2TRScript(witness: string[]): string | null {
if (witness.length < 2) return null;
// Note: see BIP341 for parsing details of witness stack
// If there are at least two witness elements, and the first byte of the
// last element is 0x50, this last element is called annex a and
// is removed from the witness stack.
const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50';
// If there are at least two witness elements left, script path spending is used.
// Call the second-to-last stack element s, the script.
// (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
if (hasAnnex && witness.length < 3) return null;
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
return witness[positionOfScript];
}
}
export default BitcoinApi;

View File

@@ -414,7 +414,7 @@ class BitcoinRoutes {
private async getBlocks(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocks(height, 15));
@@ -428,7 +428,7 @@ class BitcoinRoutes {
private async getBlocksByBulk(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid, Bisq - Not implemented
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid, Bisq - Not implemented
return res.status(404).send(`This API is only available for Bitcoin networks`);
}
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {

View File

@@ -69,6 +69,10 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId);
}
async $getMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
}
$getTransactionHex(txId: string): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex');
}
@@ -85,6 +89,10 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids');
}
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txs');
}
$getBlockHash(height: number): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height);
}

View File

@@ -26,6 +26,8 @@ import PricesRepository from '../repositories/PricesRepository';
import priceUpdater from '../tasks/price-updater';
import chainTips from './chain-tips';
import websocketHandler from './websocket-handler';
import redisCache from './redis-cache';
import rbfCache from './rbf-cache';
class Blocks {
private blocks: BlockExtended[] = [];
@@ -70,6 +72,9 @@ class Blocks {
* @param blockHash
* @param blockHeight
* @param onlyCoinbase - Set to true if you only need the coinbase transaction
* @param txIds - optional ordered list of transaction ids if already known
* @param quiet - don't print non-essential logs
* @param addMempoolData - calculate sigops etc
* @returns Promise<TransactionExtended[]>
*/
private async $getTransactionsExtended(
@@ -80,62 +85,77 @@ class Blocks {
quiet: boolean = false,
addMempoolData: boolean = false,
): Promise<TransactionExtended[]> {
const transactions: TransactionExtended[] = [];
const isEsplora = config.MEMPOOL.BACKEND === 'esplora';
const transactionMap: { [txid: string]: TransactionExtended } = {};
if (!txIds) {
txIds = await bitcoinApi.$getTxIdsForBlock(blockHash);
}
const mempool = memPool.getMempool();
let transactionsFound = 0;
let transactionsFetched = 0;
let foundInMempool = 0;
let totalFound = 0;
for (let i = 0; i < txIds.length; i++) {
if (mempool[txIds[i]]) {
// We update blocks before the mempool (index.ts), therefore we can
// optimize here by directly fetching txs in the "outdated" mempool
transactions.push(mempool[txIds[i]]);
transactionsFound++;
} else if (config.MEMPOOL.BACKEND === 'esplora' || !memPool.hasPriority() || i === 0) {
// Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...)
if (!quiet && (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length)) { // Avoid log spam
logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`);
}
try {
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, false, addMempoolData);
transactions.push(tx);
transactionsFetched++;
} catch (e) {
try {
if (config.MEMPOOL.BACKEND === 'esplora') {
// Try again with core
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true, addMempoolData);
transactions.push(tx);
transactionsFetched++;
} else {
throw e;
}
} catch (e) {
if (i === 0) {
const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e);
logger.err(msg);
throw new Error(msg);
} else {
logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
// Copy existing transactions from the mempool
if (!onlyCoinbase) {
for (const txid of txIds) {
if (mempool[txid]) {
transactionMap[txid] = mempool[txid];
foundInMempool++;
totalFound++;
}
}
}
if (onlyCoinbase === true) {
break; // Fetch the first transaction and exit
// Skip expensive lookups while mempool has priority
if (onlyCoinbase) {
try {
const coinbase = await transactionUtils.$getTransactionExtended(txIds[0], false, false, false, addMempoolData);
return [coinbase];
} catch (e) {
const msg = `Cannot fetch coinbase tx ${txIds[0]}. Reason: ` + (e instanceof Error ? e.message : e);
logger.err(msg);
throw new Error(msg);
}
}
// Fetch remaining txs in bulk
if (isEsplora && (txIds.length - totalFound > 500)) {
try {
const rawTransactions = await bitcoinApi.$getTxsForBlock(blockHash);
for (const tx of rawTransactions) {
if (!transactionMap[tx.txid]) {
transactionMap[tx.txid] = addMempoolData ? transactionUtils.extendMempoolTransaction(tx) : transactionUtils.extendTransaction(tx);
totalFound++;
}
}
} catch (e) {
logger.err(`Cannot fetch bulk txs for block ${blockHash}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
// Fetch remaining txs individually
for (const txid of txIds.filter(txid => !transactionMap[txid])) {
if (!transactionMap[txid]) {
if (!quiet && (totalFound % (Math.round((txIds.length) / 10)) === 0 || totalFound + 1 === txIds.length)) { // Avoid log spam
logger.debug(`Indexing tx ${totalFound + 1} of ${txIds.length} in block #${blockHeight}`);
}
try {
const tx = await transactionUtils.$getTransactionExtended(txid, false, false, false, addMempoolData);
transactionMap[txid] = tx;
totalFound++;
} catch (e) {
logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
}
if (!quiet) {
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
logger.debug(`${foundInMempool} of ${txIds.length} found in mempool. ${totalFound - foundInMempool} fetched through backend service.`);
}
return transactions;
// Return list of transactions, preserving block order
return txIds.map(txid => transactionMap[txid]).filter(tx => tx != null);
}
/**
@@ -263,7 +283,7 @@ class Blocks {
extras.totalInputAmt = null;
}
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
let pool: PoolTag;
if (coinbaseTx !== undefined) {
pool = await this.$findBlockMiner(coinbaseTx);
@@ -765,10 +785,18 @@ class Blocks {
if (this.newBlockCallbacks.length) {
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
}
if (!memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
if (config.MEMPOOL.CACHE_ENABLED && !memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
diskCache.$saveCacheToDisk();
}
// Update Redis cache
if (config.REDIS.ENABLED) {
await redisCache.$updateBlocks(this.blocks);
await redisCache.$updateBlockSummaries(this.blockSummaries);
await redisCache.$removeTransactions(txIds);
await rbfCache.updateCache();
}
handledBlocks++;
}
@@ -845,7 +873,7 @@ class Blocks {
}
// Not Bitcoin network, return the block as it from the bitcoin backend
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK) === false) {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return await bitcoinCoreApi.$getBlock(hash);
}
@@ -1061,7 +1089,7 @@ class Blocks {
}
public async $getBlockAuditSummary(hash: string): Promise<any> {
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
return BlocksAuditsRepository.$getBlockAudit(hash);
} else {
return null;

View File

@@ -239,7 +239,7 @@ export class Common {
static indexingEnabled(): boolean {
return (
['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK) &&
['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) &&
config.DATABASE.ENABLED === true &&
config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== 0
);

View File

@@ -104,7 +104,7 @@ class DatabaseMigration {
private async $createMissingTablesAndIndexes(databaseSchemaVersion: number) {
await this.$setStatisticsAddedIndexedFlag(databaseSchemaVersion);
const isBitcoin = ['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK);
const isBitcoin = ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK);
await this.$executeQuery(this.getCreateElementsTableQuery(), await this.$checkIfTableExists('elements_pegs'));
await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics'));
@@ -512,7 +512,7 @@ class DatabaseMigration {
await this.updateToSchemaVersion(58);
}
if (databaseSchemaVersion < 59 && ['testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
if (databaseSchemaVersion < 59 && (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'testnet')) {
// https://github.com/mempool/mempool/issues/3360
await this.$executeQuery(`TRUNCATE prices`);
}
@@ -656,7 +656,7 @@ class DatabaseMigration {
*/
private getMigrationQueriesFromVersion(version: number): string[] {
const queries: string[] = [];
const isBitcoin = ['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK);
const isBitcoin = ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK);
if (version < 1) {
if (config.MEMPOOL.NETWORK !== 'liquid' && config.MEMPOOL.NETWORK !== 'liquidtestnet') {

View File

@@ -29,7 +29,7 @@ class DiskCache {
};
constructor() {
if (!cluster.isPrimary) {
if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) {
return;
}
process.on('SIGINT', (e) => {
@@ -39,7 +39,7 @@ class DiskCache {
}
async $saveCacheToDisk(sync: boolean = false): Promise<void> {
if (!cluster.isPrimary) {
if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) {
return;
}
if (this.isWritingCache) {
@@ -175,7 +175,7 @@ class DiskCache {
}
async $loadMempoolCache(): Promise<void> {
if (!fs.existsSync(DiskCache.FILE_NAME)) {
if (!config.MEMPOOL.CACHE_ENABLED || !fs.existsSync(DiskCache.FILE_NAME)) {
return;
}
try {

View File

@@ -1,5 +1,5 @@
import config from '../config';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import { MempoolTransactionExtended, TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
import logger from '../logger';
import { Common } from './common';
@@ -9,6 +9,7 @@ import loadingIndicators from './loading-indicators';
import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
import redisCache from './redis-cache';
class Mempool {
private inSync: boolean = false;
@@ -85,6 +86,10 @@ class Mempool {
public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) {
this.mempoolCache = mempoolData;
let count = 0;
const redisTimer = Date.now();
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
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].sigops || this.mempoolCache[txid].effectiveFeePerVsize == null) {
this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]);
@@ -93,6 +98,13 @@ class Mempool {
this.mempoolCache[txid].order = transactionUtils.txidToOrdering(txid);
}
count++;
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
await redisCache.$addTransaction(this.mempoolCache[txid]);
}
}
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
await redisCache.$flushTransactions();
logger.debug(`Finished migrating cache transactions in ${((Date.now() - redisTimer) / 1000).toFixed(2)} seconds`);
}
if (this.mempoolChangedCallback) {
this.mempoolChangedCallback(this.mempoolCache, [], []);
@@ -103,6 +115,44 @@ class Mempool {
this.addToSpendMap(Object.values(this.mempoolCache));
}
public async $reloadMempool(expectedCount: number): Promise<MempoolTransactionExtended[]> {
let count = 0;
let done = false;
let last_txid;
const newTransactions: MempoolTransactionExtended[] = [];
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
while (!done) {
try {
const result = await bitcoinApi.$getMempoolTransactions(last_txid);
if (result) {
for (const tx of result) {
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
if (!this.mempoolCache[extendedTransaction.txid]) {
newTransactions.push(extendedTransaction);
this.mempoolCache[extendedTransaction.txid] = extendedTransaction;
}
count++;
}
logger.info(`Fetched ${count} of ${expectedCount} mempool transactions from esplora`);
if (result.length > 0) {
last_txid = result[result.length - 1].txid;
} else {
done = true;
}
if (Math.floor((count / expectedCount) * 100) < 100) {
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
}
} else {
done = true;
}
} catch(err) {
logger.err('failed to fetch bulk mempool transactions from esplora');
}
}
logger.info(`Done inserting loaded mempool transactions into local cache`);
return newTransactions;
}
public async $updateMemPoolInfo() {
this.mempoolInfo = await this.$getMempoolInfo();
}
@@ -143,7 +193,7 @@ class Mempool {
const currentMempoolSize = Object.keys(this.mempoolCache).length;
this.updateTimerProgress(timer, 'got raw mempool');
const diff = transactions.length - currentMempoolSize;
const newTransactions: MempoolTransactionExtended[] = [];
let newTransactions: MempoolTransactionExtended[] = [];
this.mempoolCacheDelta = Math.abs(diff);
@@ -162,41 +212,66 @@ class Mempool {
};
let intervalTimer = Date.now();
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
try {
const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
this.updateTimerProgress(timer, 'fetched new transaction');
this.mempoolCache[txid] = transaction;
if (this.inSync) {
this.txPerSecondArray.push(new Date().getTime());
this.vBytesPerSecondArray.push({
unixTime: new Date().getTime(),
vSize: transaction.vsize,
});
}
hasChange = true;
newTransactions.push(transaction);
} catch (e: any) {
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
this.missingTxCount++;
}
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
}
}
if (Date.now() - intervalTimer > 5_000) {
if (this.inSync) {
// Break and restart mempool loop if we spend too much time processing
// new transactions that may lead to falling behind on block height
logger.debug('Breaking mempool loop because the 5s time limit exceeded.');
break;
} else {
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
loadingIndicators.setProgress('mempool', progress);
intervalTimer = Date.now()
let loaded = false;
if (config.MEMPOOL.BACKEND === 'esplora' && currentMempoolSize < transactions.length * 0.5 && transactions.length > 20_000) {
this.inSync = false;
logger.info(`Missing ${transactions.length - currentMempoolSize} mempool transactions, attempting to reload in bulk from esplora`);
try {
newTransactions = await this.$reloadMempool(transactions.length);
if (config.REDIS.ENABLED) {
for (const tx of newTransactions) {
await redisCache.$addTransaction(tx);
}
}
loaded = true;
} catch (e) {
logger.err('failed to load mempool in bulk from esplora, falling back to fetching individual transactions');
}
}
if (!loaded) {
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
try {
const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
this.updateTimerProgress(timer, 'fetched new transaction');
this.mempoolCache[txid] = transaction;
if (this.inSync) {
this.txPerSecondArray.push(new Date().getTime());
this.vBytesPerSecondArray.push({
unixTime: new Date().getTime(),
vSize: transaction.vsize,
});
}
hasChange = true;
newTransactions.push(transaction);
if (config.REDIS.ENABLED) {
await redisCache.$addTransaction(transaction);
}
} catch (e: any) {
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
this.missingTxCount++;
}
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
}
}
if (Date.now() - intervalTimer > 5_000) {
if (this.inSync) {
// Break and restart mempool loop if we spend too much time processing
// new transactions that may lead to falling behind on block height
logger.debug('Breaking mempool loop because the 5s time limit exceeded.');
break;
} else {
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100;
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
if (Math.floor(progress) < 100) {
loadingIndicators.setProgress('mempool', progress);
}
intervalTimer = Date.now()
}
}
}
}
@@ -246,12 +321,6 @@ class Mempool {
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
if (!this.inSync && transactions.length === newMempoolSize) {
this.inSync = true;
logger.notice('The mempool is now in sync!');
loadingIndicators.setProgress('mempool', 100);
}
this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize);
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
@@ -263,6 +332,19 @@ class Mempool {
this.updateTimerProgress(timer, 'completed async mempool callback');
}
if (!this.inSync && transactions.length === newMempoolSize) {
this.inSync = true;
logger.notice('The mempool is now in sync!');
loadingIndicators.setProgress('mempool', 100);
}
// Update Redis cache
if (config.REDIS.ENABLED) {
await redisCache.$flushTransactions();
await redisCache.$removeTransactions(deletedTransactions.map(tx => tx.txid));
await rbfCache.updateCache();
}
const end = new Date().getTime();
const time = end - start;
logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);

View File

@@ -131,7 +131,7 @@ class PoolsParser {
let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
if (config.MEMPOOL.NETWORK === 'testnet') {
firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
} else if (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'regtest') {
} else if (config.MEMPOOL.NETWORK === 'signet') {
firstKnownBlockPool = 0;
}
@@ -159,7 +159,7 @@ class PoolsParser {
let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
if (config.MEMPOOL.NETWORK === 'testnet') {
firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
} else if (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'regtest') {
} else if (config.MEMPOOL.NETWORK === 'signet') {
firstKnownBlockPool = 0;
}

View File

@@ -1,15 +1,17 @@
import config from "../config";
import logger from "../logger";
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import { Common } from "./common";
import redisCache from "./redis-cache";
interface RbfTransaction extends TransactionStripped {
export interface RbfTransaction extends TransactionStripped {
rbf?: boolean;
mined?: boolean;
fullRbf?: boolean;
}
interface RbfTree {
export interface RbfTree {
tx: RbfTransaction;
time: number;
interval?: number;
@@ -28,6 +30,19 @@ export interface ReplacementInfo {
newVsize: number;
}
enum CacheOp {
Remove = 0,
Add = 1,
Change = 2,
}
interface CacheEvent {
op: CacheOp;
type: 'tx' | 'tree' | 'exp';
txid: string,
value?: any,
}
class RbfCache {
private replacedBy: Map<string, string> = new Map();
private replaces: Map<string, string[]> = new Map();
@@ -36,11 +51,43 @@ class RbfCache {
private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
private txs: Map<string, MempoolTransactionExtended> = new Map();
private expiring: Map<string, number> = new Map();
private cacheQueue: CacheEvent[] = [];
constructor() {
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
}
private addTx(txid: string, tx: MempoolTransactionExtended): void {
this.txs.set(txid, tx);
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
}
private addTree(txid: string, tree: RbfTree): void {
this.rbfTrees.set(txid, tree);
this.dirtyTrees.add(txid);
this.cacheQueue.push({ op: CacheOp.Add, type: 'tree', txid });
}
private addExpiration(txid: string, expiry: number): void {
this.expiring.set(txid, expiry);
this.cacheQueue.push({ op: CacheOp.Add, type: 'exp', txid, value: expiry });
}
private removeTx(txid: string): void {
this.txs.delete(txid);
this.cacheQueue.push({ op: CacheOp.Remove, type: 'tx', txid });
}
private removeTree(txid: string): void {
this.rbfTrees.delete(txid);
this.cacheQueue.push({ op: CacheOp.Remove, type: 'tree', txid });
}
private removeExpiration(txid: string): void {
this.expiring.delete(txid);
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
}
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
return;
@@ -49,7 +96,7 @@ class RbfCache {
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
const newTime = newTxExtended.firstSeen || (Date.now() / 1000);
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
this.txs.set(newTx.txid, newTxExtended);
this.addTx(newTx.txid, newTxExtended);
// maintain rbf trees
let txFullRbf = false;
@@ -66,7 +113,7 @@ class RbfCache {
const treeId = this.treeMap.get(replacedTx.txid);
if (treeId) {
const tree = this.rbfTrees.get(treeId);
this.rbfTrees.delete(treeId);
this.removeTree(treeId);
if (tree) {
tree.interval = newTime - tree?.time;
replacedTrees.push(tree);
@@ -83,7 +130,7 @@ class RbfCache {
replaces: [],
});
treeFullRbf = treeFullRbf || !replacedTx.rbf;
this.txs.set(replacedTx.txid, replacedTxExtended);
this.addTx(replacedTx.txid, replacedTxExtended);
}
}
newTx.fullRbf = txFullRbf;
@@ -94,10 +141,9 @@ class RbfCache {
fullRbf: treeFullRbf,
replaces: replacedTrees
};
this.rbfTrees.set(treeId, newTree);
this.addTree(treeId, newTree);
this.updateTreeMap(treeId, newTree);
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
this.dirtyTrees.add(treeId);
}
public getReplacedBy(txId: string): string | undefined {
@@ -173,6 +219,7 @@ class RbfCache {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
}
}
this.evict(txid);
@@ -181,7 +228,8 @@ class RbfCache {
// flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void {
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
this.expiring.set(txid, fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400)); // 24 hours
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
this.addExpiration(txid, expiryTime);
}
}
@@ -202,11 +250,11 @@ class RbfCache {
const now = Date.now();
for (const txid of this.expiring.keys()) {
if ((this.expiring.get(txid) || 0) < now) {
this.expiring.delete(txid);
this.removeExpiration(txid);
this.remove(txid);
}
}
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.expiring.size} due to expire`);
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
@@ -216,14 +264,14 @@ class RbfCache {
const replaces = this.replaces.get(txid);
this.replaces.delete(txid);
this.treeMap.delete(txid);
this.txs.delete(txid);
this.expiring.delete(txid);
this.removeTx(txid);
this.removeExpiration(txid);
for (const tx of (replaces || [])) {
// recursively remove prior versions from the cache
this.replacedBy.delete(tx);
// if this is the id of a tree, remove that too
if (this.treeMap.get(tx) === tx) {
this.rbfTrees.delete(tx);
this.removeTree(tx);
}
this.remove(tx);
}
@@ -255,6 +303,33 @@ class RbfCache {
}
}
public async updateCache(): Promise<void> {
if (!config.REDIS.ENABLED) {
return;
}
// Update the Redis cache by replaying queued events
for (const e of this.cacheQueue) {
if (e.op === CacheOp.Add || e.op === CacheOp.Change) {
let value = e.value;
switch(e.type) {
case 'tx': {
value = this.txs.get(e.txid);
} break;
case 'tree': {
const tree = this.rbfTrees.get(e.txid);
value = tree ? this.exportTree(tree) : null;
} break;
}
if (value != null) {
await redisCache.$setRbfEntry(e.type, e.txid, value);
}
} else if (e.op === CacheOp.Remove) {
await redisCache.$removeRbfEntry(e.type, e.txid);
}
}
this.cacheQueue = [];
}
public dump(): any {
const trees = Array.from(this.rbfTrees.values()).map((tree: RbfTree) => { return this.exportTree(tree); });
@@ -360,8 +435,7 @@ class RbfCache {
};
this.treeMap.set(txid, root);
if (root === txid) {
this.rbfTrees.set(root, tree);
this.dirtyTrees.add(root);
this.addTree(root, tree);
}
return tree;
}

View File

@@ -0,0 +1,253 @@
import { createClient } from 'redis';
import memPool from './mempool';
import blocks from './blocks';
import logger from '../logger';
import config from '../config';
import { BlockExtended, BlockSummary, MempoolTransactionExtended } from '../mempool.interfaces';
import rbfCache from './rbf-cache';
import transactionUtils from './transaction-utils';
enum NetworkDB {
mainnet = 0,
testnet,
signet,
liquid,
liquidtestnet,
}
class RedisCache {
private client;
private connected = false;
private schemaVersion = 1;
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();
this.client.exists('mempool:0').then((mempoolExists) => {
if (!mempoolExists) {
for (let i = 0; i < 16; i++) {
this.client.json.set(`mempool:${i.toString(16)}`, '$', {});
}
}
});
}
}
private async $ensureConnected(): Promise<void> {
if (!this.connected && config.REDIS.ENABLED) {
return this.client.connect().then(async () => {
this.connected = true;
logger.info(`Redis client connected`);
const version = await this.client.get('schema_version');
if (version !== this.schemaVersion) {
// schema changed
// perform migrations or flush DB if necessary
logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`);
await this.client.set('schema_version', this.schemaVersion);
}
});
}
}
async $updateBlocks(blocks: BlockExtended[]) {
try {
await this.$ensureConnected();
await this.client.json.set('blocks', '$', blocks);
} catch (e) {
logger.warn(`Failed to update blocks in Redis cache: ${e instanceof Error ? e.message : e}`);
}
}
async $updateBlockSummaries(summaries: BlockSummary[]) {
try {
await this.$ensureConnected();
await this.client.json.set('block-summaries', '$', summaries);
} catch (e) {
logger.warn(`Failed to update blocks in Redis cache: ${e instanceof Error ? e.message : e}`);
}
}
async $addTransaction(tx: MempoolTransactionExtended) {
this.cacheQueue.push(tx);
if (this.cacheQueue.length >= this.txFlushLimit) {
await this.$flushTransactions();
}
}
async $flushTransactions() {
const success = await this.$addTransactions(this.cacheQueue);
if (success) {
logger.info(`Flushed ${this.cacheQueue.length} transactions to Redis cache`);
this.cacheQueue = [];
} else {
logger.err(`Failed to flush ${this.cacheQueue.length} transactions to Redis cache`);
}
}
private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> {
try {
await this.$ensureConnected();
await Promise.all(newTransactions.map(tx => {
const minified: any = { ...tx };
delete minified.hex;
for (const vin of minified.vin) {
delete vin.inner_redeemscript_asm;
delete vin.inner_witnessscript_asm;
delete vin.scriptsig_asm;
}
for (const vout of minified.vout) {
delete vout.scriptpubkey_asm;
}
return this.client.json.set(`mempool:${tx.txid.slice(0,1)}`, tx.txid, minified);
}));
return true;
} catch (e) {
logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
return false;
}
}
async $removeTransactions(transactions: string[]) {
try {
await this.$ensureConnected();
await Promise.all(transactions.map(txid => {
return this.client.json.del(`mempool:${txid.slice(0,1)}`, txid);
}));
} catch (e) {
logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
}
}
async $setRbfEntry(type: string, txid: string, value: any): Promise<void> {
try {
await this.$ensureConnected();
await this.client.json.set(`rbf:${type}:${txid}`, '$', value);
} catch (e) {
logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`);
}
}
async $removeRbfEntry(type: string, txid: string): Promise<void> {
try {
await this.$ensureConnected();
await this.client.del(`rbf:${type}:${txid}`);
} catch (e) {
logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`);
}
}
async $getBlocks(): Promise<BlockExtended[]> {
try {
await this.$ensureConnected();
return this.client.json.get('blocks');
} catch (e) {
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
return [];
}
}
async $getBlockSummaries(): Promise<BlockSummary[]> {
try {
await this.$ensureConnected();
return this.client.json.get('block-summaries');
} catch (e) {
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
return [];
}
}
async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> {
const start = Date.now();
let mempool = {};
try {
await this.$ensureConnected();
for (let i = 0; i < 16; i++) {
const shard = await this.client.json.get(`mempool:${i.toString(16)}`);
logger.info(`Loaded ${Object.keys(shard).length} transactions from redis cache ${i.toString(16)}`);
mempool = Object.assign(mempool, shard);
}
logger.info(`Total ${Object.keys(mempool).length} transactions loaded from redis cache `);
logger.info(`Loaded redis cache in ${Date.now() - start} ms`);
return mempool || {};
} catch (e) {
logger.warn(`Failed to retrieve mempool from Redis cache: ${e instanceof Error ? e.message : e}`);
}
return {};
}
async $getRbfEntries(type: string): Promise<any[]> {
try {
await this.$ensureConnected();
const keys = await this.client.keys(`rbf:${type}:*`);
const promises: Promise<MempoolTransactionExtended[]>[] = [];
for (let i = 0; i < keys.length; i += 10000) {
const keySlice = keys.slice(i, i + 10000);
if (!keySlice.length) {
continue;
}
promises.push(this.client.json.mGet(keySlice, '$').then(chunk => chunk?.length ? chunk.flat().map((v, i) => [keySlice[i].slice(`rbf:${type}:`.length), v]) : [] ));
}
const entries = await Promise.all(promises);
return entries.flat();
} catch (e) {
logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: ${e instanceof Error ? e.message : e}`);
return [];
}
}
async $loadCache() {
logger.info('Restoring mempool and blocks data from Redis cache');
// Load block data
const loadedBlocks = await this.$getBlocks();
const loadedBlockSummaries = await this.$getBlockSummaries();
// Load mempool
const loadedMempool = await this.$getMempool();
this.inflateLoadedTxs(loadedMempool);
// Load rbf data
const rbfTxs = await this.$getRbfEntries('tx');
const rbfTrees = await this.$getRbfEntries('tree');
const rbfExpirations = await this.$getRbfEntries('exp');
// Set loaded data
blocks.setBlocks(loadedBlocks || []);
blocks.setBlockSummaries(loadedBlockSummaries || []);
await memPool.$setMempool(loadedMempool);
await rbfCache.load({
txs: rbfTxs,
trees: rbfTrees.map(loadedTree => loadedTree[1]),
expiring: rbfExpirations,
});
}
private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }) {
for (const tx of Object.values(mempool)) {
for (const vin of tx.vin) {
if (vin.scriptsig) {
vin.scriptsig_asm = transactionUtils.convertScriptSigAsm(vin.scriptsig);
transactionUtils.addInnerScriptsToVin(vin);
}
}
for (const vout of tx.vout) {
if (vout.scriptpubkey) {
vout.scriptpubkey_asm = transactionUtils.convertScriptSigAsm(vout.scriptpubkey);
}
}
}
}
}
export default new RedisCache();

View File

@@ -53,7 +53,7 @@ class TransactionUtils {
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
}
private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
public extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
// @ts-ignore
if (transaction.vsize) {
// @ts-ignore
@@ -170,6 +170,122 @@ class TransactionUtils {
16
);
}
public addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
if (!vin.prevout) {
return;
}
if (vin.prevout.scriptpubkey_type === 'p2sh') {
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
if (vin.witness && vin.witness.length > 2) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
}
}
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
}
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
const witnessScript = this.witnessToP2TRScript(vin.witness);
if (witnessScript !== null) {
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
}
}
}
public convertScriptSigAsm(hex: string): string {
const buf = Buffer.from(hex, 'hex');
const b: string[] = [];
let i = 0;
while (i < buf.length) {
const op = buf[i];
if (op >= 0x01 && op <= 0x4e) {
i++;
let push: number;
if (op === 0x4c) {
push = buf.readUInt8(i);
b.push('OP_PUSHDATA1');
i += 1;
} else if (op === 0x4d) {
push = buf.readUInt16LE(i);
b.push('OP_PUSHDATA2');
i += 2;
} else if (op === 0x4e) {
push = buf.readUInt32LE(i);
b.push('OP_PUSHDATA4');
i += 4;
} else {
push = op;
b.push('OP_PUSHBYTES_' + push);
}
const data = buf.slice(i, i + push);
if (data.length !== push) {
break;
}
b.push(data.toString('hex'));
i += data.length;
} else {
if (op === 0x00) {
b.push('OP_0');
} else if (op === 0x4f) {
b.push('OP_PUSHNUM_NEG1');
} else if (op === 0xb1) {
b.push('OP_CLTV');
} else if (op === 0xb2) {
b.push('OP_CSV');
} else if (op === 0xba) {
b.push('OP_CHECKSIGADD');
} else {
const opcode = bitcoinjs.script.toASM([ op ]);
if (opcode && op < 0xfd) {
if (/^OP_(\d+)$/.test(opcode)) {
b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
} else {
b.push(opcode);
}
} else {
b.push('OP_RETURN_' + op);
}
}
i += 1;
}
}
return b.join(' ');
}
/**
* This function must only be called when we know the witness we are parsing
* is a taproot witness.
* @param witness An array of hex strings that represents the witness stack of
* the input.
* @returns null if the witness is not a script spend, and the hex string of
* the script item if it is a script spend.
*/
public witnessToP2TRScript(witness: string[]): string | null {
if (witness.length < 2) return null;
// Note: see BIP341 for parsing details of witness stack
// If there are at least two witness elements, and the first byte of the
// last element is 0x50, this last element is called annex a and
// is removed from the witness stack.
const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50';
// If there are at least two witness elements left, script path spending is used.
// Call the second-to-last stack element s, the script.
// (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
if (hasAnnex && witness.length < 3) return null;
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
return witness[positionOfScript];
}
}
export default new TransactionUtils();

View File

@@ -604,7 +604,7 @@ class WebsocketHandler {
}
}
if (client['track-mempool-block'] >= 0) {
if (client['track-mempool-block'] >= 0 && memPool.isInSync()) {
const index = client['track-mempool-block'];
if (mBlockDeltas[index]) {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
@@ -644,7 +644,7 @@ class WebsocketHandler {
memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions);
if (config.MEMPOOL.AUDIT) {
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
let projectedBlocks;
let auditMempool = _memPool;
// template calculation functions have mempool side effects, so calculate audits using
@@ -665,7 +665,7 @@ class WebsocketHandler {
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
}
if (Common.indexingEnabled() && memPool.isInSync()) {
if (Common.indexingEnabled()) {
const { censored, added, fresh, sigop, fullrbf, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100;
@@ -858,7 +858,7 @@ class WebsocketHandler {
}
}
if (client['track-mempool-block'] >= 0) {
if (client['track-mempool-block'] >= 0 && memPool.isInSync()) {
const index = client['track-mempool-block'];
if (mBlockDeltas && mBlockDeltas[index]) {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {

View File

@@ -5,13 +5,14 @@ const configFromFile = require(
interface IConfig {
MEMPOOL: {
ENABLED: boolean;
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'regtest' | 'liquid' | 'liquidtestnet';
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
BACKEND: 'esplora' | 'electrum' | 'none';
HTTP_PORT: number;
SPAWN_CLUSTER_PROCS: number;
API_URL_PREFIX: string;
POLL_RATE_MS: number;
CACHE_DIR: string;
CACHE_ENABLED: boolean;
CLEAR_PROTECTION_MINUTES: number;
RECOMMENDED_FEE_PERCENTILE: number;
BLOCK_WEIGHT_UNITS: number;
@@ -137,7 +138,11 @@ interface IConfig {
AUDIT: boolean;
AUDIT_START_HEIGHT: number;
SERVERS: string[];
}
},
REDIS: {
ENABLED: boolean;
UNIX_SOCKET_PATH: string;
},
}
const defaults: IConfig = {
@@ -150,6 +155,7 @@ const defaults: IConfig = {
'API_URL_PREFIX': '/api/v1/',
'POLL_RATE_MS': 2000,
'CACHE_DIR': './cache',
'CACHE_ENABLED': true,
'CLEAR_PROTECTION_MINUTES': 20,
'RECOMMENDED_FEE_PERCENTILE': 50,
'BLOCK_WEIGHT_UNITS': 4000000,
@@ -275,7 +281,11 @@ const defaults: IConfig = {
'AUDIT': false,
'AUDIT_START_HEIGHT': 774000,
'SERVERS': [],
}
},
'REDIS': {
'ENABLED': false,
'UNIX_SOCKET_PATH': '',
},
};
class Config implements IConfig {
@@ -296,6 +306,7 @@ class Config implements IConfig {
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
MAXMIND: IConfig['MAXMIND'];
REPLICATION: IConfig['REPLICATION'];
REDIS: IConfig['REDIS'];
constructor() {
const configs = this.merge(configFromFile, defaults);
@@ -316,6 +327,7 @@ class Config implements IConfig {
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
this.MAXMIND = configs.MAXMIND;
this.REPLICATION = configs.REPLICATION;
this.REDIS = configs.REDIS;
}
merge = (...objects: object[]): IConfig => {

View File

@@ -41,6 +41,7 @@ import chainTips from './api/chain-tips';
import { AxiosError } from 'axios';
import v8 from 'v8';
import { formatBytes, getBytesUnit } from './utils/format';
import redisCache from './api/redis-cache';
class Server {
private wss: WebSocket.Server | undefined;
@@ -122,7 +123,11 @@ class Server {
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
await syncAssets.syncAssets$();
if (config.MEMPOOL.ENABLED) {
await diskCache.$loadMempoolCache();
if (config.MEMPOOL.CACHE_ENABLED) {
await diskCache.$loadMempoolCache();
} else if (config.REDIS.ENABLED) {
await redisCache.$loadCache();
}
}
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {

View File

@@ -99,7 +99,7 @@ class PoolsRepository {
if (parse) {
rows[0].regexes = JSON.parse(rows[0].regexes);
}
if (['testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
if (['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
rows[0].addresses = []; // pools-v2.json only contains mainnet addresses
} else if (parse) {
rows[0].addresses = JSON.parse(rows[0].addresses);
@@ -131,7 +131,7 @@ class PoolsRepository {
if (parse) {
rows[0].regexes = JSON.parse(rows[0].regexes);
}
if (['testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
if (['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
rows[0].addresses = []; // pools.json only contains mainnet addresses
} else if (parse) {
rows[0].addresses = JSON.parse(rows[0].addresses);

View File

@@ -17,7 +17,7 @@ class PoolsUpdater {
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
public async updatePoolsJson(): Promise<void> {
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK) === false ||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
config.MEMPOOL.ENABLED === false
) {
return;

View File

@@ -73,7 +73,7 @@ class PriceUpdater {
}
public async $run(): Promise<void> {
if (['testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
if (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'testnet') {
// Coins have no value on testnet/signet, so we want to always show 0
return;
}

View File

@@ -18,7 +18,6 @@ fi
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
__REGTEST_ENABLED__=${REGTEST_ENABLED:=false}
__LIQUID_ENABLED__=${LIQUID_EANBLED:=false}
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
@@ -45,7 +44,6 @@ __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
# Export as environment variables to be used by envsubst
export __TESTNET_ENABLED__
export __SIGNET_ENABLED__
export __REGTEST_ENABLED__
export __LIQUID_ENABLED__
export __LIQUID_TESTNET_ENABLED__
export __BISQ_ENABLED__

View File

@@ -1,7 +1,6 @@
{
"TESTNET_ENABLED": false,
"SIGNET_ENABLED": false,
"REGTEST_ENABLED": false,
"LIQUID_ENABLED": false,
"LIQUID_TESTNET_ENABLED": false,
"BISQ_ENABLED": false,

View File

@@ -249,115 +249,6 @@ let routes: Routes = [
},
]
},
{
path: 'regtest',
children: [
{
path: 'mining/blocks',
redirectTo: 'blocks',
pathMatch: 'full'
},
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
},
{
path: '',
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',
data: { networks: ['bitcoin', 'liquid'] },
component: StatusViewComponent
},
{
path: '',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
},
{
path: '**',
redirectTo: '/signet'
},
]
},
{
path: '',
pathMatch: 'full',

View File

@@ -271,11 +271,6 @@ const featureActivation = {
segwit: 0,
taproot: 0,
},
regtest: {
rbf: 0,
segwit: 0,
taproot: 0,
},
};
export function isFeatureActive(network: string, height: number, feature: 'rbf' | 'segwit' | 'taproot'): boolean {

View File

@@ -39,7 +39,7 @@
</ng-container>
</a>
<div ngbDropdown (window:resize)="onResize($event)" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.REGTEST_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
<div ngbDropdown (window:resize)="onResize($event)" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" name="bisq" width="20" height="20" viewBox="0 0 80 80"></app-svg-images>
</button>
@@ -47,7 +47,6 @@
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '/')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['regtest'] || '/regtest')" ngbDropdownItem *ngIf="env.REGTEST_ENABLED" class="regtest"><app-svg-images name="regtest" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Regtest</a>
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a ngbDropdownItem class="mainnet active" [routerLink]="networkPaths['bisq'] || '/'"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '/')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>

View File

@@ -120,10 +120,6 @@ nav {
background-color: #6f1d5d;
}
.regtest.active {
background-color: #a5a5a5;
}
.dropdown-divider {
border-top: 1px solid #121420;
}

View File

@@ -676,7 +676,6 @@ export class BlockComponent implements OnInit, OnDestroy {
}
break;
case 'signet':
case 'regtest':
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
return false;
}

View File

@@ -68,7 +68,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
'liquidtestnet': ['#494a4a', '#272e46'],
testnet: ['#1d486f', '#183550'],
signet: ['#6f1d5d', '#471850'],
regtest: ['#9339f4', '#105fb0'],
};
constructor(
@@ -87,7 +86,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
ngOnInit() {
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
if (['', 'testnet', 'signet', 'regtest'].includes(this.stateService.network)) {
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
this.enabledMiningInfoIfNeeded(this.location.path());
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
}

View File

@@ -38,7 +38,6 @@ export class ClockComponent implements OnInit {
'liquidtestnet': ['#494a4a', '#272e46'],
testnet: ['#1d486f', '#183550'],
signet: ['#6f1d5d', '#471850'],
regtest: ['#9339f4', '#105fb0'],
};
constructor(

View File

@@ -44,7 +44,7 @@
</ng-container>
</a>
<div ngbDropdown (window:resize)="onResize()" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.REGTEST_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
<div ngbDropdown (window:resize)="onResize()" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'liquid' : network.val" width="20" height="20" viewBox="0 0 125 125"></app-svg-images>
</button>
@@ -52,7 +52,6 @@
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['regtest'] || '/regtest')" ngbDropdownItem *ngIf="env.REGTEST_ENABLED" class="regtest"><app-svg-images name="regtest" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Regtest</a>
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>

View File

@@ -114,10 +114,6 @@ nav {
background-color: #6f1d5d;
}
.regtest.active {
background-color: #a5a5a5;
}
.dropdown-divider {
border-top: 1px solid #121420;
}

View File

@@ -9,7 +9,6 @@
<div [ngSwitch]="network.val">
<span *ngSwitchCase="'signet'" class="network signet"><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Signet</span>
<span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span>
<span *ngSwitchCase="'regtest'" class="network regtest"><app-svg-images name="regtest" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Regtest</span>
<span *ngSwitchCase="'bisq'" class="network bisq"><app-svg-images name="bisq" width="35" height="35" viewBox="0 0 75 75" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Bisq</span>
<span *ngSwitchCase="'liquid'" class="network liquid"><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span>
<span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span>

View File

@@ -17,7 +17,7 @@
</ng-container>
</a>
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.REGTEST_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65"></app-svg-images>
</button>
@@ -25,7 +25,6 @@
<a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
<a ngbDropdownItem *ngIf="env.REGTEST_ENABLED" class="regtest" [class.active]="network.val === 'regtest'" [routerLink]="networkPaths['regtest'] || '/regtest'"><app-svg-images name="regtest" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Regtest</a>
<h6 *ngIf="env.LIQUID_ENABLED || env.BISQ_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
@@ -63,7 +62,7 @@
</nav>
</header>
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet' || network.val === 'regtest'"></app-testnet-alert>
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>
<main>
<router-outlet></router-outlet>

View File

@@ -131,10 +131,6 @@ nav {
background-color: #6f1d5d;
}
.regtest.active {
background-color: #a5a5a5;
}
.dropdown-divider {
border-top: 1px solid #121420;
}

View File

@@ -93,7 +93,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
ngOnInit() {
this.chainTip = this.stateService.latestBlockHeight;
if (['', 'testnet', 'signet', 'regtest'].includes(this.stateService.network)) {
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
this.enabledMiningInfoIfNeeded(this.location.path());
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
}
@@ -117,7 +117,14 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
});
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
this.loadingBlocks$ = this.stateService.isLoadingWebSocket$;
this.loadingBlocks$ = combineLatest([
this.stateService.isLoadingWebSocket$,
this.stateService.isLoadingMempool$
]).pipe(
switchMap(([loadingBlocks, loadingMempool]) => {
return of(loadingBlocks || loadingMempool);
})
);
this.mempoolBlocks$ = merge(
of(true),

View File

@@ -60,9 +60,6 @@
<ng-container *ngSwitchCase="'testnet'">
<ng-component *ngTemplateOutlet="bitcoinLogo; context: {$implicit: '#5fd15c', width, height, viewBox}"></ng-component>
</ng-container>
<ng-container *ngSwitchCase="'regtest'">
<ng-component *ngTemplateOutlet="bitcoinLogo; context: {$implicit: '#a5a5a5', width, height, viewBox}"></ng-component>
</ng-container>
<ng-container *ngSwitchCase="'bisq'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox"><path id="Combined-Shape" fill="#25b135" d="M62.6 18.2c2.6 5.7 3.2 6.8 5.2 12.6 1.1 3.6-1.2 9-10.6 4.4-6.8-3.2-11.7-5.5-7.6-10.9 2-2.6 4.2-5 6.6-7.3 2.5-2.2 4.1-3.6 6.4 1.2zm-2.3 36.4c5-4.9 11.9.6 7.1 7.1C57.6 75 40.7 80.3 25.5 74.9S0 55 0 38.3c0-10 3.3-18.4 9.6-25.9C15.7 5.6 24.8.1 33.8 0 46.4 0 52 11 47.6 17.2c-5 6.8-10.7 6.3-15 3.3 0 0-5.9-4.2-7.6-5.6-2.7-2.1-5.6-2.1-6.7 1.3-1.9 6-2.8 9.8-4.3 15.8-.7 3-1 6.1-1.1 9.2 0 5.7 2.5 11.1 6.8 14.6 3 2.8 6.1 5.3 9.5 7.5 2.7 1.9 5.9 2.9 9.1 2.9h.3c3.2-.1 6.4-1.1 9.1-2.9 4.4-2.6 8.6-5.5 12.6-8.7zm-35.4-7c-1.8-.7-5.8-4.2-4.6-6.8.5-1.4 3.9-1.8 5-1.8 6.2-.1 6.5 11.1-.4 8.6zm11.1 12c-.8-.7-2.3-2.2-3.6-3.4-.4-.4-.4-1-.2-1.4.3-.5.8-.8 1.4-.7 2.4-.1 4.8-.1 7.1 0 .6 0 1 .4 1.2.8.2.5.1 1-.3 1.3-1.3 1.2-2.8 2.7-3.5 3.4-.3.3-.7.4-1.1.4-.4 0-.7-.1-1-.4zm14.1-12c-6.9 2.5-6.7-8.8-.4-8.6 1.1 0 4.4.4 5 1.8 1.3 2.7-2.8 6.2-4.6 6.8z"/></svg>
</ng-container>

View File

@@ -92,7 +92,6 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
testnet: ['#4edf77', '#10a0af', '#4edf7700'],
// signet: ['#6f1d5d', '#471850'],
signet: ['#d24fc8', '#a84fd2', '#d24fc800'],
regtest: ['#9339f4', '#105fb0', '#9339f400'],
};
gradient: string[] = ['#105fb0', '#105fb0'];

View File

@@ -1,19 +1,43 @@
<div class="box">
<table class="table table-borderless table-striped">
<tbody>
<tr></tr>
<tr>
<td i18n="lightning.starting-balance|Channel starting balance">Starting balance</td>
<td *ngIf="showStartingBalance && minStartingBalance === maxStartingBalance"><app-sats [satoshis]="minStartingBalance"></app-sats></td>
<td *ngIf="showStartingBalance && minStartingBalance !== maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td>
<td *ngIf="!showStartingBalance">?</td>
</tr>
<tr *ngIf="channel.status === 2">
<td i18n="lightning.closing-balance|Channel closing balance">Closing balance</td>
<td *ngIf="showClosingBalance && minClosingBalance === maxClosingBalance"><app-sats [satoshis]="minClosingBalance"></app-sats></td>
<td *ngIf="showClosingBalance && minClosingBalance !== maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td>
<td *ngIf="!showClosingBalance">?</td>
</tr>
</tbody>
</table>
<div class="starting-balance" *ngIf="showStartingBalance">
<h5 i18n="lightning.starting-balance|Channel starting balance">Starting balance</h5>
<div class="nodes">
<h5 class="alias">{{ left.alias }}</h5>
<h5 class="alias">{{ right.alias }}</h5>
</div>
<div class="balances">
<div class="balance left">
<span class="value" *ngIf="minStartingBalance !== maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
<span class="value" *ngIf="minStartingBalance === maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
</div>
<div class="balance right">
<span class="value" *ngIf="minStartingBalance !== maxStartingBalance">{{ channel.capacity - maxStartingBalance | number : '1.0-0' }} - {{ channel.capacity - minStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
<span class="value" *ngIf="minStartingBalance === maxStartingBalance">{{ channel.capacity - maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
</div>
</div>
<div class="balance-bar">
<div class="bar left" [class.hide-value]="hideStartingLeft" [style]="startingBalanceStyle.left"></div>
<div class="bar center" [style]="startingBalanceStyle.center"></div>
<div class="bar right" [class.hide-value]="hideStartingRight" [style]="startingBalanceStyle.right"></div>
</div>
</div>
<br>
<div class="closing-balance" *ngIf="showClosingBalance">
<h5 i18n="lightning.closing-balance|Channel closing balance">Closing balance</h5>
<div class="balances">
<div class="balance left">
<span class="value" *ngIf="minClosingBalance !== maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
<span class="value" *ngIf="minClosingBalance === maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
</div>
<div class="balance right">
<span class="value" *ngIf="minClosingBalance !== maxClosingBalance">{{ channel.capacity - maxClosingBalance | number : '1.0-0' }} - {{ channel.capacity - minClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
<span class="value" *ngIf="minClosingBalance === maxClosingBalance">{{ channel.capacity - maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
</div>
</div>
<div class="balance-bar">
<div class="bar left" [class.hide-value]="hideClosingLeft" [style]="closingBalanceStyle.left"></div>
<div class="bar center" [style]="closingBalanceStyle.center"></div>
<div class="bar right" [class.hide-value]="hideClosingRight" [style]="closingBalanceStyle.right"></div>
</div>
</div>
</div>

View File

@@ -6,4 +6,98 @@
.box {
margin-bottom: 20px;
}
}
.starting-balance, .closing-balance {
width: 100%;
h5 {
text-align: center;
}
}
.nodes {
display: none;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
@media (max-width: 768px) {
display: flex;
}
}
.balances {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
margin-bottom: 8px;
.balance {
&.left {
text-align: start;
}
&.right {
text-align: end;
}
}
}
.balance-bar {
width: 100%;
height: 2em;
position: relative;
.bar {
position: absolute;
top: 0;
bottom: 0;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
&.left {
background: #105fb0;
}
&.center {
background: repeating-linear-gradient(
60deg,
#105fb0 0,
#105fb0 12px,
#1a9436 12px,
#1a9436 24px
);
}
&.right {
background: #1a9436;
}
.value {
flex: 0;
white-space: nowrap;
}
&.hide-value {
.value {
display: none;
}
}
}
@media (max-width: 768px) {
height: 1em;
.bar.center {
background: repeating-linear-gradient(
60deg,
#105fb0 0,
#105fb0 8px,
#1a9436 8px,
#1a9436 16px
)
}
}
}

View File

@@ -8,8 +8,8 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } f
})
export class ChannelCloseBoxComponent implements OnChanges {
@Input() channel: any;
@Input() local: any;
@Input() remote: any;
@Input() left: any;
@Input() right: any;
showStartingBalance: boolean = false;
showClosingBalance: boolean = false;
@@ -18,29 +18,55 @@ export class ChannelCloseBoxComponent implements OnChanges {
minClosingBalance: number;
maxClosingBalance: number;
startingBalanceStyle: {
left: string,
center: string,
right: string,
} = {
left: '',
center: '',
right: '',
};
closingBalanceStyle: {
left: string,
center: string,
right: string,
} = {
left: '',
center: '',
right: '',
};
hideStartingLeft: boolean = false;
hideStartingRight: boolean = false;
hideClosingLeft: boolean = false;
hideClosingRight: boolean = false;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
if (this.channel && this.local && this.remote) {
this.showStartingBalance = (this.local.funding_balance || this.remote.funding_balance) && this.channel.funding_ratio;
this.showClosingBalance = this.local.closing_balance || this.remote.closing_balance;
let closingCapacity;
if (this.channel && this.left && this.right) {
this.showStartingBalance = (this.left.funding_balance || this.right.funding_balance) && this.channel.funding_ratio;
this.showClosingBalance = this.left.closing_balance || this.right.closing_balance;
if (this.channel.single_funded) {
if (this.local.funding_balance) {
if (this.left.funding_balance) {
this.minStartingBalance = this.channel.capacity;
this.maxStartingBalance = this.channel.capacity;
} else if (this.remote.funding_balance) {
} else if (this.right.funding_balance) {
this.minStartingBalance = 0;
this.maxStartingBalance = 0;
}
} else {
this.minStartingBalance = clampRound(0, this.channel.capacity, this.local.funding_balance * this.channel.funding_ratio);
this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.remote.funding_balance * this.channel.funding_ratio));
this.minStartingBalance = clampRound(0, this.channel.capacity, this.left.funding_balance * this.channel.funding_ratio);
this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.right.funding_balance * this.channel.funding_ratio));
}
const closingCapacity = this.channel.capacity - this.channel.closing_fee;
this.minClosingBalance = clampRound(0, closingCapacity, this.local.closing_balance);
this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.remote.closing_balance);
closingCapacity = this.channel.capacity - this.channel.closing_fee;
this.minClosingBalance = clampRound(0, closingCapacity, this.left.closing_balance);
this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.right.closing_balance);
// margin of error to account for 2 x 330 sat anchor outputs
if (Math.abs(this.minClosingBalance - this.maxClosingBalance) <= 660) {
@@ -50,6 +76,26 @@ export class ChannelCloseBoxComponent implements OnChanges {
this.showStartingBalance = false;
this.showClosingBalance = false;
}
const startingMinPc = (this.minStartingBalance / this.channel.capacity) * 100;
const startingMaxPc = (this.maxStartingBalance / this.channel.capacity) * 100;
this.startingBalanceStyle = {
left: `left: 0%; right: ${100 - startingMinPc}%;`,
center: `left: ${startingMinPc}%; right: ${100 -startingMaxPc}%;`,
right: `left: ${startingMaxPc}%; right: 0%;`,
};
this.hideStartingLeft = startingMinPc < 15;
this.hideStartingRight = startingMaxPc > 85;
const closingMinPc = (this.minClosingBalance / closingCapacity) * 100;
const closingMaxPc = (this.maxClosingBalance / closingCapacity) * 100;
this.closingBalanceStyle = {
left: `left: 0%; right: ${100 - closingMinPc}%;`,
center: `left: ${closingMinPc}%; right: ${100 - closingMaxPc}%;`,
right: `left: ${closingMaxPc}%; right: 0%;`,
};
this.hideClosingLeft = closingMinPc < 15;
this.hideClosingRight = closingMaxPc > 85;
}
}

View File

@@ -75,14 +75,14 @@
<div class="row row-cols-1 row-cols-md-2" *ngIf="!error">
<div class="col">
<app-channel-box [channel]="channel.node_left"></app-channel-box>
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [local]="channel.node_left" [remote]="channel.node_right"></app-channel-close-box>
</div>
<div class="col">
<app-channel-box [channel]="channel.node_right"></app-channel-box>
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [local]="channel.node_right" [remote]="channel.node_left"></app-channel-close-box>
</div>
</div>
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [left]="channel.node_left" [right]="channel.node_right"></app-channel-close-box>
<br>
<ng-container *ngIf="transactions$ | async as transactions">

View File

@@ -38,7 +38,6 @@ export class EnterpriseService {
this.stateService.env.LIQUID_ENABLED = false;
this.stateService.env.LIQUID_TESTNET_ENABLED = false;
this.stateService.env.SIGNET_ENABLED = false;
this.stateService.env.REGTEST_ENABLED = false;
this.stateService.env.BISQ_ENABLED = false;
}

View File

@@ -10,7 +10,6 @@ const networkModules = {
{ name: 'mainnet', path: '' },
{ name: 'testnet', path: '/testnet' },
{ name: 'signet', path: '/signet' },
{ name: 'regtest', path: '/regtest' },
],
},
liquid: {
@@ -74,7 +73,7 @@ export class NavigationService {
}
if (route.url?.length) {
path = [path, ...route.url.map(segment => segment.path).filter(path => {
return path.length && !['testnet', 'signet', 'regtest'].includes(path);
return path.length && !['testnet', 'signet'].includes(path);
})].join('/');
}
route = route.firstChild;

View File

@@ -41,8 +41,6 @@ export class SeoService {
return this.baseTitle + ' - Bitcoin Testnet';
if (this.network === 'signet')
return this.baseTitle + ' - Bitcoin Signet';
if (this.network === 'regtest')
return this.baseTitle + ' - Bitcoin Regtest';
if (this.network === 'liquid')
return this.baseTitle + ' - Liquid Network';
if (this.network === 'liquidtestnet')

View File

@@ -21,7 +21,6 @@ export interface ILoadingIndicators { [name: string]: number; }
export interface Env {
TESTNET_ENABLED: boolean;
SIGNET_ENABLED: boolean;
REGTEST_ENABLED: boolean;
LIQUID_ENABLED: boolean;
LIQUID_TESTNET_ENABLED: boolean;
BISQ_ENABLED: boolean;
@@ -52,7 +51,6 @@ export interface Env {
const defaultEnv: Env = {
'TESTNET_ENABLED': false,
'SIGNET_ENABLED': false,
'REGTEST_ENABLED': false,
'LIQUID_ENABLED': false,
'LIQUID_TESTNET_ENABLED': false,
'BASE_MODULE': 'mempool',
@@ -115,6 +113,7 @@ export class StateService {
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition}>();
blockTransactions$ = new Subject<Transaction>();
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
isLoadingMempool$ = new BehaviorSubject<boolean>(true);
vbytesPerSecond$ = new ReplaySubject<number>(1);
previousRetarget$ = new ReplaySubject<number>(1);
backendInfo$ = new ReplaySubject<IBackendInfo>(1);
@@ -249,9 +248,9 @@ export class StateService {
// /^\/ starts with a forward slash...
// (?:[a-z]{2}(?:-[A-Z]{2})?\/)? optional locale prefix (non-capturing)
// (?:preview\/)? optional "preview" prefix (non-capturing)
// (bisq|testnet|liquidtestnet|liquid|signet|regtest)/ network string (captured as networkMatches[1])
// (bisq|testnet|liquidtestnet|liquid|signet)/ network string (captured as networkMatches[1])
// ($|\/) network string must end or end with a slash
const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(bisq|testnet|liquidtestnet|liquid|signet|regtest)($|\/)/);
const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(bisq|testnet|liquidtestnet|liquid|signet)($|\/)/);
switch (networkMatches && networkMatches[1]) {
case 'liquid':
if (this.network !== 'liquid') {
@@ -271,12 +270,6 @@ export class StateService {
this.networkChanged$.next('signet');
}
return;
case 'regtest':
if (this.network !== 'regtest') {
this.network = 'regtest';
this.networkChanged$.next('regtest');
}
return;
case 'testnet':
if (this.network !== 'testnet') {
if (this.env.BASE_MODULE === 'liquid') {

View File

@@ -368,6 +368,11 @@ export class WebsocketService {
if (response.loadingIndicators) {
this.stateService.loadingIndicators$.next(response.loadingIndicators);
if (response.loadingIndicators.mempool != null && response.loadingIndicators.mempool < 100) {
this.stateService.isLoadingMempool$.next(true);
} else {
this.stateService.isLoadingMempool$.next(false);
}
}
if (response.mempoolInfo) {

View File

@@ -55,7 +55,7 @@ export class GlobalFooterComponent implements OnInit {
networkLink(network) {
const thisNetwork = network || 'mainnet';
if( network === '' || network === 'mainnet' || network === 'testnet' || network === 'signet' || network === 'regtest' ) {
if( network === '' || network === 'mainnet' || network === 'testnet' || network === 'signet' ) {
return (this.env.BASE_MODULE === 'mempool' ? '' : this.env.MEMPOOL_WEBSITE_URL + this.urlLanguage) + this.networkPaths[thisNetwork] || '/';
}
if( network === 'liquid' || network === 'liquidtestnet' ) {

View File

@@ -111,10 +111,6 @@ main {
background-color: #6f1d5d !important;
}
.navbar-nav.regtest > .active {
background-color: #a5a5a5 !important;
}
.navbar-nav.liquidtestnet > .active {
background-color: #494a4a !important;
}