diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c435d6ea5..b2d34bb03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,10 @@ jobs: - name: Sync-assets run: npm run sync-assets-dev + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MEMPOOL_CDN: 1 + VERBOSE: 1 working-directory: assets/frontend - name: Zip mining-pool assets @@ -237,6 +241,8 @@ jobs: working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MEMPOOL_CDN: 1 + VERBOSE: 1 e2e: if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" @@ -329,4 +335,32 @@ jobs: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} - \ No newline at end of file + + validate_docker_json: + if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" + runs-on: "ubuntu-latest" + name: Validate generated backend Docker JSON + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + path: docker + + - name: Install jq + run: sudo apt-get install jq -y + + - name: Create new start script to run on CI + run: | + sed '$d' start.sh > start_ci.sh + working-directory: docker/docker/backend + + - name: Run the script to generate the sample JSON + run: | + sh start_ci.sh + working-directory: docker/docker/backend + + - name: Validate JSON syntax + run: | + cat mempool-config.json | jq + working-directory: docker/docker/backend diff --git a/.gitignore b/.gitignore index 4f19f2522..381f2187c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ backend/mempool-config.json frontend/src/resources/config.template.js frontend/src/resources/config.js target +docker/backend/start_ci.sh \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index b4393c2f0..5cefd4bab 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -7,6 +7,12 @@ mempool-config.json pools.json icons.json +# docker +Dockerfile +GeoIP +start.sh +wait-for-it.sh + # compiled output /dist /tmp diff --git a/backend/src/api/bisq/markets-api.ts b/backend/src/api/bisq/markets-api.ts index 54e0297b7..1b5b93059 100644 --- a/backend/src/api/bisq/markets-api.ts +++ b/backend/src/api/bisq/markets-api.ts @@ -646,7 +646,7 @@ class BisqMarketsApi { case 'year': return strtotime('midnight first day of january', ts); default: - throw new Error('Unsupported interval: ' + interval); + throw new Error('Unsupported interval'); } } diff --git a/backend/src/api/bitcoin/bitcoin-api.interface.ts b/backend/src/api/bitcoin/bitcoin-api.interface.ts index 3afc22897..e176566d7 100644 --- a/backend/src/api/bitcoin/bitcoin-api.interface.ts +++ b/backend/src/api/bitcoin/bitcoin-api.interface.ts @@ -106,6 +106,7 @@ export namespace IBitcoinApi { address?: string; // (string) bitcoin address addresses?: string[]; // (string) bitcoin addresses pegout_chain?: string; // (string) Elements peg-out chain + pegout_address?: string; // (string) Elements peg-out address pegout_addresses?: string[]; // (string) Elements peg-out addresses }; } diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 2cd043fe2..837bc0ee9 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -2,7 +2,7 @@ import config from '../config'; import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; import logger from '../logger'; import memPool from './mempool'; -import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified } from '../mempool.interfaces'; +import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces'; import { Common } from './common'; import diskCache from './disk-cache'; import transactionUtils from './transaction-utils'; @@ -451,7 +451,9 @@ class Blocks { if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx)); const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs); - await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary + if (cpfpSummary) { + await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary + } } else { await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary } @@ -995,11 +997,11 @@ class Blocks { return state; } - private updateTimerProgress(state, msg) { + private updateTimerProgress(state, msg): void { state.progress = msg; } - private clearTimer(state) { + private clearTimer(state): void { if (state.timer) { clearTimeout(state.timer); } @@ -1088,13 +1090,19 @@ class Blocks { summary = { id: hash, transactions: cpfpSummary.transactions.map(tx => { + let flags: number = 0; + try { + flags = tx.flags || Common.getTransactionFlags(tx); + } catch (e) { + logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e)); + } return { txid: tx.txid, fee: tx.fee || 0, vsize: tx.vsize, value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)), rate: tx.effectiveFeePerVsize, - flags: tx.flags || Common.getTransactionFlags(tx), + flags: flags, }; }), }; @@ -1284,7 +1292,7 @@ class Blocks { return blocks; } - public async $getBlockAuditSummary(hash: string): Promise { + public async $getBlockAuditSummary(hash: string): Promise { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { return BlocksAuditsRepository.$getBlockAudit(hash); } else { @@ -1304,7 +1312,7 @@ class Blocks { return this.currentBlockHeight; } - public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise { + public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise { let transactions = txs; if (!transactions) { if (config.MEMPOOL.BACKEND === 'esplora') { @@ -1319,14 +1327,19 @@ class Blocks { } } - const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]); + if (transactions?.length != null) { + const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]); - await this.$saveCpfp(hash, height, summary); + await this.$saveCpfp(hash, height, summary); - const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions); - await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats); + const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions); + await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats); - return summary; + return summary; + } else { + logger.err(`Cannot index CPFP for block ${height} - missing transaction data`); + return null; + } } public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise { diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 208c67d70..4ca0e50d1 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -6,6 +6,7 @@ import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { isIP } from 'net'; import transactionUtils from './transaction-utils'; import { isPoint } from '../utils/secp256k1'; +import logger from '../logger'; export class Common { static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ? '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49' @@ -245,7 +246,8 @@ export class Common { } else if (tx.version === 2) { flags |= TransactionFlags.v2; } - const reusedAddresses: { [address: string ]: number } = {}; + const reusedInputAddresses: { [address: string ]: number } = {}; + const reusedOutputAddresses: { [address: string ]: number } = {}; const inValues = {}; const outValues = {}; let rbf = false; @@ -261,6 +263,9 @@ export class Common { case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break; case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break; case 'v1_p2tr': { + if (!vin.witness?.length) { + throw new Error('Taproot input missing witness data'); + } flags |= TransactionFlags.p2tr; // in taproot, if the last witness item begins with 0x50, it's an annex const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50'); @@ -286,7 +291,7 @@ export class Common { } if (vin.prevout?.scriptpubkey_address) { - reusedAddresses[vin.prevout?.scriptpubkey_address] = (reusedAddresses[vin.prevout?.scriptpubkey_address] || 0) + 1; + reusedInputAddresses[vin.prevout?.scriptpubkey_address] = (reusedInputAddresses[vin.prevout?.scriptpubkey_address] || 0) + 1; } inValues[vin.prevout?.value || Math.random()] = (inValues[vin.prevout?.value || Math.random()] || 0) + 1; } @@ -301,7 +306,7 @@ export class Common { case 'p2pk': { flags |= TransactionFlags.p2pk; // detect fake pubkey (i.e. not a valid DER point on the secp256k1 curve) - hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey.slice(2, -2)); + hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey?.slice(2, -2)); } break; case 'multisig': { flags |= TransactionFlags.p2ms; @@ -321,7 +326,7 @@ export class Common { case 'op_return': flags |= TransactionFlags.op_return; break; } if (vout.scriptpubkey_address) { - reusedAddresses[vout.scriptpubkey_address] = (reusedAddresses[vout.scriptpubkey_address] || 0) + 1; + reusedOutputAddresses[vout.scriptpubkey_address] = (reusedOutputAddresses[vout.scriptpubkey_address] || 0) + 1; } outValues[vout.value || Math.random()] = (outValues[vout.value || Math.random()] || 0) + 1; } @@ -331,7 +336,7 @@ export class Common { // fast but bad heuristic to detect possible coinjoins // (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse) - const addressReuse = Object.values(reusedAddresses).reduce((acc, count) => Math.max(acc, count), 0) > 1; + const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1; if (!addressReuse && tx.vin.length >= 5 && tx.vout.length >= 5 && (Object.keys(inValues).length + Object.keys(outValues).length) <= (tx.vin.length + tx.vout.length) / 2 ) { flags |= TransactionFlags.coinjoin; } @@ -348,7 +353,12 @@ export class Common { } static classifyTransaction(tx: TransactionExtended): TransactionClassified { - const flags = Common.getTransactionFlags(tx); + let flags = 0; + try { + flags = Common.getTransactionFlags(tx); + } catch (e) { + logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e)); + } tx.flags = flags; return { ...Common.stripTransaction(tx), diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 162616af6..9a5eb310a 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository'; import { RowDataPacket } from 'mysql2'; class DatabaseMigration { - private static currentVersion = 67; + private static currentVersion = 68; private queryTimeout = 3600_000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -566,6 +566,20 @@ class DatabaseMigration { await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)'); await this.updateToSchemaVersion(67); } + + if (databaseSchemaVersion < 68 && config.MEMPOOL.NETWORK === "liquid") { + await this.$executeQuery('TRUNCATE TABLE elements_pegs'); + await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);'); + await this.$executeQuery(`UPDATE state SET number = 0 WHERE name = 'last_elements_block';`); + // Create the federation_addresses table and add the two Liquid Federation change addresses in + await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses')); + await this.$executeQuery(`INSERT INTO federation_addresses (bitcoinaddress) VALUES ('bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4')`); // Federation change address + await this.$executeQuery(`INSERT INTO federation_addresses (bitcoinaddress) VALUES ('3EiAcrzq1cELXScc98KeCswGWZaPGceT1d')`); // Federation change address + // Create the federation_txos table that uses the federation_addresses table as a foreign key + await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos')); + await this.$executeQuery(`INSERT INTO state VALUES('last_bitcoin_block_audit', 0, NULL);`); + await this.updateToSchemaVersion(68); + } } /** @@ -813,6 +827,32 @@ class DatabaseMigration { ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } + private getCreateFederationAddressesTableQuery(): string { + return `CREATE TABLE IF NOT EXISTS federation_addresses ( + bitcoinaddress varchar(100) NOT NULL, + PRIMARY KEY (bitcoinaddress) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + + private getCreateFederationTxosTableQuery(): string { + return `CREATE TABLE IF NOT EXISTS federation_txos ( + txid varchar(65) NOT NULL, + txindex int(11) NOT NULL, + bitcoinaddress varchar(100) NOT NULL, + amount bigint(20) unsigned NOT NULL, + blocknumber int(11) unsigned NOT NULL, + blocktime int(11) unsigned NOT NULL, + unspent tinyint(1) NOT NULL, + lastblockupdate int(11) unsigned NOT NULL, + lasttimeupdate int(11) unsigned NOT NULL, + pegtxid varchar(65) NOT NULL, + pegindex int(11) NOT NULL, + pegblocktime int(11) unsigned NOT NULL, + PRIMARY KEY (txid, txindex), + FOREIGN KEY (bitcoinaddress) REFERENCES federation_addresses (bitcoinaddress) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + private getCreatePoolsTableQuery(): string { return `CREATE TABLE IF NOT EXISTS pools ( id int(11) NOT NULL AUTO_INCREMENT, diff --git a/backend/src/api/liquid/elements-parser.ts b/backend/src/api/liquid/elements-parser.ts index 12439e037..05f35c085 100644 --- a/backend/src/api/liquid/elements-parser.ts +++ b/backend/src/api/liquid/elements-parser.ts @@ -5,8 +5,12 @@ import { Common } from '../common'; import DB from '../../database'; import logger from '../../logger'; +const federationChangeAddresses = ['bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4', '3EiAcrzq1cELXScc98KeCswGWZaPGceT1d']; +const auditBlockOffsetWithTip = 1; // Wait for 1 block confirmation before processing the block in the audit process to reduce the risk of reorgs + class ElementsParser { private isRunning = false; + private isUtxosUpdatingRunning = false; constructor() { } @@ -32,12 +36,6 @@ class ElementsParser { } } - public async $getPegDataByMonth(): Promise { - const query = `SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y-%m-01') AS date FROM elements_pegs GROUP BY DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y%m')`; - const [rows] = await DB.query(query); - return rows; - } - protected async $parseBlock(block: IBitcoinApi.Block) { for (const tx of block.tx) { await this.$parseInputs(tx, block); @@ -55,29 +53,30 @@ class ElementsParser { protected async $parsePegIn(input: IBitcoinApi.Vin, vindex: number, txid: string, block: IBitcoinApi.Block) { const bitcoinTx: IBitcoinApi.Transaction = await bitcoinSecondClient.getRawTransaction(input.txid, true); + const bitcoinBlock: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(bitcoinTx.blockhash); const prevout = bitcoinTx.vout[input.vout || 0]; const outputAddress = prevout.scriptPubKey.address || (prevout.scriptPubKey.addresses && prevout.scriptPubKey.addresses[0]) || ''; await this.$savePegToDatabase(block.height, block.time, prevout.value * 100000000, txid, vindex, - outputAddress, bitcoinTx.txid, prevout.n, 1); + outputAddress, bitcoinTx.txid, prevout.n, bitcoinBlock.height, bitcoinBlock.time, 1); } protected async $parseOutputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) { for (const output of tx.vout) { if (output.scriptPubKey.pegout_chain) { await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n, - (output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 0); + (output.scriptPubKey.pegout_address || ''), '', 0, 0, 0, 0); } if (!output.scriptPubKey.pegout_chain && output.scriptPubKey.type === 'nulldata' && output.value && output.value > 0 && output.asset && output.asset === Common.nativeAssetId) { await this.$savePegToDatabase(block.height, block.time, 0 - output.value * 100000000, tx.txid, output.n, - (output.scriptPubKey.pegout_addresses && output.scriptPubKey.pegout_addresses[0] || ''), '', 0, 1); + (output.scriptPubKey.pegout_address || ''), '', 0, 0, 0, 1); } } } protected async $savePegToDatabase(height: number, blockTime: number, amount: number, txid: string, - txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, final_tx: number): Promise { - const query = `INSERT INTO elements_pegs( + txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, bitcoinblock: number, bitcoinBlockTime: number, final_tx: number): Promise { + const query = `INSERT IGNORE INTO elements_pegs( block, datetime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`; @@ -85,7 +84,22 @@ class ElementsParser { height, blockTime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx ]; await DB.query(query, params); - logger.debug(`Saved L-BTC peg from block height #${height} with TXID ${txid}.`); + logger.debug(`Saved L-BTC peg from Liquid block height #${height} with TXID ${txid}.`); + + if (amount > 0) { // Peg-in + + // Add the address to the federation addresses table + await DB.query(`INSERT IGNORE INTO federation_addresses (bitcoinaddress) VALUES (?)`, [bitcoinaddress]); + + // Add the UTXO to the federation txos table + const query_utxos = `INSERT IGNORE INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, pegtxid, pegindex, pegblocktime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; + const params_utxos: (string | number)[] = [bitcointxid, bitcoinindex, bitcoinaddress, amount, bitcoinblock, bitcoinBlockTime, 1, bitcoinblock - 1, 0, txid, txindex, blockTime]; + await DB.query(query_utxos, params_utxos); + const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`) + await this.$saveLastBlockAuditToDatabase(minBlockUpdate[0]['lastblockupdate']); + logger.debug(`Saved new Federation UTXO ${bitcointxid}:${bitcoinindex} belonging to ${bitcoinaddress} to federation txos`); + + } } protected async $getLatestBlockHeightFromDatabase(): Promise { @@ -98,6 +112,327 @@ class ElementsParser { const query = `UPDATE state SET number = ? WHERE name = 'last_elements_block'`; await DB.query(query, [blockHeight]); } + + ///////////// FEDERATION AUDIT ////////////// + + public async $updateFederationUtxos() { + if (this.isUtxosUpdatingRunning) { + return; + } + + this.isUtxosUpdatingRunning = true; + + try { + let auditProgress = await this.$getAuditProgress(); + // If no peg in transaction was found in the database, return + if (!auditProgress.lastBlockAudit) { + logger.debug(`No Federation UTXOs found in the database. Waiting for some to be confirmed before starting the Federation UTXOs audit`); + this.isUtxosUpdatingRunning = false; + return; + } + + const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState(); + // If the bitcoin blockchain is not synced yet, return + if (bitcoinBlocksToSync.bitcoinHeaders > bitcoinBlocksToSync.bitcoinBlocks + 1) { + logger.debug(`Bitcoin client is not synced yet. ${bitcoinBlocksToSync.bitcoinHeaders - bitcoinBlocksToSync.bitcoinBlocks} blocks remaining to sync before the Federation audit process can start`); + this.isUtxosUpdatingRunning = false; + return; + } + + auditProgress.lastBlockAudit++; + + // Logging + let indexedThisRun = 0; + let timer = Date.now() / 1000; + const startedAt = Date.now() / 1000; + const indexingSpeeds: number[] = []; + + while (auditProgress.lastBlockAudit <= auditProgress.confirmedTip) { + + // First, get the current UTXOs that need to be scanned in the block + const utxos = await this.$getFederationUtxosToScan(auditProgress.lastBlockAudit); + + // Get the peg-out addresses that need to be scanned + const redeemAddresses = await this.$getRedeemAddressesToScan(); + + // The fast way: check if these UTXOs are still unspent as of the current block with gettxout + let spentAsTip: any[]; + let unspentAsTip: any[]; + if (auditProgress.confirmedTip - auditProgress.lastBlockAudit <= 150) { // If the audit status is not too far in the past, we can use gettxout (fast way) + const utxosToParse = await this.$getFederationUtxosToParse(utxos); + spentAsTip = utxosToParse.spentAsTip; + unspentAsTip = utxosToParse.unspentAsTip; + logger.debug(`Found ${utxos.length} Federation UTXOs and ${redeemAddresses.length} Peg-Out Addresses to scan in Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip}`); + logger.debug(`${unspentAsTip.length} / ${utxos.length} Federation UTXOs are unspent as of tip`); + } else { // If the audit status is too far in the past, it is useless and wasteful to look for still unspent txos since they will all be spent as of the tip + spentAsTip = utxos; + unspentAsTip = []; + + // Logging + const elapsedSeconds = (Date.now() / 1000) - timer; + if (elapsedSeconds > 5) { + const runningFor = (Date.now() / 1000) - startedAt; + const blockPerSeconds = indexedThisRun / elapsedSeconds; + indexingSpeeds.push(blockPerSeconds); + if (indexingSpeeds.length > 100) indexingSpeeds.shift(); // Keep the length of the up to 100 last indexing speeds + const meanIndexingSpeed = indexingSpeeds.reduce((a, b) => a + b, 0) / indexingSpeeds.length; + const eta = (auditProgress.confirmedTip - auditProgress.lastBlockAudit) / meanIndexingSpeed; + logger.debug(`Scanning ${utxos.length} Federation UTXOs and ${redeemAddresses.length} Peg-Out Addresses at Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip} | ~${meanIndexingSpeed.toFixed(2)} blocks/sec | elapsed: ${(runningFor / 60).toFixed(0)} minutes | ETA: ${(eta / 60).toFixed(0)} minutes`); + timer = Date.now() / 1000; + indexedThisRun = 0; + } + } + + // The slow way: parse the block to look for the spending tx + const blockHash: IBitcoinApi.ChainTips = await bitcoinSecondClient.getBlockHash(auditProgress.lastBlockAudit); + const block: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(blockHash, 2); + await this.$parseBitcoinBlock(block, spentAsTip, unspentAsTip, auditProgress.confirmedTip, redeemAddresses); + + // Finally, update the lastblockupdate of the remaining UTXOs and save to the database + const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`) + await this.$saveLastBlockAuditToDatabase(minBlockUpdate[0]['lastblockupdate']); + + auditProgress = await this.$getAuditProgress(); + auditProgress.lastBlockAudit++; + indexedThisRun++; + } + + this.isUtxosUpdatingRunning = false; + } catch (e) { + this.isUtxosUpdatingRunning = false; + throw new Error(e instanceof Error ? e.message : 'Error'); + } + } + + // Get the UTXOs that need to be scanned in block height (UTXOs that were last updated in the block height - 1) + protected async $getFederationUtxosToScan(height: number) { + const query = `SELECT txid, txindex, bitcoinaddress, amount FROM federation_txos WHERE lastblockupdate = ? AND unspent = 1`; + const [rows] = await DB.query(query, [height - 1]); + return rows as any[]; + } + + // Returns the UTXOs that are spent as of tip and need to be scanned + protected async $getFederationUtxosToParse(utxos: any[]): Promise { + const spentAsTip: any[] = []; + const unspentAsTip: any[] = []; + + for (const utxo of utxos) { + const result = await bitcoinSecondClient.getTxOut(utxo.txid, utxo.txindex, false); + result ? unspentAsTip.push(utxo) : spentAsTip.push(utxo); + } + + return {spentAsTip, unspentAsTip}; + } + + protected async $parseBitcoinBlock(block: IBitcoinApi.Block, spentAsTip: any[], unspentAsTip: any[], confirmedTip: number, redeemAddressesData: any[] = []) { + const redeemAddresses: string[] = redeemAddressesData.map(redeemAddress => redeemAddress.bitcoinaddress); + for (const tx of block.tx) { + let mightRedeemInThisTx = false; // If a Federation UTXO is spent in this block, we might find a peg-out address in the outputs... + // Check if the Federation UTXOs that was spent as of tip are spent in this block + for (const input of tx.vin) { + const txo = spentAsTip.find(txo => txo.txid === input.txid && txo.txindex === input.vout); + if (txo) { + mightRedeemInThisTx = true; + await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, lasttimeupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, block.time, txo.txid, txo.txindex]); + // Remove the TXO from the utxo array + spentAsTip.splice(spentAsTip.indexOf(txo), 1); + logger.debug(`Federation UTXO ${txo.txid}:${txo.txindex} (${txo.amount} sats) was spent in block ${block.height}`); + } + } + // Check if an output is sent to a change address of the federation + for (const output of tx.vout) { + if (output.scriptPubKey.address && federationChangeAddresses.includes(output.scriptPubKey.address)) { + // Check that the UTXO was not already added in the DB by previous scans + const [rows_check] = await DB.query(`SELECT txid FROM federation_txos WHERE txid = ? AND txindex = ?`, [tx.txid, output.n]) as any[]; + if (rows_check.length === 0) { + const query_utxos = `INSERT INTO federation_txos (txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, unspent, lastblockupdate, lasttimeupdate, pegtxid, pegindex, pegblocktime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; + const params_utxos: (string | number)[] = [tx.txid, output.n, output.scriptPubKey.address, output.value * 100000000, block.height, block.time, 1, block.height, 0, '', 0, 0]; + await DB.query(query_utxos, params_utxos); + // Add the UTXO to the utxo array + spentAsTip.push({ + txid: tx.txid, + txindex: output.n, + bitcoinaddress: output.scriptPubKey.address, + amount: output.value * 100000000 + }); + logger.debug(`Added new Federation UTXO ${tx.txid}:${output.n} (${output.value * 100000000} sats), change address: ${output.scriptPubKey.address}`); + } + } + if (mightRedeemInThisTx && output.scriptPubKey.address && redeemAddresses.includes(output.scriptPubKey.address)) { + // Find the number of times output.scriptPubKey.address appears in redeemAddresses. There can be address reuse for peg-outs... + const matchingAddress: any[] = redeemAddressesData.filter(redeemAddress => redeemAddress.bitcoinaddress === output.scriptPubKey.address && -redeemAddress.amount === Math.round(output.value * 100000000)); + if (matchingAddress.length > 0) { + if (matchingAddress.length > 1) { + // If there are more than one peg out address with the same amount, we can't know which one redeemed the UTXO: we take the oldest one + matchingAddress.sort((a, b) => a.datetime - b.datetime); + logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${matchingAddress[0].bitcoinaddress}, amount ${matchingAddress[0].amount}, datetime ${matchingAddress[0].datetime}`); + } else { + logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${matchingAddress[0].bitcoinaddress}, amount ${matchingAddress[0].amount}`); + } + const query_add_redeem = `UPDATE elements_pegs SET bitcointxid = ?, bitcoinindex = ? WHERE bitcoinaddress = ? AND amount = ? AND datetime = ?`; + const params_add_redeem: (string | number)[] = [tx.txid, output.n, matchingAddress[0].bitcoinaddress, matchingAddress[0].amount, matchingAddress[0].datetime]; + await DB.query(query_add_redeem, params_add_redeem); + const index = redeemAddressesData.indexOf(matchingAddress[0]); + redeemAddressesData.splice(index, 1); + redeemAddresses.splice(index, 1); + } else { // The output amount does not match the peg-out amount... log it + logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${output.scriptPubKey.address} but output amount ${Math.round(output.value * 100000000)} does not match the peg-out amount!`); + } + } + } + } + + + for (const utxo of spentAsTip) { + await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, utxo.txid, utxo.txindex]); + } + + for (const utxo of unspentAsTip) { + await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [confirmedTip, utxo.txid, utxo.txindex]); + } + } + + protected async $saveLastBlockAuditToDatabase(blockHeight: number) { + const query = `UPDATE state SET number = ? WHERE name = 'last_bitcoin_block_audit'`; + await DB.query(query, [blockHeight]); + } + + // Get the bitcoin block where the audit process was last updated + protected async $getAuditProgress(): Promise { + const lastblockaudit = await this.$getLastBlockAudit(); + const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState(); + return { + lastBlockAudit: lastblockaudit, + confirmedTip: bitcoinBlocksToSync.bitcoinBlocks - auditBlockOffsetWithTip, + }; + } + + // Get the bitcoin blocks remaining to be synced + protected async $getBitcoinBlockchainState(): Promise { + const result = await bitcoinSecondClient.getBlockchainInfo(); + return { + bitcoinBlocks: result.blocks, + bitcoinHeaders: result.headers, + } + } + + protected async $getLastBlockAudit(): Promise { + const query = `SELECT number FROM state WHERE name = 'last_bitcoin_block_audit'`; + const [rows] = await DB.query(query); + return rows[0]['number']; + } + + protected async $getRedeemAddressesToScan(): Promise { + const query = `SELECT datetime, amount, bitcoinaddress FROM elements_pegs where amount < 0 AND bitcoinaddress != '' AND bitcointxid = '';`; + const [rows]: any[] = await DB.query(query); + return rows; + } + + ///////////// DATA QUERY ////////////// + + public async $getAuditStatus(): Promise { + const lastBlockAudit = await this.$getLastBlockAudit(); + const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState(); + return { + bitcoinBlocks: bitcoinBlocksToSync.bitcoinBlocks, + bitcoinHeaders: bitcoinBlocksToSync.bitcoinHeaders, + lastBlockAudit: lastBlockAudit, + isAuditSynced: bitcoinBlocksToSync.bitcoinHeaders - bitcoinBlocksToSync.bitcoinBlocks <= 2 && bitcoinBlocksToSync.bitcoinBlocks - lastBlockAudit <= 3, + }; + } + + public async $getPegDataByMonth(): Promise { + const query = `SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y-%m-01') AS date FROM elements_pegs GROUP BY DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y%m')`; + const [rows] = await DB.query(query); + return rows; + } + + public async $getFederationReservesByMonth(): Promise { + const query = ` + SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(blocktime), '%Y-%m-01') AS date FROM federation_txos + WHERE + (blocktime > UNIX_TIMESTAMP(LAST_DAY(FROM_UNIXTIME(blocktime) - INTERVAL 1 MONTH) + INTERVAL 1 DAY)) + AND + ((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(LAST_DAY(FROM_UNIXTIME(blocktime)) + INTERVAL 1 DAY))) + GROUP BY + date;`; + const [rows] = await DB.query(query); + return rows; + } + + // Get the current L-BTC pegs and the last Liquid block it was updated + public async $getCurrentLbtcSupply(): Promise { + const [rows] = await DB.query(`SELECT SUM(amount) AS LBTC_supply FROM elements_pegs;`); + const lastblockupdate = await this.$getLatestBlockHeightFromDatabase(); + const hash = await bitcoinClient.getBlockHash(lastblockupdate); + return { + amount: rows[0]['LBTC_supply'], + lastBlockUpdate: lastblockupdate, + hash: hash + }; + } + + // Get the current reserves of the federation and the last Bitcoin block it was updated + public async $getCurrentFederationReserves(): Promise { + const [rows] = await DB.query(`SELECT SUM(amount) AS total_balance FROM federation_txos WHERE unspent = 1;`); + const lastblockaudit = await this.$getLastBlockAudit(); + const hash = await bitcoinSecondClient.getBlockHash(lastblockaudit); + return { + amount: rows[0]['total_balance'], + lastBlockUpdate: lastblockaudit, + hash: hash + }; + } + + // Get all of the federation addresses, most balances first + public async $getFederationAddresses(): Promise { + const query = `SELECT bitcoinaddress, SUM(amount) AS balance FROM federation_txos WHERE unspent = 1 GROUP BY bitcoinaddress ORDER BY balance DESC;`; + const [rows] = await DB.query(query); + return rows; + } + + // Get all of the UTXOs held by the federation, most recent first + public async $getFederationUtxos(): Promise { + const query = `SELECT txid, txindex, bitcoinaddress, amount, blocknumber, blocktime, pegtxid, pegindex, pegblocktime FROM federation_txos WHERE unspent = 1 ORDER BY blocktime DESC;`; + const [rows] = await DB.query(query); + return rows; + } + + // Get all of the federation addresses one month ago, most balances first + public async $getFederationAddressesOneMonthAgo(): Promise { + const query = ` + SELECT COUNT(*) AS addresses_count_one_month FROM ( + SELECT bitcoinaddress, SUM(amount) AS balance + FROM federation_txos + WHERE + (blocktime < UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP()))) + AND + ((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP())))) + GROUP BY bitcoinaddress + ) AS result;`; + const [rows] = await DB.query(query); + return rows[0]; + } + + // Get all of the UTXOs held by the federation one month ago, most recent first + public async $getFederationUtxosOneMonthAgo(): Promise { + const query = ` + SELECT COUNT(*) AS utxos_count_one_month FROM federation_txos + WHERE + (blocktime < UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP()))) + AND + ((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP())))) + ORDER BY blocktime DESC;`; + const [rows] = await DB.query(query); + return rows[0]; + } + + // Get recent pegouts from the federation (3 months old) + public async $getRecentPegouts(): Promise { + const query = `SELECT txid, txindex, amount, bitcoinaddress, bitcointxid, bitcoinindex, datetime AS blocktime FROM elements_pegs WHERE amount < 0 AND datetime > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -90, CURRENT_TIMESTAMP())) ORDER BY blocktime;`; + const [rows] = await DB.query(query); + return rows; + } } export default new ElementsParser(); diff --git a/backend/src/api/liquid/liquid.routes.ts b/backend/src/api/liquid/liquid.routes.ts index b130373e1..64d631a05 100644 --- a/backend/src/api/liquid/liquid.routes.ts +++ b/backend/src/api/liquid/liquid.routes.ts @@ -15,7 +15,16 @@ class LiquidRoutes { if (config.DATABASE.ENABLED) { app + .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs', this.$getElementsPegs) .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', this.$getElementsPegsByMonth) + .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves', this.$getFederationReserves) + .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/month', this.$getFederationReservesByMonth) + .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegouts', this.$getPegOuts) + .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses', this.$getFederationAddresses) + .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses/previous-month', this.$getFederationAddressesOneMonthAgo) + .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos', this.$getFederationUtxos) + .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/previous-month', this.$getFederationUtxosOneMonthAgo) + .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/status', this.$getFederationAuditStatus) ; } } @@ -63,11 +72,123 @@ class LiquidRoutes { private async $getElementsPegsByMonth(req: Request, res: Response) { try { const pegs = await elementsParser.$getPegDataByMonth(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.json(pegs); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } } + + private async $getFederationReservesByMonth(req: Request, res: Response) { + try { + const reserves = await elementsParser.$getFederationReservesByMonth(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); + res.json(reserves); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getElementsPegs(req: Request, res: Response) { + try { + const currentSupply = await elementsParser.$getCurrentLbtcSupply(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); + res.json(currentSupply); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getFederationReserves(req: Request, res: Response) { + try { + const currentReserves = await elementsParser.$getCurrentFederationReserves(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); + res.json(currentReserves); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getFederationAuditStatus(req: Request, res: Response) { + try { + const auditStatus = await elementsParser.$getAuditStatus(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); + res.json(auditStatus); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getFederationAddresses(req: Request, res: Response) { + try { + const federationAddresses = await elementsParser.$getFederationAddresses(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); + res.json(federationAddresses); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getFederationAddressesOneMonthAgo(req: Request, res: Response) { + try { + const federationAddresses = await elementsParser.$getFederationAddressesOneMonthAgo(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60 * 24).toUTCString()); + res.json(federationAddresses); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getFederationUtxos(req: Request, res: Response) { + try { + const federationUtxos = await elementsParser.$getFederationUtxos(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); + res.json(federationUtxos); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getFederationUtxosOneMonthAgo(req: Request, res: Response) { + try { + const federationUtxos = await elementsParser.$getFederationUtxosOneMonthAgo(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60 * 24).toUTCString()); + res.json(federationUtxos); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getPegOuts(req: Request, res: Response) { + try { + const recentPegOuts = await elementsParser.$getRecentPegouts(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); + res.json(recentPegOuts); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + } export default new LiquidRoutes(); diff --git a/backend/src/api/mining/mining.ts b/backend/src/api/mining/mining.ts index b23ad04c5..85554db2d 100644 --- a/backend/src/api/mining/mining.ts +++ b/backend/src/api/mining/mining.ts @@ -142,7 +142,7 @@ class Mining { public async $getPoolStat(slug: string): Promise { const pool = await PoolsRepository.$getPool(slug); if (!pool) { - throw new Error('This mining pool does not exist ' + escape(slug)); + throw new Error('This mining pool does not exist'); } const blockCount: number = await BlocksRepository.$blockCount(pool.id); diff --git a/backend/src/index.ts b/backend/src/index.ts index a7b2ad4df..3a8449131 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -266,6 +266,7 @@ class Server { blocks.setNewBlockCallback(async () => { try { await elementsParser.$parse(); + await elementsParser.$updateFederationUtxos(); } catch (e) { logger.warn('Elements parsing error: ' + (e instanceof Error ? e.message : e)); } diff --git a/backend/src/repositories/BlocksAuditsRepository.ts b/backend/src/repositories/BlocksAuditsRepository.ts index c17958d2b..62f28c56f 100644 --- a/backend/src/repositories/BlocksAuditsRepository.ts +++ b/backend/src/repositories/BlocksAuditsRepository.ts @@ -59,7 +59,7 @@ class BlocksAuditRepositories { } } - public async $getBlockAudit(hash: string): Promise { + public async $getBlockAudit(hash: string): Promise { try { const [rows]: any[] = await DB.query( `SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp, @@ -75,8 +75,8 @@ class BlocksAuditRepositories { expected_weight as expectedWeight FROM blocks_audits JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash - WHERE blocks_audits.hash = "${hash}" - `); + WHERE blocks_audits.hash = ? + `, [hash]); if (rows.length) { rows[0].missingTxs = JSON.parse(rows[0].missingTxs); @@ -101,8 +101,8 @@ class BlocksAuditRepositories { const [rows]: any[] = await DB.query( `SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight FROM blocks_audits - WHERE blocks_audits.hash = "${hash}" - `); + WHERE blocks_audits.hash = ? + `, [hash]); return rows[0]; } catch (e: any) { logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e)); diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index a2a084265..e6e92d60f 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -5,7 +5,7 @@ import logger from '../logger'; import { Common } from '../api/common'; import PoolsRepository from './PoolsRepository'; import HashratesRepository from './HashratesRepository'; -import { escape } from 'mysql2'; +import { RowDataPacket, escape } from 'mysql2'; import BlocksSummariesRepository from './BlocksSummariesRepository'; import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository'; import bitcoinClient from '../api/bitcoin/bitcoin-client'; @@ -478,7 +478,7 @@ class BlocksRepository { public async $getBlocksByPool(slug: string, startHeight?: number): Promise { const pool = await PoolsRepository.$getPool(slug); if (!pool) { - throw new Error('This mining pool does not exist ' + escape(slug)); + throw new Error('This mining pool does not exist'); } const params: any[] = []; @@ -802,10 +802,10 @@ class BlocksRepository { /** * Get a list of blocks that have been indexed */ - public async $getIndexedBlocks(): Promise { + public async $getIndexedBlocks(): Promise<{ height: number, hash: string }[]> { try { - const [rows]: any = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`); - return rows; + const [rows] = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`) as RowDataPacket[][]; + return rows as { height: number, hash: string }[]; } catch (e) { logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e)); throw e; @@ -815,7 +815,7 @@ class BlocksRepository { /** * Get a list of blocks that have not had CPFP data indexed */ - public async $getCPFPUnindexedBlocks(): Promise { + public async $getCPFPUnindexedBlocks(): Promise { try { const blockchainInfo = await bitcoinClient.getBlockchainInfo(); const currentBlockHeight = blockchainInfo.blocks; @@ -825,13 +825,13 @@ class BlocksRepository { } const minHeight = Math.max(0, currentBlockHeight - indexingBlockAmount + 1); - const [rows]: any[] = await DB.query(` + const [rows] = await DB.query(` SELECT height FROM compact_cpfp_clusters WHERE height <= ? AND height >= ? GROUP BY height ORDER BY height DESC; - `, [currentBlockHeight, minHeight]); + `, [currentBlockHeight, minHeight]) as RowDataPacket[][]; const indexedHeights = {}; rows.forEach((row) => { indexedHeights[row.height] = true; }); diff --git a/backend/src/repositories/BlocksSummariesRepository.ts b/backend/src/repositories/BlocksSummariesRepository.ts index f85914e31..63ad5ddf2 100644 --- a/backend/src/repositories/BlocksSummariesRepository.ts +++ b/backend/src/repositories/BlocksSummariesRepository.ts @@ -1,3 +1,4 @@ +import { RowDataPacket } from 'mysql2'; import DB from '../database'; import logger from '../logger'; import { BlockSummary, TransactionClassified } from '../mempool.interfaces'; @@ -69,7 +70,7 @@ class BlocksSummariesRepository { public async $getIndexedSummariesId(): Promise { try { - const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`); + const [rows] = await DB.query(`SELECT id from blocks_summaries`) as RowDataPacket[][]; return rows.map(row => row.id); } catch (e) { logger.err(`Cannot get block summaries id list. Reason: ` + (e instanceof Error ? e.message : e)); diff --git a/backend/src/repositories/HashratesRepository.ts b/backend/src/repositories/HashratesRepository.ts index 96cbf6f75..ec44afebe 100644 --- a/backend/src/repositories/HashratesRepository.ts +++ b/backend/src/repositories/HashratesRepository.ts @@ -139,7 +139,7 @@ class HashratesRepository { public async $getPoolWeeklyHashrate(slug: string): Promise { const pool = await PoolsRepository.$getPool(slug); if (!pool) { - throw new Error('This mining pool does not exist ' + escape(slug)); + throw new Error('This mining pool does not exist'); } // Find hashrate boundaries diff --git a/backend/src/utils/secp256k1.ts b/backend/src/utils/secp256k1.ts index cc731f17d..9e0f6dc3b 100644 --- a/backend/src/utils/secp256k1.ts +++ b/backend/src/utils/secp256k1.ts @@ -31,6 +31,9 @@ const curveP = BigInt(`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF * @returns {boolean} true if the point is on the SECP256K1 curve */ export function isPoint(pointHex: string): boolean { + if (!pointHex?.length) { + return false; + } if ( !( // is uncompressed diff --git a/contributors/jamesblacklock.txt b/contributors/jamesblacklock.txt new file mode 100644 index 000000000..11591f451 --- /dev/null +++ b/contributors/jamesblacklock.txt @@ -0,0 +1,3 @@ +I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of December 20, 2023. + +Signed: jamesblacklock diff --git a/contributors/natsee.txt b/contributors/natsoni.txt similarity index 90% rename from contributors/natsee.txt rename to contributors/natsoni.txt index c391ce823..ac1007ecf 100644 --- a/contributors/natsee.txt +++ b/contributors/natsoni.txt @@ -1,3 +1,3 @@ I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 16, 2023. -Signed: natsee +Signed: natsoni diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json index aefb095cf..8f69fd0c1 100644 --- a/docker/backend/mempool-config.json +++ b/docker/backend/mempool-config.json @@ -35,7 +35,7 @@ "ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__, "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", - "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__ + "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__, "MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__ }, "CORE_RPC": { diff --git a/docker/backend/start.sh b/docker/backend/start.sh index ce8f72368..ba9b99233 100755 --- a/docker/backend/start.sh +++ b/docker/backend/start.sh @@ -55,7 +55,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false} # ESPLORA __ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000} -__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"} +__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:=""} __ESPLORA_BATCH_QUERY_BASE_SIZE__=${ESPLORA_BATCH_QUERY_BASE_SIZE:=1000} __ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000} __ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000} diff --git a/frontend/.gitignore b/frontend/.gitignore index 8159e7c7b..d2a765dda 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -6,6 +6,13 @@ /out-tsc server.run.js +# docker +Dockerfile +entrypoint.sh +nginx-mempool.conf +nginx.conf +wait-for + # Only exists if Bazel was run /bazel-out diff --git a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts index 79a77a600..c39dbd253 100644 --- a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts +++ b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts @@ -45,28 +45,30 @@ export class AcceleratorDashboardComponent implements OnInit { this.pendingAccelerations$ = interval(30000).pipe( startWith(true), switchMap(() => { - return this.apiService.getAccelerations$(); - }), - catchError((e) => { - return of([]); + return this.apiService.getAccelerations$().pipe( + catchError(() => { + return of([]); + }), + ); }), share(), ); this.accelerations$ = this.stateService.chainTip$.pipe( distinctUntilChanged(), - switchMap((chainTip) => { - return this.apiService.getAccelerationHistory$({ timeframe: '1m' }); - }), - catchError((e) => { - return of([]); + switchMap(() => { + return this.apiService.getAccelerationHistory$({ timeframe: '1m' }).pipe( + catchError(() => { + return of([]); + }), + ); }), share(), ); this.minedAccelerations$ = this.accelerations$.pipe( map(accelerations => { - return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status)) + return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status)); }) ); diff --git a/frontend/src/app/components/amount/amount.component.html b/frontend/src/app/components/amount/amount.component.html index 29f61ca41..34f9be8ae 100644 --- a/frontend/src/app/components/amount/amount.component.html +++ b/frontend/src/app/components/amount/amount.component.html @@ -19,7 +19,7 @@ ‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }} - L- + L- tL- t sBTC diff --git a/frontend/src/app/components/amount/amount.component.ts b/frontend/src/app/components/amount/amount.component.ts index 479ae4791..9c779265c 100644 --- a/frontend/src/app/components/amount/amount.component.ts +++ b/frontend/src/app/components/amount/amount.component.ts @@ -23,6 +23,7 @@ export class AmountComponent implements OnInit, OnDestroy { @Input() noFiat = false; @Input() addPlus = false; @Input() blockConversion: Price; + @Input() forceBtc: boolean = false; constructor( private stateService: StateService, diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts index b0557ca7c..3f0e9258f 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts @@ -11,6 +11,13 @@ import { MiningService } from '../../services/mining.service'; import { download } from '../../shared/graphs.utils'; import { ActivatedRoute } from '@angular/router'; +interface Hashrate { + timestamp: number; + avgHashRate: number; + share: number; + poolName: string; +} + @Component({ selector: 'app-hashrate-chart-pools', templateUrl: './hashrate-chart-pools.component.html', @@ -32,6 +39,7 @@ export class HashrateChartPoolsComponent implements OnInit { miningWindowPreference: string; radioGroupForm: UntypedFormGroup; + hashrates: Hashrate[]; chartOptions: EChartsOption = {}; chartInitOptions = { renderer: 'svg', @@ -87,56 +95,9 @@ export class HashrateChartPoolsComponent implements OnInit { return this.apiService.getHistoricalPoolsHashrate$(timespan) .pipe( tap((response) => { - const hashrates = response.body; + this.hashrates = response.body; // Prepare series (group all hashrates data point by pool) - const grouped = {}; - for (const hashrate of hashrates) { - if (!grouped.hasOwnProperty(hashrate.poolName)) { - grouped[hashrate.poolName] = []; - } - grouped[hashrate.poolName].push(hashrate); - } - - const series = []; - const legends = []; - for (const name in grouped) { - series.push({ - zlevel: 0, - stack: 'Total', - name: name, - showSymbol: false, - symbol: 'none', - data: grouped[name].map((val) => [val.timestamp * 1000, val.share * 100]), - type: 'line', - lineStyle: { width: 0 }, - areaStyle: { opacity: 1 }, - smooth: true, - color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()], - emphasis: { - disabled: true, - scale: false, - }, - }); - - legends.push({ - name: name, - inactiveColor: 'rgb(110, 112, 121)', - textStyle: { - color: 'white', - }, - icon: 'roundRect', - itemStyle: { - color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()], - }, - }); - } - - this.prepareChartOptions({ - legends: legends, - series: series, - }); - this.isLoading = false; - + const series = this.applyHashrates(); if (series.length === 0) { this.cd.markForCheck(); throw new Error(); @@ -156,6 +117,77 @@ export class HashrateChartPoolsComponent implements OnInit { ); } + applyHashrates(): any[] { + const times: { [time: number]: { hashrates: { [pool: string]: Hashrate } } } = {}; + const pools = {}; + for (const hashrate of this.hashrates) { + if (!times[hashrate.timestamp]) { + times[hashrate.timestamp] = { hashrates: {} }; + } + times[hashrate.timestamp].hashrates[hashrate.poolName] = hashrate; + if (!pools[hashrate.poolName]) { + pools[hashrate.poolName] = true; + } + } + + const sortedTimes = Object.keys(times).sort((a,b) => parseInt(a) - parseInt(b)).map(time => ({ time: parseInt(time), hashrates: times[time].hashrates })); + const lastHashrates = sortedTimes[sortedTimes.length - 1].hashrates; + const sortedPools = Object.keys(pools).sort((a,b) => { + if (lastHashrates[b]?.share ?? lastHashrates[a]?.share ?? false) { + // sort by descending share of hashrate in latest period + return (lastHashrates[b]?.share || 0) - (lastHashrates[a]?.share || 0); + } else { + // tiebreak by pool name + b < a; + } + }); + + const series = []; + const legends = []; + for (const name of sortedPools) { + const data = sortedTimes.map(({ time, hashrates }) => { + return [time * 1000, (hashrates[name]?.share || 0) * 100]; + }); + series.push({ + zlevel: 0, + stack: 'Total', + name: name, + showSymbol: false, + symbol: 'none', + data, + type: 'line', + lineStyle: { width: 0 }, + areaStyle: { opacity: 1 }, + smooth: true, + color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()], + emphasis: { + disabled: true, + scale: false, + }, + }); + + legends.push({ + name: name, + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + itemStyle: { + color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()], + }, + }); + } + + this.prepareChartOptions({ + legends: legends, + series: series, + }); + this.isLoading = false; + + return series; + } + prepareChartOptions(data) { let title: object; if (data.series.length === 0) { @@ -256,6 +288,7 @@ export class HashrateChartPoolsComponent implements OnInit { }, }], }; + this.cd.markForCheck(); } onChartInit(ec) { diff --git a/frontend/src/app/components/lbtc-pegs-graph/lbtc-pegs-graph.component.ts b/frontend/src/app/components/lbtc-pegs-graph/lbtc-pegs-graph.component.ts index c4e8cbf91..0f6f115ff 100644 --- a/frontend/src/app/components/lbtc-pegs-graph/lbtc-pegs-graph.component.ts +++ b/frontend/src/app/components/lbtc-pegs-graph/lbtc-pegs-graph.component.ts @@ -27,7 +27,6 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges { template: ('widget' | 'advanced') = 'widget'; isLoading = true; - pegsChartOption: EChartsOption = {}; pegsChartInitOption = { renderer: 'svg' }; @@ -41,20 +40,24 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges { } ngOnChanges() { - if (!this.data) { + if (!this.data?.liquidPegs) { return; } - this.pegsChartOptions = this.createChartOptions(this.data.series, this.data.labels); + if (!this.data.liquidReserves) { + this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels); + } else { + this.pegsChartOptions = this.createChartOptions(this.data.liquidPegs.series, this.data.liquidPegs.labels, this.data.liquidReserves.series); + } } rendered() { - if (!this.data) { + if (!this.data.liquidPegs) { return; } this.isLoading = false; } - createChartOptions(series: number[], labels: string[]): EChartsOption { + createChartOptions(pegSeries: number[], labels: string[], reservesSeries?: number[],): EChartsOption { return { grid: { height: this.height, @@ -99,17 +102,18 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges { type: 'line', }, formatter: (params: any) => { - const colorSpan = (color: string) => ``; + const colorSpan = (color: string) => ``; let itemFormatted = '
' + params[0].axisValue + '
'; - params.map((item: any, index: number) => { + for (let index = params.length - 1; index >= 0; index--) { + const item = params[index]; if (index < 26) { itemFormatted += `
${colorSpan(item.color)}
-
-
${formatNumber(item.value, this.locale, '1.2-2')} L-BTC
+
+
${formatNumber(item.value, this.locale, '1.2-2')} ${item.seriesName}
`; } - }); + } return `
${itemFormatted}
`; } }, @@ -138,20 +142,34 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges { }, series: [ { - data: series, + data: pegSeries, + name: 'L-BTC', + color: '#116761', type: 'line', stack: 'total', - smooth: false, + smooth: true, showSymbol: false, areaStyle: { opacity: 0.2, color: '#116761', }, lineStyle: { - width: 3, + width: 2, color: '#116761', }, }, + { + data: reservesSeries, + name: 'BTC', + color: '#EA983B', + type: 'line', + smooth: true, + showSymbol: false, + lineStyle: { + width: 2, + color: '#EA983B', + }, + }, ], }; } diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html index 49f05c3a2..760cadda4 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html @@ -78,6 +78,9 @@ + diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.html b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.html new file mode 100644 index 000000000..0a37b1d13 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.html @@ -0,0 +1,72 @@ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AddressBalance
+ + + + + +
+ + + + + +
+ + + +
+ + + +
+ + + + + +
+
+
+
+ +
diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.scss b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.scss new file mode 100644 index 000000000..fb0232064 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.scss @@ -0,0 +1,45 @@ +.spinner-border { + height: 25px; + width: 25px; + margin-top: 13px; +} + +tr, td, th { + border: 0px; + padding-top: 0.65rem !important; + padding-bottom: 0.6rem !important; + padding-right: 2rem !important; + .widget { + padding-right: 1rem !important; + } +} + +.clear-link { + color: white; +} + +.disabled { + pointer-events: none; + opacity: 0.5; +} + +.progress { + background-color: #2d3348; +} + +.address { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; +} +.address.widget { + width: 60%; +} + +.amount { + width: 25%; +} +.amount.widget { + width: 40%; +} diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.ts b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.ts new file mode 100644 index 000000000..caeac1987 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component.ts @@ -0,0 +1,109 @@ +import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; +import { Observable, Subject, combineLatest, of, timer } from 'rxjs'; +import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; +import { ApiService } from '../../../services/api.service'; +import { Env, StateService } from '../../../services/state.service'; +import { AuditStatus, CurrentPegs, FederationAddress } from '../../../interfaces/node-api.interface'; +import { WebsocketService } from '../../../services/websocket.service'; + +@Component({ + selector: 'app-federation-addresses-list', + templateUrl: './federation-addresses-list.component.html', + styleUrls: ['./federation-addresses-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FederationAddressesListComponent implements OnInit { + @Input() widget: boolean = false; + @Input() federationAddresses$: Observable; + + env: Env; + isLoading = true; + page = 1; + pageSize = 15; + maxSize = window.innerWidth <= 767.98 ? 3 : 5; + skeletonLines: number[] = []; + auditStatus$: Observable; + auditUpdated$: Observable; + lastReservesBlockUpdate: number = 0; + currentPeg$: Observable; + lastPegBlockUpdate: number = 0; + lastPegAmount: string = ''; + isLoad: boolean = true; + + private destroy$ = new Subject(); + + constructor( + private apiService: ApiService, + public stateService: StateService, + private websocketService: WebsocketService + ) { + } + + ngOnInit(): void { + this.isLoading = !this.widget; + this.env = this.stateService.env; + this.skeletonLines = this.widget === true ? [...Array(5).keys()] : [...Array(15).keys()]; + if (!this.widget) { + this.websocketService.want(['blocks']); + this.auditStatus$ = this.stateService.blocks$.pipe( + takeUntil(this.destroy$), + throttleTime(40000), + delayWhen(_ => this.isLoad ? timer(0) : timer(2000)), + tap(() => this.isLoad = false), + switchMap(() => this.apiService.federationAuditSynced$()), + shareReplay(1) + ); + + this.currentPeg$ = this.auditStatus$.pipe( + filter(auditStatus => auditStatus.isAuditSynced === true), + switchMap(_ => + this.apiService.liquidPegs$().pipe( + filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate), + tap((currentPegs) => { + this.lastPegBlockUpdate = currentPegs.lastBlockUpdate; + }) + ) + ), + share() + ); + + this.auditUpdated$ = combineLatest([ + this.auditStatus$, + this.currentPeg$ + ]).pipe( + filter(([auditStatus, _]) => auditStatus.isAuditSynced === true), + map(([auditStatus, currentPeg]) => ({ + lastBlockAudit: auditStatus.lastBlockAudit, + currentPegAmount: currentPeg.amount + })), + switchMap(({ lastBlockAudit, currentPegAmount }) => { + const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate; + const amountCheck = currentPegAmount !== this.lastPegAmount; + this.lastReservesBlockUpdate = lastBlockAudit; + this.lastPegAmount = currentPegAmount; + return of(blockAuditCheck || amountCheck); + }), + share() + ); + + this.federationAddresses$ = this.auditUpdated$.pipe( + filter(auditUpdated => auditUpdated === true), + throttleTime(40000), + switchMap(_ => this.apiService.federationAddresses$()), + tap(_ => this.isLoading = false), + share() + ); + } + + } + + ngOnDestroy(): void { + this.destroy$.next(1); + this.destroy$.complete(); + } + + pageChange(page: number): void { + this.page = page; + } + +} diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component.html b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component.html new file mode 100644 index 000000000..dace541b7 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component.html @@ -0,0 +1,34 @@ +
+ +
+
+ +
Liquid Federation Wallet 
+
+
+
{{ federationAddresses.length }} addresses
+ + + +
+
+
+
+ + + + + + +
+
diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component.scss b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component.scss new file mode 100644 index 000000000..f7c2f104c --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component.scss @@ -0,0 +1,75 @@ +.fee-estimation-container { + display: flex; + justify-content: space-between; + @media (min-width: 376px) { + flex-direction: row; + } + .item { + max-width: 300px; + margin: 0; + width: -webkit-fill-available; + @media (min-width: 376px) { + margin: 0 auto 0px; + } + + .card-title { + margin: 0; + color: #4a68b9; + font-size: 10px; + font-size: 1rem; + white-space: nowrap; + } + + .card-text { + font-size: 22px; + span { + font-size: 11px; + position: relative; + top: -2px; + } + } + + .card-text span { + color: #ffffff66; + font-size: 12px; + top: 0px; + } + .fee-text{ + border-bottom: 1px solid #ffffff1c; + width: fit-content; + margin: auto; + line-height: 1.45; + padding: 0px 2px; + } + .fiat { + display: block; + font-size: 14px !important; + } + } +} + +.loading-container{ + min-height: 76px; +} + +.card-text { + .skeleton-loader { + width: 100%; + display: block; + &:first-child { + max-width: 90px; + margin: 15px auto 3px; + } + &:last-child { + margin: 10px auto 3px; + max-width: 55px; + } + } +} + +.title-link, .title-link:hover, .title-link:focus, .title-link:active { + display: block; + margin-bottom: 4px; + text-decoration: none; + color: inherit; +} diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component.ts b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component.ts new file mode 100644 index 000000000..081b22a4f --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { FederationAddress } from '../../../interfaces/node-api.interface'; + +@Component({ + selector: 'app-federation-addresses-stats', + templateUrl: './federation-addresses-stats.component.html', + styleUrls: ['./federation-addresses-stats.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FederationAddressesStatsComponent implements OnInit { + @Input() federationAddresses$: Observable; + @Input() federationAddressesOneMonthAgo$: Observable; + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html b/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html new file mode 100644 index 000000000..ea52cd8d7 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html @@ -0,0 +1,109 @@ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OutputAddressAmountRelated Peg-InDate
+ + + + + + + +
+ + + + + + + + + + + + + + + + + Change output + + + ‎{{ utxo.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }} +
()
+
+ + + + + +
+ + + + + + + + + +
+ + + + + +
+
+
+
+ +
diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.scss b/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.scss new file mode 100644 index 000000000..617edc869 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.scss @@ -0,0 +1,94 @@ +.spinner-border { + height: 25px; + width: 25px; + margin-top: 13px; +} + +tr, td, th { + border: 0px; + padding-top: 0.65rem !important; + padding-bottom: 0.6rem !important; + padding-right: 2rem !important; + .widget { + padding-right: 1rem !important; + } +} + +.clear-link { + color: white; +} + +.disabled { + pointer-events: none; + opacity: 0.5; +} + +.progress { + background-color: #2d3348; +} + +.txid { + width: 25%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; +} +.txid.widget { + width: 40%; + +} + +.address { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; + @media (max-width: 527px) { + display: none; + } +} + +.amount { + width: 12%; +} +.amount.widget { + width: 30%; +} + +.pegin { + width: 25%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; + @media (max-width: 872px) { + display: none; + } +} + +.timestamp { + width: 18%; + @media (max-width: 800px) { + display: none; + } + @media (max-width: 1000px) { + .relative-time { + display: none; + } + } +} +.timestamp.widget { + width: 100%; + @media (min-width: 768px) AND (max-width: 1050px) { + display: none; + } + @media (max-width: 767px) { + display: block; + } + + @media (max-width: 500px) { + display: none; + } +} + diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.ts b/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.ts new file mode 100644 index 000000000..30f401abf --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.ts @@ -0,0 +1,109 @@ +import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; +import { Observable, Subject, combineLatest, of, timer } from 'rxjs'; +import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; +import { ApiService } from '../../../services/api.service'; +import { Env, StateService } from '../../../services/state.service'; +import { AuditStatus, CurrentPegs, FederationUtxo } from '../../../interfaces/node-api.interface'; +import { WebsocketService } from '../../../services/websocket.service'; + +@Component({ + selector: 'app-federation-utxos-list', + templateUrl: './federation-utxos-list.component.html', + styleUrls: ['./federation-utxos-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FederationUtxosListComponent implements OnInit { + @Input() widget: boolean = false; + @Input() federationUtxos$: Observable; + + env: Env; + isLoading = true; + page = 1; + pageSize = 15; + maxSize = window.innerWidth <= 767.98 ? 3 : 5; + skeletonLines: number[] = []; + auditStatus$: Observable; + auditUpdated$: Observable; + lastReservesBlockUpdate: number = 0; + currentPeg$: Observable; + lastPegBlockUpdate: number = 0; + lastPegAmount: string = ''; + isLoad: boolean = true; + + private destroy$ = new Subject(); + + constructor( + private apiService: ApiService, + public stateService: StateService, + private websocketService: WebsocketService, + ) { + } + + ngOnInit(): void { + this.isLoading = !this.widget; + this.env = this.stateService.env; + this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; + + if (!this.widget) { + this.websocketService.want(['blocks']); + this.auditStatus$ = this.stateService.blocks$.pipe( + takeUntil(this.destroy$), + throttleTime(40000), + delayWhen(_ => this.isLoad ? timer(0) : timer(2000)), + tap(() => this.isLoad = false), + switchMap(() => this.apiService.federationAuditSynced$()), + shareReplay(1) + ); + + this.currentPeg$ = this.auditStatus$.pipe( + filter(auditStatus => auditStatus.isAuditSynced === true), + switchMap(_ => + this.apiService.liquidPegs$().pipe( + filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate), + tap((currentPegs) => { + this.lastPegBlockUpdate = currentPegs.lastBlockUpdate; + }) + ) + ), + share() + ); + + this.auditUpdated$ = combineLatest([ + this.auditStatus$, + this.currentPeg$ + ]).pipe( + filter(([auditStatus, _]) => auditStatus.isAuditSynced === true), + map(([auditStatus, currentPeg]) => ({ + lastBlockAudit: auditStatus.lastBlockAudit, + currentPegAmount: currentPeg.amount + })), + switchMap(({ lastBlockAudit, currentPegAmount }) => { + const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate; + const amountCheck = currentPegAmount !== this.lastPegAmount; + this.lastReservesBlockUpdate = lastBlockAudit; + this.lastPegAmount = currentPegAmount; + return of(blockAuditCheck || amountCheck); + }), + share() + ); + + this.federationUtxos$ = this.auditUpdated$.pipe( + filter(auditUpdated => auditUpdated === true), + throttleTime(40000), + switchMap(_ => this.apiService.federationUtxos$()), + tap(_ => this.isLoading = false), + share() + ); + } + } + + ngOnDestroy(): void { + this.destroy$.next(1); + this.destroy$.complete(); + } + + pageChange(page: number): void { + this.page = page; + } + +} diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.html b/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.html new file mode 100644 index 000000000..1bb397533 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.html @@ -0,0 +1,24 @@ +
+
+

Liquid Federation Wallet

+
+ + + +
+ + + +
+ +
diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.scss b/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.scss new file mode 100644 index 000000000..4b07e45e3 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.scss @@ -0,0 +1,13 @@ +ul { + margin-bottom: 20px; +} + +@media (max-width: 767.98px) { + .nav-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: auto; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.ts b/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.ts new file mode 100644 index 000000000..cbf931e28 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.ts @@ -0,0 +1,20 @@ +import { Component, OnInit } from '@angular/core'; +import { SeoService } from '../../../services/seo.service'; + +@Component({ + selector: 'app-federation-wallet', + templateUrl: './federation-wallet.component.html', + styleUrls: ['./federation-wallet.component.scss'] +}) +export class FederationWalletComponent implements OnInit { + + constructor( + private seoService: SeoService + ) { + this.seoService.setTitle($localize`:@@993e5bc509c26db81d93018e24a6afe6e50cae52:Liquid Federation Wallet`); + } + + ngOnInit(): void { + } + +} diff --git a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html new file mode 100644 index 000000000..d562328a5 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html @@ -0,0 +1,139 @@ +
+
+ +
+

Recent Peg-In / Out's

+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TransactionDateAmountFund / Redemption TxBTC Address
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + ‎{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }} +
()
+
+ + + + + + + + + + Peg out in progress... + + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+ + + + + +
+
+
+
+ +
+
+ +
+ + + - + \ No newline at end of file diff --git a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.scss b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.scss new file mode 100644 index 000000000..92f5bc64f --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.scss @@ -0,0 +1,107 @@ +.spinner-border { + height: 25px; + width: 25px; + margin-top: 13px; +} + +tr, td, th { + border: 0px; + padding-top: 0.65rem !important; + padding-bottom: 0.6rem !important; + padding-right: 2rem !important; + .widget { + padding-right: 1rem !important; + } +} + +.clear-link { + color: white; +} + +.disabled { + pointer-events: none; + opacity: 0.5; +} + +.progress { + background-color: #2d3348; +} + +.transaction { + width: 20%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 120px; +} +.transaction.widget { + width: 100%; + +} + +.address { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; + @media (max-width: 527px) { + display: none; + } +} + +.amount { + width: 0%; +} + +.output { + width: 20%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; + @media (max-width: 800px) { + display: none; + } +} + +.address { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; + @media (max-width: 960px) { + display: none; + } +} + +.timestamp { + width: 0%; + @media (max-width: 650px) { + display: none; + } + @media (max-width: 1000px) { + .relative-time { + display: none; + } + } +} +.timestamp.widget { + @media (min-width: 768px) AND (max-width: 1050px) { + display: none; + } + @media (max-width: 767px) { + display: block; + } + + @media (max-width: 500px) { + display: none; + } +} + +.credit { + color: #7CB342; +} + +.debit { + color: #D81B60; +} diff --git a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts new file mode 100644 index 000000000..e921e1250 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts @@ -0,0 +1,154 @@ +import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; +import { Observable, Subject, combineLatest, of, timer } from 'rxjs'; +import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; +import { ApiService } from '../../../services/api.service'; +import { Env, StateService } from '../../../services/state.service'; +import { AuditStatus, CurrentPegs, FederationUtxo, RecentPeg } from '../../../interfaces/node-api.interface'; +import { WebsocketService } from '../../../services/websocket.service'; +import { SeoService } from '../../../services/seo.service'; + +@Component({ + selector: 'app-recent-pegs-list', + templateUrl: './recent-pegs-list.component.html', + styleUrls: ['./recent-pegs-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RecentPegsListComponent implements OnInit { + @Input() widget: boolean = false; + @Input() recentPegIns$: Observable = of([]); + @Input() recentPegOuts$: Observable = of([]); + + env: Env; + isLoading = true; + page = 1; + pageSize = 15; + maxSize = window.innerWidth <= 767.98 ? 3 : 5; + skeletonLines: number[] = []; + auditStatus$: Observable; + auditUpdated$: Observable; + federationUtxos$: Observable; + recentPegs$: Observable; + lastReservesBlockUpdate: number = 0; + currentPeg$: Observable; + lastPegBlockUpdate: number = 0; + lastPegAmount: string = ''; + isLoad: boolean = true; + + private destroy$ = new Subject(); + + constructor( + private apiService: ApiService, + public stateService: StateService, + private websocketService: WebsocketService, + private seoService: SeoService + ) { + } + + ngOnInit(): void { + this.isLoading = !this.widget; + this.env = this.stateService.env; + this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; + + if (!this.widget) { + this.seoService.setTitle($localize`:@@a8b0889ea1b41888f1e247f2731cc9322198ca04:Recent Peg-In / Out's`); + this.websocketService.want(['blocks']); + this.auditStatus$ = this.stateService.blocks$.pipe( + takeUntil(this.destroy$), + throttleTime(40000), + delayWhen(_ => this.isLoad ? timer(0) : timer(2000)), + tap(() => this.isLoad = false), + switchMap(() => this.apiService.federationAuditSynced$()), + shareReplay(1) + ); + + this.currentPeg$ = this.auditStatus$.pipe( + filter(auditStatus => auditStatus.isAuditSynced === true), + switchMap(_ => + this.apiService.liquidPegs$().pipe( + filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate), + tap((currentPegs) => { + this.lastPegBlockUpdate = currentPegs.lastBlockUpdate; + }) + ) + ), + share() + ); + + this.auditUpdated$ = combineLatest([ + this.auditStatus$, + this.currentPeg$ + ]).pipe( + filter(([auditStatus, _]) => auditStatus.isAuditSynced === true), + map(([auditStatus, currentPeg]) => ({ + lastBlockAudit: auditStatus.lastBlockAudit, + currentPegAmount: currentPeg.amount + })), + switchMap(({ lastBlockAudit, currentPegAmount }) => { + const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate; + const amountCheck = currentPegAmount !== this.lastPegAmount; + this.lastReservesBlockUpdate = lastBlockAudit; + this.lastPegAmount = currentPegAmount; + return of(blockAuditCheck || amountCheck); + }), + share() + ); + + this.federationUtxos$ = this.auditUpdated$.pipe( + filter(auditUpdated => auditUpdated === true), + throttleTime(40000), + switchMap(_ => this.apiService.federationUtxos$()), + share() + ); + + this.recentPegIns$ = this.federationUtxos$.pipe( + map(federationUtxos => federationUtxos.filter(utxo => utxo.pegtxid).map(utxo => { + return { + txid: utxo.pegtxid, + txindex: utxo.pegindex, + amount: utxo.amount, + bitcoinaddress: utxo.bitcoinaddress, + bitcointxid: utxo.txid, + bitcoinindex: utxo.txindex, + blocktime: utxo.pegblocktime, + } + })), + share() + ); + + this.recentPegOuts$ = this.auditUpdated$.pipe( + filter(auditUpdated => auditUpdated === true), + throttleTime(40000), + switchMap(_ => this.apiService.recentPegOuts$()), + share() + ); + + } + + this.recentPegs$ = combineLatest([ + this.recentPegIns$, + this.recentPegOuts$ + ]).pipe( + map(([recentPegIns, recentPegOuts]) => { + return [ + ...recentPegIns, + ...recentPegOuts + ].sort((a, b) => { + return b.blocktime - a.blocktime; + }); + }), + filter(recentPegs => recentPegs.length > 0), + tap(_ => this.isLoading = false), + share() + ); + } + + ngOnDestroy(): void { + this.destroy$.next(1); + this.destroy$.complete(); + } + + pageChange(page: number): void { + this.page = page; + } + +} diff --git a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.html b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.html new file mode 100644 index 000000000..fca78b881 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.html @@ -0,0 +1,7 @@ + diff --git a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.scss b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.scss new file mode 100644 index 000000000..0534c9b5d --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.scss @@ -0,0 +1,71 @@ +.fee-estimation-container { + display: flex; + justify-content: space-between; + padding-bottom: 1rem; + @media (min-width: 376px) { + flex-direction: row; + } + .item { + max-width: 300px; + margin: 0; + width: -webkit-fill-available; + @media (min-width: 376px) { + margin: 0 auto 0px; + } + + .card-title { + margin: 0; + color: #4a68b9; + font-size: 10px; + font-size: 1rem; + white-space: nowrap; + } + + .card-text { + font-size: 22px; + span { + font-size: 11px; + position: relative; + top: -2px; + } + } + + .card-text span { + color: #ffffff66; + font-size: 12px; + top: 0px; + } + .fee-text{ + width: fit-content; + margin: auto; + line-height: 1.45; + padding: 0px 2px; + } + .fiat { + display: block; + font-size: 14px !important; + } + } +} + +.card-text { + .skeleton-loader { + width: 100%; + display: block; + &:first-child { + max-width: 90px; + margin: 15px auto 3px; + } + &:last-child { + margin: 10px auto 3px; + max-width: 55px; + } + } +} + +.title-link, .title-link:hover, .title-link:focus, .title-link:active { + display: block; + margin-bottom: 4px; + text-decoration: none; + color: inherit; +} diff --git a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.ts b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.ts new file mode 100644 index 000000000..3fbebf715 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +@Component({ + selector: 'app-recent-pegs-stats', + templateUrl: './recent-pegs-stats.component.html', + styleUrls: ['./recent-pegs-stats.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RecentPegsStatsComponent implements OnInit { + constructor() { } + + ngOnInit(): void { + + } + +} diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.html b/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.html new file mode 100644 index 000000000..e9f6b4ccd --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.html @@ -0,0 +1,98 @@ +
+ +
+ +
+
+
+ + +
+
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+
+ + + +
+ +
+ +
+
+
+ + +
+
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+
+
+ + + +
+ Audit in progress: Bitcoin block height #{{ auditStatus.lastBlockAudit }} / #{{ auditStatus.bitcoinHeaders }} +
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.scss b/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.scss new file mode 100644 index 000000000..1116f8d85 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.scss @@ -0,0 +1,138 @@ +.dashboard-container { + text-align: center; + margin-top: 0.5rem; + .col { + margin-bottom: 1.5rem; + } +} + +.card { + background-color: #1d1f31; +} + +.card-title { + padding-top: 20px; +} + +.card-body.pool-ranking { + padding: 1.25rem 0.25rem 0.75rem 0.25rem; +} +.card-text { + font-size: 22px; +} + +#blockchain-container { + position: relative; + overflow-x: scroll; + overflow-y: hidden; + scrollbar-width: none; + -ms-overflow-style: none; +} + +#blockchain-container::-webkit-scrollbar { + display: none; +} + +.fade-border { + -webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%) +} + +.in-progress-message { + position: relative; + color: #ffffff91; + margin-top: 20px; + text-align: center; + padding-bottom: 3px; + font-weight: 500; +} + +.more-padding { + padding: 24px 20px !important; +} + +.card-wrapper { + .card { + height: auto !important; + } + .card-body { + display: flex; + flex: inherit; + text-align: center; + flex-direction: column; + justify-content: space-around; + padding: 22px 20px; + } +} + +.skeleton-loader { + width: 100%; + display: block; + &:first-child { + max-width: 90px; + margin: 15px auto 3px; + } + &:last-child { + margin: 10px auto 3px; + max-width: 55px; + } +} + +.card-text { + font-size: 22px; +} + +.title-link, .title-link:hover, .title-link:focus, .title-link:active { + display: block; + margin-bottom: 10px; + text-decoration: none; + color: inherit; +} + +.lastest-blocks-table { + width: 100%; + text-align: left; + tr, td, th { + border: 0px; + padding-top: 0.65rem !important; + padding-bottom: 0.8rem !important; + } + .table-cell-height { + width: 25%; + } + .table-cell-fee { + width: 25%; + text-align: right; + } + .table-cell-pool { + text-align: left; + width: 30%; + + @media (max-width: 875px) { + display: none; + } + + .pool-name { + margin-left: 1em; + } + } + .table-cell-acceleration-count { + text-align: right; + width: 20%; + } +} + +.card { + height: 385px; +} +.list-card { + height: 410px; + @media (max-width: 767px) { + height: auto; + } +} + +.mempool-block-wrapper { + max-height: 380px; + max-width: 380px; + margin: auto; +} \ No newline at end of file diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.ts b/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.ts new file mode 100644 index 000000000..9b23eb7cb --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.ts @@ -0,0 +1,204 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { SeoService } from '../../../services/seo.service'; +import { WebsocketService } from '../../../services/websocket.service'; +import { StateService } from '../../../services/state.service'; +import { Observable, Subject, combineLatest, delayWhen, filter, interval, map, of, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime, timer } from 'rxjs'; +import { ApiService } from '../../../services/api.service'; +import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo, RecentPeg } from '../../../interfaces/node-api.interface'; + +@Component({ + selector: 'app-reserves-audit-dashboard', + templateUrl: './reserves-audit-dashboard.component.html', + styleUrls: ['./reserves-audit-dashboard.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReservesAuditDashboardComponent implements OnInit { + auditStatus$: Observable; + auditUpdated$: Observable; + currentPeg$: Observable; + currentReserves$: Observable; + federationUtxos$: Observable; + recentPegIns$: Observable; + recentPegOuts$: Observable; + federationAddresses$: Observable; + federationAddressesOneMonthAgo$: Observable; + liquidPegsMonth$: Observable; + liquidReservesMonth$: Observable; + fullHistory$: Observable; + isLoad: boolean = true; + private lastPegBlockUpdate: number = 0; + private lastPegAmount: string = ''; + private lastReservesBlockUpdate: number = 0; + + private destroy$ = new Subject(); + + constructor( + private seoService: SeoService, + private websocketService: WebsocketService, + private apiService: ApiService, + private stateService: StateService, + ) { + this.seoService.setTitle($localize`:@@liquid.reserves-audit:Reserves Audit Dashboard`); + } + + ngOnInit(): void { + this.websocketService.want(['blocks', 'mempool-blocks']); + + this.auditStatus$ = this.stateService.blocks$.pipe( + takeUntil(this.destroy$), + throttleTime(40000), + delayWhen(_ => this.isLoad ? timer(0) : timer(2000)), + tap(() => this.isLoad = false), + switchMap(() => this.apiService.federationAuditSynced$()), + shareReplay(1), + ); + + this.currentPeg$ = this.auditStatus$.pipe( + filter(auditStatus => auditStatus.isAuditSynced === true), + switchMap(_ => + this.apiService.liquidPegs$().pipe( + filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate), + tap((currentPegs) => { + this.lastPegBlockUpdate = currentPegs.lastBlockUpdate; + }) + ) + ), + share() + ); + + this.auditUpdated$ = combineLatest([ + this.auditStatus$, + this.currentPeg$ + ]).pipe( + filter(([auditStatus, _]) => auditStatus.isAuditSynced === true), + map(([auditStatus, currentPeg]) => ({ + lastBlockAudit: auditStatus.lastBlockAudit, + currentPegAmount: currentPeg.amount + })), + switchMap(({ lastBlockAudit, currentPegAmount }) => { + const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate; + const amountCheck = currentPegAmount !== this.lastPegAmount; + this.lastPegAmount = currentPegAmount; + return of(blockAuditCheck || amountCheck); + }), + share() + ); + + this.currentReserves$ = this.auditUpdated$.pipe( + filter(auditUpdated => auditUpdated === true), + throttleTime(40000), + switchMap(_ => + this.apiService.liquidReserves$().pipe( + filter((currentReserves) => currentReserves.lastBlockUpdate >= this.lastReservesBlockUpdate), + tap((currentReserves) => { + this.lastReservesBlockUpdate = currentReserves.lastBlockUpdate; + }) + ) + ), + share() + ); + + this.federationUtxos$ = this.auditUpdated$.pipe( + filter(auditUpdated => auditUpdated === true), + throttleTime(40000), + switchMap(_ => this.apiService.federationUtxos$()), + share() + ); + + this.recentPegIns$ = this.federationUtxos$.pipe( + map(federationUtxos => federationUtxos.filter(utxo => utxo.pegtxid).map(utxo => { + return { + txid: utxo.pegtxid, + txindex: utxo.pegindex, + amount: utxo.amount, + bitcoinaddress: utxo.bitcoinaddress, + bitcointxid: utxo.txid, + bitcoinindex: utxo.txindex, + blocktime: utxo.pegblocktime, + } + })), + share() + ); + + this.recentPegOuts$ = this.auditUpdated$.pipe( + filter(auditUpdated => auditUpdated === true), + throttleTime(40000), + switchMap(_ => this.apiService.recentPegOuts$()), + share() + ); + + this.federationAddresses$ = this.auditUpdated$.pipe( + filter(auditUpdated => auditUpdated === true), + throttleTime(40000), + switchMap(_ => this.apiService.federationAddresses$()), + share() + ); + + this.federationAddressesOneMonthAgo$ = interval(60 * 60 * 1000) + .pipe( + startWith(0), + switchMap(() => this.apiService.federationAddressesOneMonthAgo$()) + ); + + this.liquidPegsMonth$ = interval(60 * 60 * 1000) + .pipe( + startWith(0), + switchMap(() => this.apiService.listLiquidPegsMonth$()), + map((pegs) => { + const labels = pegs.map(stats => stats.date); + const series = pegs.map(stats => parseFloat(stats.amount) / 100000000); + series.reduce((prev, curr, i) => series[i] = prev + curr, 0); + return { + series, + labels + }; + }), + share(), + ); + + this.liquidReservesMonth$ = interval(60 * 60 * 1000).pipe( + startWith(0), + switchMap(() => this.apiService.listLiquidReservesMonth$()), + map(reserves => { + const labels = reserves.map(stats => stats.date); + const series = reserves.map(stats => parseFloat(stats.amount) / 100000000); + return { + series, + labels + }; + }), + share() + ); + + this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$, this.currentReserves$]) + .pipe( + map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => { + liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000; + + if (liquidPegs.series.length === liquidReserves?.series.length) { + liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000; + } else if (liquidPegs.series.length === liquidReserves?.series.length + 1) { + liquidReserves.series.push(parseFloat(currentReserves?.amount) / 100000000); + liquidReserves.labels.push(liquidPegs.labels[liquidPegs.labels.length - 1]); + } else { + liquidReserves = { + series: [], + labels: [] + }; + } + + return { + liquidPegs, + liquidReserves + }; + }), + share() + ); + } + + ngOnDestroy(): void { + this.destroy$.next(1); + this.destroy$.complete(); + } + +} diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html new file mode 100644 index 000000000..dfaefd534 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html @@ -0,0 +1,42 @@ +
+ +
+
+
Unpeg
+
+
+ {{ unbackedMonths.total }} Unpeg Event +
+
+
+ +
+
Avg Peg Ratio
+
+
+ {{ unbackedMonths.avg.toFixed(5) }} +
+
+
+
+
+
+ + +
+
+
Unpeg
+
+
+
+
+ +
+
Avg Peg Ratio
+
+
+
+
+
+
+ diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.scss b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.scss new file mode 100644 index 000000000..72aa390e4 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.scss @@ -0,0 +1,63 @@ +.fee-estimation-container { + display: flex; + justify-content: space-between; + @media (min-width: 376px) { + flex-direction: row; + } + .item { + max-width: 300px; + margin: 0; + width: -webkit-fill-available; + @media (min-width: 376px) { + margin: 0 auto 0px; + } + + .card-title { + margin-bottom: 4px; + color: #4a68b9; + font-size: 10px; + font-size: 1rem; + white-space: nowrap; + } + + .card-text { + font-size: 22px; + span { + font-size: 11px; + position: relative; + top: -2px; + } + .danger { + color: #D81B60; + } + .correct { + color: #7CB342; + } + } + + .card-text span { + color: #ffffff66; + font-size: 12px; + top: 0px; + } + .fee-text{ + width: fit-content; + margin: auto; + line-height: 1.45; + padding: 0px 2px; + } + } +} + +.loading-container{ + min-height: 76px; +} + +.card-text { + .skeleton-loader { + width: 100%; + display: block; + max-width: 90px; + margin: 15px auto 3px; + } +} diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.ts b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.ts new file mode 100644 index 000000000..45a114c1f --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.ts @@ -0,0 +1,51 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { Observable, map } from 'rxjs'; + +@Component({ + selector: 'app-reserves-ratio-stats', + templateUrl: './reserves-ratio-stats.component.html', + styleUrls: ['./reserves-ratio-stats.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReservesRatioStatsComponent implements OnInit { + @Input() fullHistory$: Observable; + unbackedMonths$: Observable + + constructor() { } + + ngOnInit(): void { + if (!this.fullHistory$) { + return; + } + this.unbackedMonths$ = this.fullHistory$ + .pipe( + map((fullHistory) => { + if (fullHistory.liquidPegs.series.length !== fullHistory.liquidReserves.series.length) { + return { + historyComplete: false, + total: null + }; + } + // Only check the last 3 years + let ratioSeries = fullHistory.liquidReserves.series.map((value: number, index: number) => value / fullHistory.liquidPegs.series[index]); + ratioSeries = ratioSeries.slice(Math.max(ratioSeries.length - 36, 0)); + let total = 0; + let avg = 0; + for (let i = 0; i < ratioSeries.length; i++) { + avg += ratioSeries[i]; + if (ratioSeries[i] < 1) { + total++; + } + } + avg = avg / ratioSeries.length; + return { + historyComplete: true, + total: total, + avg: avg, + }; + }) + ); + + } + +} diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.html b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.html new file mode 100644 index 000000000..cffb73c06 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.html @@ -0,0 +1,4 @@ +
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.scss b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.scss new file mode 100644 index 000000000..9881148fc --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.scss @@ -0,0 +1,6 @@ +.loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 16px); + z-index: 100; +} \ No newline at end of file diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.ts b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.ts new file mode 100644 index 000000000..187a059a1 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.ts @@ -0,0 +1,195 @@ +import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core'; +import { formatDate, formatNumber } from '@angular/common'; +import { EChartsOption } from '../../../graphs/echarts'; + +@Component({ + selector: 'app-reserves-ratio-graph', + templateUrl: './reserves-ratio-graph.component.html', + styleUrls: ['./reserves-ratio-graph.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReservesRatioGraphComponent implements OnInit, OnChanges { + @Input() data: any; + ratioHistoryChartOptions: EChartsOption; + ratioSeries: number[] = []; + + height: number | string = '200'; + right: number | string = '10'; + top: number | string = '20'; + left: number | string = '50'; + template: ('widget' | 'advanced') = 'widget'; + isLoading = true; + + ratioHistoryChartInitOptions = { + renderer: 'svg' + }; + + constructor( + @Inject(LOCALE_ID) private locale: string, + ) { } + + ngOnInit() { + this.isLoading = true; + } + + ngOnChanges() { + if (!this.data) { + return; + } + // Compute the ratio series: the ratio of the reserves to the pegs + this.ratioSeries = this.data.liquidReserves.series.map((value: number, index: number) => value / this.data.liquidPegs.series[index]); + // Truncate the ratio series and labels series to last 3 years + this.ratioSeries = this.ratioSeries.slice(Math.max(this.ratioSeries.length - 36, 0)); + this.data.liquidPegs.labels = this.data.liquidPegs.labels.slice(Math.max(this.data.liquidPegs.labels.length - 36, 0)); + // Cut the values that are too high or too low + this.ratioSeries = this.ratioSeries.map((value: number) => Math.min(Math.max(value, 0.995), 1.005)); + this.ratioHistoryChartOptions = this.createChartOptions(this.ratioSeries, this.data.liquidPegs.labels); + } + + rendered() { + if (!this.data) { + return; + } + this.isLoading = false; + } + + createChartOptions(ratioSeries: number[], labels: string[]): EChartsOption { + return { + grid: { + height: this.height, + right: this.right, + top: this.top, + left: this.left, + }, + animation: false, + dataZoom: [{ + type: 'inside', + realtime: true, + zoomOnMouseWheel: (this.template === 'advanced') ? true : false, + maxSpan: 100, + minSpan: 10, + }, { + show: (this.template === 'advanced') ? true : false, + type: 'slider', + brushSelect: false, + realtime: true, + selectedDataBackground: { + lineStyle: { + color: '#fff', + opacity: 0.45, + }, + areaStyle: { + opacity: 0, + } + } + }], + tooltip: { + trigger: 'axis', + position: (pos, params, el, elRect, size) => { + const obj = { top: -20 }; + obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80; + return obj; + }, + extraCssText: `width: ${(this.template === 'widget') ? '125px' : '135px'}; + background: transparent; + border: none; + box-shadow: none;`, + axisPointer: { + type: 'line', + }, + formatter: (params: any) => { + const colorSpan = (color: string) => ``; + let itemFormatted = '
' + params[0].axisValue + '
'; + const item = params[0]; + const formattedValue = formatNumber(item.value, this.locale, '1.5-5'); + const symbol = (item.value === 1.005) ? '≥ ' : (item.value === 0.995) ? '≤ ' : ''; + itemFormatted += `
+
${colorSpan(item.color)}
+
+
${symbol}${formattedValue}
+
`; + return `
${itemFormatted}
`; + } + }, + xAxis: { + type: 'category', + axisLabel: { + align: 'center', + fontSize: 11, + lineHeight: 12 + }, + boundaryGap: false, + data: labels.map((value: any) => `${formatDate(value, 'MMM\ny', this.locale)}`), + }, + yAxis: { + type: 'value', + axisLabel: { + fontSize: 11, + }, + splitLine: { + lineStyle: { + type: 'dotted', + color: '#ffffff66', + opacity: 0.25, + } + }, + min: 0.995, + max: 1.005, + }, + series: [ + { + data: ratioSeries, + name: '', + type: 'line', + smooth: true, + showSymbol: false, + lineStyle: { + width: 3, + + }, + markLine: { + silent: true, + symbol: 'none', + lineStyle: { + color: '#fff', + opacity: 1, + width: 1, + }, + data: [{ + yAxis: 1, + label: { + show: false, + color: '#ffffff', + } + }], + }, + }, + ], + visualMap: { + show: false, + top: 50, + right: 10, + pieces: [{ + gt: 0, + lte: 0.999, + color: '#D81B60' + }, + { + gt: 0.999, + lte: 1.001, + color: '#FDD835' + }, + { + gt: 1.001, + lte: 2, + color: '#7CB342' + } + ], + outOfRange: { + color: '#999' + } + }, + }; + } +} + diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.html b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.html new file mode 100644 index 000000000..64e68624b --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.html @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.scss b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.scss new file mode 100644 index 000000000..9881148fc --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.scss @@ -0,0 +1,6 @@ +.loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 16px); + z-index: 100; +} \ No newline at end of file diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.ts b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.ts new file mode 100644 index 000000000..b53172e97 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.ts @@ -0,0 +1,126 @@ +import { Component, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core'; +import { EChartsOption } from '../../../graphs/echarts'; +import { CurrentPegs } from '../../../interfaces/node-api.interface'; + + +@Component({ + selector: 'app-reserves-ratio', + templateUrl: './reserves-ratio.component.html', + styleUrls: ['./reserves-ratio.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReservesRatioComponent implements OnInit, OnChanges { + @Input() currentPeg: CurrentPegs; + @Input() currentReserves: CurrentPegs; + ratioChartOptions: EChartsOption; + + height: number | string = '200'; + right: number | string = '10'; + top: number | string = '20'; + left: number | string = '50'; + template: ('widget' | 'advanced') = 'widget'; + isLoading = true; + + ratioChartInitOptions = { + renderer: 'svg' + }; + + constructor() { } + + ngOnInit() { + this.isLoading = true; + } + + ngOnChanges() { + if (!this.currentPeg || !this.currentReserves || this.currentPeg.amount === '0') { + return; + } + this.ratioChartOptions = this.createChartOptions(this.currentPeg, this.currentReserves); + } + + rendered() { + if (!this.currentPeg || !this.currentReserves) { + return; + } + this.isLoading = false; + } + + createChartOptions(currentPeg: CurrentPegs, currentReserves: CurrentPegs): EChartsOption { + return { + series: [ + { + type: 'gauge', + startAngle: 180, + endAngle: 0, + center: ['50%', '70%'], + radius: '100%', + min: 0.999, + max: 1.001, + splitNumber: 2, + axisLine: { + lineStyle: { + width: 6, + color: [ + [0.49, '#D81B60'], + [1, '#7CB342'] + ] + } + }, + axisLabel: { + color: 'inherit', + fontFamily: 'inherit', + }, + pointer: { + icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z', + length: '50%', + width: 16, + offsetCenter: [0, '-27%'], + itemStyle: { + color: 'auto' + } + }, + axisTick: { + length: 12, + lineStyle: { + color: 'auto', + width: 2 + } + }, + splitLine: { + length: 20, + lineStyle: { + color: 'auto', + width: 5 + } + }, + title: { + show: true, + offsetCenter: [0, '-117.5%'], + fontSize: 18, + color: '#4a68b9', + fontFamily: 'inherit', + fontWeight: 500, + }, + detail: { + fontSize: 25, + offsetCenter: [0, '-0%'], + valueAnimation: true, + fontFamily: 'inherit', + fontWeight: 500, + formatter: function (value) { + return (value).toFixed(5); + }, + color: 'inherit' + }, + data: [ + { + value: parseFloat(currentReserves.amount) / parseFloat(currentPeg.amount), + name: 'Peg-O-Meter' + } + ] + } + ] + }; + } +} + diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.html b/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.html new file mode 100644 index 000000000..2856cc210 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.html @@ -0,0 +1,44 @@ +
+
+
+
+
L-BTC in circulation
+
+
{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} L-BTC
+ + As of block {{ currentPeg.lastBlockUpdate }} + +
+
+
+
BTC Reserves
+
+
{{ (+currentReserves.amount) / 100000000 | number: '1.2-2' }} BTC
+ + As of block {{ currentReserves.lastBlockUpdate }} + +
+
+
+
+
+ + +
+
+
L-BTC in circulation
+
+
+
+
+
+
+
BTC Reserves
+
+
+
+
+
+
+
+ diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.scss b/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.scss new file mode 100644 index 000000000..3a8a83f26 --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.scss @@ -0,0 +1,73 @@ +.fee-estimation-container { + display: flex; + justify-content: space-between; + @media (min-width: 376px) { + flex-direction: row; + } + .item { + max-width: 150px; + margin: 0; + width: -webkit-fill-available; + @media (min-width: 376px) { + margin: 0 auto 0px; + } + + .card-title { + color: #4a68b9; + font-size: 10px; + margin-bottom: 4px; + font-size: 1rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .card-text { + font-size: 22px; + span { + font-size: 11px; + position: relative; + top: -2px; + } + } + + &:last-child { + margin-bottom: 0; + } + .card-text span { + color: #ffffff66; + font-size: 12px; + top: 0px; + } + .fee-text{ + border-bottom: 1px solid #ffffff1c; + width: fit-content; + margin: auto; + line-height: 1.45; + padding: 0px 2px; + } + .fiat { + display: block; + font-size: 14px !important; + } + } +} + +.loading-container{ + min-height: 76px; +} + +.card-text { + .skeleton-loader { + width: 100%; + display: block; + &:first-child { + max-width: 90px; + margin: 15px auto 3px; + } + &:last-child { + margin: 10px auto 3px; + max-width: 55px; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.ts b/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.ts new file mode 100644 index 000000000..61f2deb8c --- /dev/null +++ b/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.ts @@ -0,0 +1,24 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Env, StateService } from '../../../services/state.service'; +import { CurrentPegs } from '../../../interfaces/node-api.interface'; + +@Component({ + selector: 'app-reserves-supply-stats', + templateUrl: './reserves-supply-stats.component.html', + styleUrls: ['./reserves-supply-stats.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReservesSupplyStatsComponent implements OnInit { + @Input() currentReserves$: Observable; + @Input() currentPeg$: Observable; + + env: Env; + + constructor(private stateService: StateService) { } + + ngOnInit(): void { + this.env = this.stateService.env; + } + +} diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index b283e0d23..c135fb909 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -540,7 +540,7 @@
- + diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index ace4ded37..9f72931ca 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -507,7 +507,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } } } - if (!found && txFeePerVSize < mempoolBlocks[mempoolBlocks.length - 1].feeRange[0]) { + if (!found && mempoolBlocks.length && txFeePerVSize < mempoolBlocks[mempoolBlocks.length - 1].feeRange[0]) { this.txInBlockIndex = 7; } }); diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 12ce14512..92b64fe5e 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -33,7 +33,7 @@ - + @@ -270,8 +270,16 @@
L-BTC in circulation
- -

{{ liquidPegsMonth.series.slice(-1)[0] | number: '1.2-2' }} L-BTC

+ +

{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} L-BTC

+
+
+
+ +
BTC Reserves 
+
+ +

{{ +(currentReserves.amount) / 100000000 | number: '1.2-2' }} BTC

diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss index 884ba1027..2b319c425 100644 --- a/frontend/src/app/dashboard/dashboard.component.scss +++ b/frontend/src/app/dashboard/dashboard.component.scss @@ -97,6 +97,9 @@ color: #ffffff66; font-size: 12px; } + .bitcoin-color { + color: #b86d12; + } } .progress { width: 90%; diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 8a34bf768..6e65f2332 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -1,7 +1,7 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; -import { combineLatest, merge, Observable, of, Subscription } from 'rxjs'; -import { catchError, filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; -import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface'; +import { combineLatest, EMPTY, merge, Observable, of, Subject, Subscription, timer } from 'rxjs'; +import { catchError, delayWhen, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; +import { AuditStatus, BlockExtended, CurrentPegs, OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface'; import { ApiService } from '../services/api.service'; import { StateService } from '../services/state.service'; @@ -47,8 +47,20 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { transactionsWeightPerSecondOptions: any; isLoadingWebSocket$: Observable; liquidPegsMonth$: Observable; + currentPeg$: Observable; + auditStatus$: Observable; + auditUpdated$: Observable; + liquidReservesMonth$: Observable; + currentReserves$: Observable; + fullHistory$: Observable; + isLoad: boolean = true; currencySubscription: Subscription; currency: string; + private lastPegBlockUpdate: number = 0; + private lastPegAmount: string = ''; + private lastReservesBlockUpdate: number = 0; + + private destroy$ = new Subject(); constructor( public stateService: StateService, @@ -64,6 +76,8 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { ngOnDestroy(): void { this.currencySubscription.unsubscribe(); this.websocketService.stopTrackRbfSummary(); + this.destroy$.next(1); + this.destroy$.complete(); } ngOnInit(): void { @@ -82,35 +96,35 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { this.stateService.mempoolInfo$, this.stateService.vbytesPerSecond$ ]) - .pipe( - map(([mempoolInfo, vbytesPerSecond]) => { - const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100); + .pipe( + map(([mempoolInfo, vbytesPerSecond]) => { + const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100); - let progressColor = 'bg-success'; - if (vbytesPerSecond > 1667) { - progressColor = 'bg-warning'; - } - if (vbytesPerSecond > 3000) { - progressColor = 'bg-danger'; - } + let progressColor = 'bg-success'; + if (vbytesPerSecond > 1667) { + progressColor = 'bg-warning'; + } + if (vbytesPerSecond > 3000) { + progressColor = 'bg-danger'; + } - const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100); - let mempoolSizeProgress = 'bg-danger'; - if (mempoolSizePercentage <= 50) { - mempoolSizeProgress = 'bg-success'; - } else if (mempoolSizePercentage <= 75) { - mempoolSizeProgress = 'bg-warning'; - } + const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100); + let mempoolSizeProgress = 'bg-danger'; + if (mempoolSizePercentage <= 50) { + mempoolSizeProgress = 'bg-success'; + } else if (mempoolSizePercentage <= 75) { + mempoolSizeProgress = 'bg-warning'; + } - return { - memPoolInfo: mempoolInfo, - vBytesPerSecond: vbytesPerSecond, - progressWidth: percent + '%', - progressColor: progressColor, - mempoolSizeProgress: mempoolSizeProgress, - }; - }) - ); + return { + memPoolInfo: mempoolInfo, + vBytesPerSecond: vbytesPerSecond, + progressWidth: percent + '%', + progressColor: progressColor, + mempoolSizeProgress: mempoolSizeProgress, + }; + }) + ); this.mempoolBlocksData$ = this.stateService.mempoolBlocks$ .pipe( @@ -204,18 +218,114 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { ); if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') { - this.liquidPegsMonth$ = this.apiService.listLiquidPegsMonth$() + this.auditStatus$ = this.stateService.blocks$.pipe( + takeUntil(this.destroy$), + throttleTime(40000), + delayWhen(_ => this.isLoad ? timer(0) : timer(2000)), + tap(() => this.isLoad = false), + switchMap(() => this.apiService.federationAuditSynced$()), + shareReplay(1) + ); + + ////////// Pegs historical data ////////// + this.liquidPegsMonth$ = this.auditStatus$.pipe( + throttleTime(60 * 60 * 1000), + switchMap(() => this.apiService.listLiquidPegsMonth$()), + map((pegs) => { + const labels = pegs.map(stats => stats.date); + const series = pegs.map(stats => parseFloat(stats.amount) / 100000000); + series.reduce((prev, curr, i) => series[i] = prev + curr, 0); + return { + series, + labels + }; + }), + share(), + ); + + this.currentPeg$ = this.auditStatus$.pipe( + switchMap(_ => + this.apiService.liquidPegs$().pipe( + filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate), + tap((currentPegs) => { + this.lastPegBlockUpdate = currentPegs.lastBlockUpdate; + }) + ) + ), + share() + ); + + ////////// BTC Reserves historical data ////////// + this.auditUpdated$ = combineLatest([ + this.auditStatus$, + this.currentPeg$ + ]).pipe( + filter(([auditStatus, _]) => auditStatus.isAuditSynced === true), + map(([auditStatus, currentPeg]) => ({ + lastBlockAudit: auditStatus.lastBlockAudit, + currentPegAmount: currentPeg.amount + })), + switchMap(({ lastBlockAudit, currentPegAmount }) => { + const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate; + const amountCheck = currentPegAmount !== this.lastPegAmount; + this.lastPegAmount = currentPegAmount; + return of(blockAuditCheck || amountCheck); + }) + ); + + this.liquidReservesMonth$ = this.auditStatus$.pipe( + throttleTime(60 * 60 * 1000), + switchMap((auditStatus) => { + return auditStatus.isAuditSynced ? this.apiService.listLiquidReservesMonth$() : EMPTY; + }), + map(reserves => { + const labels = reserves.map(stats => stats.date); + const series = reserves.map(stats => parseFloat(stats.amount) / 100000000); + return { + series, + labels + }; + }), + share() + ); + + this.currentReserves$ = this.auditUpdated$.pipe( + filter(auditUpdated => auditUpdated === true), + throttleTime(40000), + switchMap(_ => + this.apiService.liquidReserves$().pipe( + filter((currentReserves) => currentReserves.lastBlockUpdate >= this.lastReservesBlockUpdate), + tap((currentReserves) => { + this.lastReservesBlockUpdate = currentReserves.lastBlockUpdate; + }) + ) + ), + share() + ); + + this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$.pipe(startWith(null)), this.currentReserves$.pipe(startWith(null))]) .pipe( - map((pegs) => { - const labels = pegs.map(stats => stats.date); - const series = pegs.map(stats => parseFloat(stats.amount) / 100000000); - series.reduce((prev, curr, i) => series[i] = prev + curr, 0); + map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => { + liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000; + + if (liquidPegs.series.length === liquidReserves?.series.length) { + liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000; + } else if (liquidPegs.series.length === liquidReserves?.series.length + 1) { + liquidReserves.series.push(parseFloat(currentReserves?.amount) / 100000000); + liquidReserves.labels.push(liquidPegs.labels[liquidPegs.labels.length - 1]); + } else { + liquidReserves = { + series: [], + labels: [] + }; + } + return { - series, - labels + liquidPegs, + liquidReserves }; }), - share(), + share() ); } diff --git a/frontend/src/app/graphs/echarts.ts b/frontend/src/app/graphs/echarts.ts index 342867168..74fec1e71 100644 --- a/frontend/src/app/graphs/echarts.ts +++ b/frontend/src/app/graphs/echarts.ts @@ -1,6 +1,6 @@ // Import tree-shakeable echarts import * as echarts from 'echarts/core'; -import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart } from 'echarts/charts'; +import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart } from 'echarts/charts'; import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components'; import { SVGRenderer, CanvasRenderer } from 'echarts/renderers'; // Typescript interfaces @@ -12,6 +12,6 @@ echarts.use([ TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent, - LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart + LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart ]); export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption }; \ No newline at end of file diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 9d936722d..cebb23f27 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -76,6 +76,46 @@ export interface LiquidPegs { date: string; } +export interface CurrentPegs { + amount: string; + lastBlockUpdate: number; + hash: string; +} + +export interface FederationAddress { + bitcoinaddress: string; + balance: string; +} + +export interface FederationUtxo { + txid: string; + txindex: number; + bitcoinaddress: string; + amount: number; + blocknumber: number; + blocktime: number; + pegtxid: string; + pegindex: number; + pegblocktime: number; +} + +export interface RecentPeg { + txid: string; + txindex: number; + amount: number; + bitcoinaddress: string; + bitcointxid: string; + bitcoinindex: number; + blocktime: number; +} + +export interface AuditStatus { + bitcoinBlocks: number; + bitcoinHeaders: number; + lastBlockAudit: number; + isAuditSynced: boolean; +} + export interface ITranslators { [language: string]: string; } /** diff --git a/frontend/src/app/liquid/liquid-master-page.module.ts b/frontend/src/app/liquid/liquid-master-page.module.ts index bb6e4cff8..0134365bc 100644 --- a/frontend/src/app/liquid/liquid-master-page.module.ts +++ b/frontend/src/app/liquid/liquid-master-page.module.ts @@ -2,8 +2,10 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; import { SharedModule } from '../shared/shared.module'; +import { NgxEchartsModule } from 'ngx-echarts'; import { LiquidMasterPageComponent } from '../components/liquid-master-page/liquid-master-page.component'; + import { StartComponent } from '../components/start/start.component'; import { AddressComponent } from '../components/address/address.component'; import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; @@ -13,6 +15,17 @@ import { AssetsComponent } from '../components/assets/assets.component'; import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component' import { AssetComponent } from '../components/asset/asset.component'; import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component'; +import { ReservesAuditDashboardComponent } from '../components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component'; +import { ReservesSupplyStatsComponent } from '../components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component'; +import { RecentPegsStatsComponent } from '../components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component'; +import { RecentPegsListComponent } from '../components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component'; +import { FederationWalletComponent } from '../components/liquid-reserves-audit/federation-wallet/federation-wallet.component'; +import { FederationUtxosListComponent } from '../components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component'; +import { FederationAddressesStatsComponent } from '../components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component'; +import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component'; +import { ReservesRatioComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component'; +import { ReservesRatioStatsComponent } from '../components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component'; +import { ReservesRatioGraphComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component'; const routes: Routes = [ { @@ -64,6 +77,44 @@ const routes: Routes = [ data: { preload: true, networkSpecific: true }, loadChildren: () => import('../components/block/block.module').then(m => m.BlockModule), }, + { + path: 'audit', + data: { networks: ['liquid'] }, + component: StartComponent, + children: [ + { + path: '', + data: { networks: ['liquid'] }, + component: ReservesAuditDashboardComponent, + } + ] + }, + { + path: 'audit/wallet', + data: { networks: ['liquid'] }, + component: FederationWalletComponent, + children: [ + { + path: 'utxos', + data: { networks: ['liquid'] }, + component: FederationUtxosListComponent, + }, + { + path: 'addresses', + data: { networks: ['liquid'] }, + component: FederationAddressesListComponent, + }, + { + path: '**', + redirectTo: 'utxos' + } + ] + }, + { + path: 'audit/pegs', + data: { networks: ['liquid'] }, + component: RecentPegsListComponent, + }, { path: 'assets', data: { networks: ['liquid'] }, @@ -123,9 +174,23 @@ export class LiquidRoutingModule { } CommonModule, LiquidRoutingModule, SharedModule, + NgxEchartsModule.forRoot({ + echarts: () => import('../graphs/echarts').then(m => m.echarts), + }) ], declarations: [ LiquidMasterPageComponent, + ReservesAuditDashboardComponent, + ReservesSupplyStatsComponent, + RecentPegsStatsComponent, + RecentPegsListComponent, + FederationWalletComponent, + FederationUtxosListComponent, + FederationAddressesStatsComponent, + FederationAddressesListComponent, + ReservesRatioComponent, + ReservesRatioStatsComponent, + ReservesRatioGraphComponent, ] }) export class LiquidMasterPageModule { } \ No newline at end of file diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 854d15c2a..38060d47d 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, - PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface'; + PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg } from '../interfaces/node-api.interface'; import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs'; import { StateService } from './state.service'; import { IBackendInfo, WebsocketResponse } from '../interfaces/websocket.interface'; @@ -178,10 +178,46 @@ export class ApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/' + (fullRbf ? 'fullrbf/' : '') + 'replacements/' + (after || '')); } + liquidPegs$(): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs'); + } + listLiquidPegsMonth$(): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month'); } + liquidReserves$(): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves'); + } + + listLiquidReservesMonth$(): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/month'); + } + + federationAuditSynced$(): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/status'); + } + + federationAddresses$(): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses'); + } + + federationUtxos$(): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos'); + } + + recentPegOuts$(): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegouts'); + } + + federationAddressesOneMonthAgo$(): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses/previous-month'); + } + + federationUtxosOneMonthAgo$(): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos/previous-month'); + } + listFeaturedAssets$(): Observable { return this.httpClient.get(this.apiBaseUrl + '/api/v1/assets/featured'); } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 6a80e9851..36e7e79b8 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, - faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket } from '@fortawesome/free-solid-svg-icons'; + faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MenuComponent } from '../components/menu/menu.component'; import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component'; @@ -385,5 +385,6 @@ export class SharedModule { library.addIcons(faUserCircle); library.addIcons(faCheck); library.addIcons(faRocket); + library.addIcons(faScaleBalanced); } } diff --git a/frontend/sync-assets.js b/frontend/sync-assets.js index b15b5e315..2ff642e27 100644 --- a/frontend/sync-assets.js +++ b/frontend/sync-assets.js @@ -4,6 +4,8 @@ var crypto = require('crypto'); var path = require('node:path'); const LOG_TAG = '[sync-assets]'; let verbose = false; +let MEMPOOL_CDN = false; +let DRY_RUN = false; if (parseInt(process.env.SKIP_SYNC) === 1) { console.log(`${LOG_TAG} SKIP_SYNC is set, not checking any assets`); @@ -15,6 +17,18 @@ if (parseInt(process.env.VERBOSE) === 1) { verbose = true; } +if (parseInt(process.env.MEMPOOL_CDN) === 1) { + console.log(`${LOG_TAG} MEMPOOL_CDN is set, assets will be downloaded from mempool.space`); + MEMPOOL_CDN = true; +} + +if (parseInt(process.env.DRY_RUN) === 1) { + console.log(`${LOG_TAG} DRY_RUN is set, not downloading any assets`); + DRY_RUN = true; +} + +const githubSecret = process.env.GITHUB_TOKEN; + const CONFIG_FILE_NAME = 'mempool-frontend-config.json'; let configContent = {}; @@ -46,8 +60,6 @@ try { } } -const githubSecret = process.env.GITHUB_TOKEN; - function download(filename, url) { https.get(url, (response) => { if (response.statusCode < 200 || response.statusCode > 299) { @@ -60,7 +72,7 @@ function download(filename, url) { }) .on('finish', () => { if (verbose) { - console.log(`${LOG_TAG} Finished downloading ${url} to ${filename}`); + console.log(`${LOG_TAG} \tFinished downloading ${url} to ${filename}`); } }); } @@ -72,7 +84,7 @@ function getLocalHash(filePath) { const hash = crypto.createHash('sha1').update(bufferWithHeader).digest('hex'); if (verbose) { - console.log(`${LOG_TAG} \tgetLocalHash ${filePath} ${hash}`); + console.log(`${LOG_TAG} \t\tgetLocalHash ${filePath} ${hash}`); } return hash; @@ -80,7 +92,7 @@ function getLocalHash(filePath) { function downloadMiningPoolLogos$() { return new Promise((resolve, reject) => { - console.log(`${LOG_TAG} Checking if mining pool logos needs downloading or updating...`); + console.log(`${LOG_TAG} \tChecking if mining pool logos needs downloading or updating...`); const options = { host: 'api.github.com', path: '/repos/mempool/mining-pool-logos/contents/', @@ -110,29 +122,54 @@ function downloadMiningPoolLogos$() { } let downloadedCount = 0; for (const poolLogo of poolLogos) { + if (verbose) { + console.log(`${LOG_TAG} Processing ${poolLogo.name}`); + } const filePath = `${PATH}/mining-pools/${poolLogo.name}`; if (fs.existsSync(filePath)) { const localHash = getLocalHash(filePath); if (verbose) { - console.log(`${LOG_TAG} Remote ${poolLogo.name} logo hash ${poolLogo.sha}`); - console.log(`${LOG_TAG} \tchecking if ${filePath} exists: ${fs.existsSync(filePath)}`); + console.log(`${LOG_TAG} \t\tremote ${poolLogo.name} logo hash ${poolLogo.sha}`); + console.log(`${LOG_TAG} \t\t\tchecking if ${filePath} exists: ${fs.existsSync(filePath)}`); } if (localHash !== poolLogo.sha) { - console.log(`${LOG_TAG} \t\t${poolLogo.name} is different on the remote, downloading...`); - download(filePath, poolLogo.download_url); - downloadedCount++; + console.log(`${LOG_TAG} \t\t\t\t${poolLogo.name} is different on the remote, downloading...`); + let download_url = poolLogo.download_url; + if (MEMPOOL_CDN) { + download_url = download_url.replace("raw.githubusercontent.com/mempool/mining-pool-logos/master", "mempool.space/resources/mining-pools"); + } + if (DRY_RUN) { + console.log(`${LOG_TAG} \t\tDRY_RUN is set, not downloading ${poolLogo.name} but we should`); + } else { + if (verbose) { + console.log(`${LOG_TAG} \t\tDownloading ${download_url} to ${filePath}`); + } + download(filePath, download_url); + downloadedCount++; + } + } else { + console.log(`${LOG_TAG} \t\t${poolLogo.name} is already up to date. Skipping.`); } } else { - console.log(`${LOG_TAG} ${poolLogo.name} is missing, downloading...`); + console.log(`${LOG_TAG} \t\t${poolLogo.name} is missing, downloading...`); const miningPoolsDir = `${PATH}/mining-pools/`; if (!fs.existsSync(miningPoolsDir)){ fs.mkdirSync(miningPoolsDir, { recursive: true }); } - download(filePath, poolLogo.download_url); - downloadedCount++; + let download_url = poolLogo.download_url; + if (MEMPOOL_CDN) { + download_url = download_url.replace("raw.githubusercontent.com/mempool/mining-pool-logos/master", "mempool.space/resources/mining-pools"); + } + if (DRY_RUN) { + console.log(`${LOG_TAG} DRY_RUN is set, not downloading ${poolLogo.name} but it should`); + } else { + console.log(`${LOG_TAG} \tDownloading ${download_url} to ${filePath}`); + download(filePath, download_url); + downloadedCount++; + } } } - console.log(`${LOG_TAG} Downloaded ${downloadedCount} and skipped ${poolLogos.length - downloadedCount} existing mining pool logos`); + console.log(`${LOG_TAG} \t\tDownloaded ${downloadedCount} and skipped ${poolLogos.length - downloadedCount} existing mining pool logos`); resolve(); } catch (e) { reject(`Unable to download mining pool logos. Trying again at next restart. Reason: ${e instanceof Error ? e.message : e}`); @@ -148,7 +185,7 @@ function downloadMiningPoolLogos$() { function downloadPromoVideoSubtiles$() { return new Promise((resolve, reject) => { - console.log(`${LOG_TAG} Checking if promo video subtitles needs downloading or updating...`); + console.log(`${LOG_TAG} \tChecking if promo video subtitles needs downloading or updating...`); const options = { host: 'api.github.com', path: '/repos/mempool/mempool-promo/contents/subtitles', @@ -157,7 +194,7 @@ function downloadPromoVideoSubtiles$() { }; if (githubSecret) { - console.log(`${LOG_TAG} Downloading the promo video subtitles with authentication`); + console.log(`${LOG_TAG} \tDownloading the promo video subtitles with authentication`); options.headers['authorization'] = `Bearer ${githubSecret}`; options.headers['X-GitHub-Api-Version'] = '2022-11-28'; } @@ -179,27 +216,53 @@ function downloadPromoVideoSubtiles$() { } let downloadedCount = 0; for (const language of videoLanguages) { + if (verbose) { + console.log(`${LOG_TAG} Processing ${language.name}`); + } const filePath = `${PATH}/promo-video/${language.name}`; if (fs.existsSync(filePath)) { if (verbose) { - console.log(`${LOG_TAG} ${language.name} remote promo video hash ${language.sha}`); + console.log(`${LOG_TAG} \t${language.name} remote promo video hash ${language.sha}`); } const localHash = getLocalHash(filePath); - if (localHash !== language.sha) { - console.log(`${LOG_TAG} ${language.name} is different on the remote, updating`); - download(filePath, language.download_url); - downloadedCount++; + console.log(`${LOG_TAG} \t\t${language.name} is different on the remote, updating`); + let download_url = language.download_url; + if (MEMPOOL_CDN) { + download_url = download_url.replace("raw.githubusercontent.com/mempool/mempool-promo/master/subtitles", "mempool.space/resources/promo-video"); + } + if (DRY_RUN) { + console.log(`${LOG_TAG} \t\tDRY_RUN is set, not downloading ${language.name} but we should`); + } else { + if (verbose) { + console.log(`${LOG_TAG} \t\tdownloading ${download_url} to ${filePath}`); + } + download(filePath, download_url); + downloadedCount++; + } + } else { + console.log(`${LOG_TAG} \t\t${language.name} is already up to date. Skipping.`); } } else { - console.log(`${LOG_TAG} ${language.name} is missing, downloading`); + console.log(`${LOG_TAG} \t\t${language.name} is missing, downloading`); const promoVideosDir = `${PATH}/promo-video/`; if (!fs.existsSync(promoVideosDir)){ fs.mkdirSync(promoVideosDir, { recursive: true }); } - download(filePath, language.download_url); - downloadedCount++; + let download_url = language.download_url; + if (MEMPOOL_CDN) { + download_url = downloadownload_url = download_url.replace("raw.githubusercontent.com/mempool/mempool-promo/master/subtitles", "mempool.space/resources/promo-video"); + } + if (DRY_RUN) { + console.log(`${LOG_TAG} \tDRY_RUN is set, not downloading ${language.name} but we should`); + } else { + if (verbose) { + console.log(`${LOG_TAG} downloading ${download_url} to ${filePath}`); + } + download(filePath, download_url); + downloadedCount++; + } } } console.log(`${LOG_TAG} Downloaded ${downloadedCount} and skipped ${videoLanguages.length - downloadedCount} existing video subtitles`); @@ -218,7 +281,7 @@ function downloadPromoVideoSubtiles$() { function downloadPromoVideo$() { return new Promise((resolve, reject) => { - console.log(`${LOG_TAG} Checking if promo video needs downloading or updating...`); + console.log(`${LOG_TAG} \tChecking if promo video needs downloading or updating...`); const options = { host: 'api.github.com', path: '/repos/mempool/mempool-promo/contents', @@ -227,7 +290,7 @@ function downloadPromoVideo$() { }; if (githubSecret) { - console.log(`${LOG_TAG} Downloading the promo video with authentication`); + console.log(`${LOG_TAG} \tDownloading the promo video with authentication`); options.headers['authorization'] = `Bearer ${githubSecret}`; options.headers['X-GitHub-Api-Version'] = '2022-11-28'; } @@ -256,14 +319,36 @@ function downloadPromoVideo$() { if (localHash !== item.sha) { console.log(`${LOG_TAG} \tmempool-promo.mp4 is different on the remote, updating`); - download(filePath, item.download_url); - console.log(`${LOG_TAG} \tmempool-promo.mp4 downloaded.`); + let download_url = item.download_url; + if (MEMPOOL_CDN) { + download_url = download_url.replace("raw.githubusercontent.com/mempool/mempool-promo/master/promo.mp4", "mempool.space/resources/promo-video/mempool-promo.mp4"); + } + if (DRY_RUN) { + console.log(`${LOG_TAG} DRY_RUN is set, not downloading mempool-promo.mp4 but we should`); + } else { + if (verbose) { + console.log(`${LOG_TAG} downloading ${download_url} to ${filePath}`); + } + download(filePath, download_url); + console.log(`${LOG_TAG} \tmempool-promo.mp4 downloaded.`); + } } else { - console.log(`${LOG_TAG} \tmempool-promo.mp4 is already up to date. Skipping.`); + console.log(`${LOG_TAG} \t\tmempool-promo.mp4 is already up to date. Skipping.`); } } else { console.log(`${LOG_TAG} \tmempool-promo.mp4 is missing, downloading`); - download(filePath, item.download_url); + let download_url = item.download_url; + if (MEMPOOL_CDN) { + download_url = download_url.replace("raw.githubusercontent.com/mempool/mempool-promo/master/promo.mp4", "mempool.space/resources/promo-video/mempool-promo.mp4"); + } + if (DRY_RUN) { + console.log(`${LOG_TAG} DRY_RUN is set, not downloading mempool-promo.mp4 but we should`); + } else { + if (verbose) { + console.log(`${LOG_TAG} downloading ${download_url} to ${filePath}`); + } + download(filePath, download_url); + } } } resolve(); @@ -300,7 +385,7 @@ if (configContent.BASE_MODULE && configContent.BASE_MODULE === 'liquid') { download(`${PATH}/assets-testnet.minimal.json`, testnetAssetsMinimalJsonUrl); } else { if (verbose) { - console.log(`${LOG_TAG} BASE_MODULE is not set to Liquid (${configContent.BASE_MODULE}), skipping downloading assets`); + console.log(`${LOG_TAG} BASE_MODULE is not set to Liquid (currently ${configContent.BASE_MODULE}), skipping downloading assets`); } } diff --git a/production/mempool-config.liquid.json b/production/mempool-config.liquid.json index 3d25a7a28..459476688 100644 --- a/production/mempool-config.liquid.json +++ b/production/mempool-config.liquid.json @@ -25,30 +25,54 @@ "ESPLORA": { "UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquid", "FALLBACK": [ - "http://node201.va1.mempool.space:3001", - "http://node202.va1.mempool.space:3001", - "http://node203.va1.mempool.space:3001", - "http://node204.va1.mempool.space:3001", - "http://node205.va1.mempool.space:3001", - "http://node206.va1.mempool.space:3001", "http://node201.fmt.mempool.space:3001", "http://node202.fmt.mempool.space:3001", "http://node203.fmt.mempool.space:3001", "http://node204.fmt.mempool.space:3001", "http://node205.fmt.mempool.space:3001", "http://node206.fmt.mempool.space:3001", + "http://node201.va1.mempool.space:3001", + "http://node202.va1.mempool.space:3001", + "http://node203.va1.mempool.space:3001", + "http://node204.va1.mempool.space:3001", + "http://node205.va1.mempool.space:3001", + "http://node206.va1.mempool.space:3001", + "http://node207.va1.mempool.space:3001", + "http://node208.va1.mempool.space:3001", + "http://node209.va1.mempool.space:3001", + "http://node210.va1.mempool.space:3001", + "http://node211.va1.mempool.space:3001", + "http://node212.va1.mempool.space:3001", + "http://node213.va1.mempool.space:3001", + "http://node214.va1.mempool.space:3001", "http://node201.fra.mempool.space:3001", "http://node202.fra.mempool.space:3001", "http://node203.fra.mempool.space:3001", "http://node204.fra.mempool.space:3001", "http://node205.fra.mempool.space:3001", "http://node206.fra.mempool.space:3001", + "http://node207.fra.mempool.space:3001", + "http://node208.fra.mempool.space:3001", + "http://node209.fra.mempool.space:3001", + "http://node210.fra.mempool.space:3001", + "http://node211.fra.mempool.space:3001", + "http://node212.fra.mempool.space:3001", + "http://node213.fra.mempool.space:3001", + "http://node214.fra.mempool.space:3001", "http://node201.tk7.mempool.space:3001", "http://node202.tk7.mempool.space:3001", "http://node203.tk7.mempool.space:3001", "http://node204.tk7.mempool.space:3001", "http://node205.tk7.mempool.space:3001", - "http://node206.tk7.mempool.space:3001" + "http://node206.tk7.mempool.space:3001", + "http://node207.tk7.mempool.space:3001", + "http://node208.tk7.mempool.space:3001", + "http://node209.tk7.mempool.space:3001", + "http://node210.tk7.mempool.space:3001", + "http://node211.tk7.mempool.space:3001", + "http://node212.tk7.mempool.space:3001", + "http://node213.tk7.mempool.space:3001", + "http://node214.tk7.mempool.space:3001" ] }, "DATABASE": { diff --git a/production/mempool-config.liquidtestnet.json b/production/mempool-config.liquidtestnet.json index 47c2f5fef..d77148341 100644 --- a/production/mempool-config.liquidtestnet.json +++ b/production/mempool-config.liquidtestnet.json @@ -25,30 +25,54 @@ "ESPLORA": { "UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquidtestnet", "FALLBACK": [ - "http://node201.va1.mempool.space:3004", - "http://node202.va1.mempool.space:3004", - "http://node203.va1.mempool.space:3004", - "http://node204.va1.mempool.space:3004", - "http://node205.va1.mempool.space:3004", - "http://node206.va1.mempool.space:3004", "http://node201.fmt.mempool.space:3004", "http://node202.fmt.mempool.space:3004", "http://node203.fmt.mempool.space:3004", "http://node204.fmt.mempool.space:3004", "http://node205.fmt.mempool.space:3004", "http://node206.fmt.mempool.space:3004", + "http://node201.va1.mempool.space:3004", + "http://node202.va1.mempool.space:3004", + "http://node203.va1.mempool.space:3004", + "http://node204.va1.mempool.space:3004", + "http://node205.va1.mempool.space:3004", + "http://node206.va1.mempool.space:3004", + "http://node207.va1.mempool.space:3004", + "http://node208.va1.mempool.space:3004", + "http://node209.va1.mempool.space:3004", + "http://node210.va1.mempool.space:3004", + "http://node211.va1.mempool.space:3004", + "http://node212.va1.mempool.space:3004", + "http://node213.va1.mempool.space:3004", + "http://node214.va1.mempool.space:3004", "http://node201.fra.mempool.space:3004", "http://node202.fra.mempool.space:3004", "http://node203.fra.mempool.space:3004", "http://node204.fra.mempool.space:3004", "http://node205.fra.mempool.space:3004", "http://node206.fra.mempool.space:3004", + "http://node207.fra.mempool.space:3004", + "http://node208.fra.mempool.space:3004", + "http://node209.fra.mempool.space:3004", + "http://node210.fra.mempool.space:3004", + "http://node211.fra.mempool.space:3004", + "http://node212.fra.mempool.space:3004", + "http://node213.fra.mempool.space:3004", + "http://node214.fra.mempool.space:3004", "http://node201.tk7.mempool.space:3004", "http://node202.tk7.mempool.space:3004", "http://node203.tk7.mempool.space:3004", "http://node204.tk7.mempool.space:3004", "http://node205.tk7.mempool.space:3004", - "http://node206.tk7.mempool.space:3004" + "http://node206.tk7.mempool.space:3004", + "http://node207.tk7.mempool.space:3004", + "http://node208.tk7.mempool.space:3004", + "http://node209.tk7.mempool.space:3004", + "http://node210.tk7.mempool.space:3004", + "http://node211.tk7.mempool.space:3004", + "http://node212.tk7.mempool.space:3004", + "http://node213.tk7.mempool.space:3004", + "http://node214.tk7.mempool.space:3004" ] }, "DATABASE": { diff --git a/production/mempool-config.mainnet-lightning.json b/production/mempool-config.mainnet-lightning.json index 6bd326bfa..48454ef9f 100644 --- a/production/mempool-config.mainnet-lightning.json +++ b/production/mempool-config.mainnet-lightning.json @@ -18,30 +18,54 @@ "ESPLORA": { "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet", "FALLBACK": [ - "http://node201.va1.mempool.space:3000", - "http://node202.va1.mempool.space:3000", - "http://node203.va1.mempool.space:3000", - "http://node204.va1.mempool.space:3000", - "http://node205.va1.mempool.space:3000", - "http://node206.va1.mempool.space:3000", "http://node201.fmt.mempool.space:3000", "http://node202.fmt.mempool.space:3000", "http://node203.fmt.mempool.space:3000", "http://node204.fmt.mempool.space:3000", "http://node205.fmt.mempool.space:3000", "http://node206.fmt.mempool.space:3000", + "http://node201.va1.mempool.space:3000", + "http://node202.va1.mempool.space:3000", + "http://node203.va1.mempool.space:3000", + "http://node204.va1.mempool.space:3000", + "http://node205.va1.mempool.space:3000", + "http://node206.va1.mempool.space:3000", + "http://node207.va1.mempool.space:3000", + "http://node208.va1.mempool.space:3000", + "http://node209.va1.mempool.space:3000", + "http://node210.va1.mempool.space:3000", + "http://node211.va1.mempool.space:3000", + "http://node212.va1.mempool.space:3000", + "http://node213.va1.mempool.space:3000", + "http://node214.va1.mempool.space:3000", "http://node201.fra.mempool.space:3000", "http://node202.fra.mempool.space:3000", "http://node203.fra.mempool.space:3000", "http://node204.fra.mempool.space:3000", "http://node205.fra.mempool.space:3000", "http://node206.fra.mempool.space:3000", + "http://node207.fra.mempool.space:3000", + "http://node208.fra.mempool.space:3000", + "http://node209.fra.mempool.space:3000", + "http://node210.fra.mempool.space:3000", + "http://node211.fra.mempool.space:3000", + "http://node212.fra.mempool.space:3000", + "http://node213.fra.mempool.space:3000", + "http://node214.fra.mempool.space:3000", "http://node201.tk7.mempool.space:3000", "http://node202.tk7.mempool.space:3000", "http://node203.tk7.mempool.space:3000", "http://node204.tk7.mempool.space:3000", "http://node205.tk7.mempool.space:3000", - "http://node206.tk7.mempool.space:3000" + "http://node206.tk7.mempool.space:3000", + "http://node207.tk7.mempool.space:3000", + "http://node208.tk7.mempool.space:3000", + "http://node209.tk7.mempool.space:3000", + "http://node210.tk7.mempool.space:3000", + "http://node211.tk7.mempool.space:3000", + "http://node212.tk7.mempool.space:3000", + "http://node213.tk7.mempool.space:3000", + "http://node214.tk7.mempool.space:3000" ] }, "LIGHTNING": { diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json index 100d185b9..37adae32f 100644 --- a/production/mempool-config.mainnet.json +++ b/production/mempool-config.mainnet.json @@ -39,30 +39,54 @@ "ESPLORA": { "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet", "FALLBACK": [ - "http://node201.va1.mempool.space:3000", - "http://node202.va1.mempool.space:3000", - "http://node203.va1.mempool.space:3000", - "http://node204.va1.mempool.space:3000", - "http://node205.va1.mempool.space:3000", - "http://node206.va1.mempool.space:3000", "http://node201.fmt.mempool.space:3000", "http://node202.fmt.mempool.space:3000", "http://node203.fmt.mempool.space:3000", "http://node204.fmt.mempool.space:3000", "http://node205.fmt.mempool.space:3000", "http://node206.fmt.mempool.space:3000", + "http://node201.va1.mempool.space:3000", + "http://node202.va1.mempool.space:3000", + "http://node203.va1.mempool.space:3000", + "http://node204.va1.mempool.space:3000", + "http://node205.va1.mempool.space:3000", + "http://node206.va1.mempool.space:3000", + "http://node207.va1.mempool.space:3000", + "http://node208.va1.mempool.space:3000", + "http://node209.va1.mempool.space:3000", + "http://node210.va1.mempool.space:3000", + "http://node211.va1.mempool.space:3000", + "http://node212.va1.mempool.space:3000", + "http://node213.va1.mempool.space:3000", + "http://node214.va1.mempool.space:3000", "http://node201.fra.mempool.space:3000", "http://node202.fra.mempool.space:3000", "http://node203.fra.mempool.space:3000", "http://node204.fra.mempool.space:3000", "http://node205.fra.mempool.space:3000", "http://node206.fra.mempool.space:3000", + "http://node207.fra.mempool.space:3000", + "http://node208.fra.mempool.space:3000", + "http://node209.fra.mempool.space:3000", + "http://node210.fra.mempool.space:3000", + "http://node211.fra.mempool.space:3000", + "http://node212.fra.mempool.space:3000", + "http://node213.fra.mempool.space:3000", + "http://node214.fra.mempool.space:3000", "http://node201.tk7.mempool.space:3000", "http://node202.tk7.mempool.space:3000", "http://node203.tk7.mempool.space:3000", "http://node204.tk7.mempool.space:3000", "http://node205.tk7.mempool.space:3000", - "http://node206.tk7.mempool.space:3000" + "http://node206.tk7.mempool.space:3000", + "http://node207.tk7.mempool.space:3000", + "http://node208.tk7.mempool.space:3000", + "http://node209.tk7.mempool.space:3000", + "http://node210.tk7.mempool.space:3000", + "http://node211.tk7.mempool.space:3000", + "http://node212.tk7.mempool.space:3000", + "http://node213.tk7.mempool.space:3000", + "http://node214.tk7.mempool.space:3000" ] }, "DATABASE": { @@ -82,30 +106,54 @@ "AUDIT": true, "AUDIT_START_HEIGHT": 774000, "SERVERS": [ - "node201.va1.mempool.space", - "node202.va1.mempool.space", - "node203.va1.mempool.space", - "node204.va1.mempool.space", - "node205.va1.mempool.space", - "node206.va1.mempool.space", "node201.fmt.mempool.space", "node202.fmt.mempool.space", "node203.fmt.mempool.space", "node204.fmt.mempool.space", "node205.fmt.mempool.space", "node206.fmt.mempool.space", + "node201.va1.mempool.space", + "node202.va1.mempool.space", + "node203.va1.mempool.space", + "node204.va1.mempool.space", + "node205.va1.mempool.space", + "node206.va1.mempool.space", + "node207.va1.mempool.space", + "node208.va1.mempool.space", + "node209.va1.mempool.space", + "node210.va1.mempool.space", + "node211.va1.mempool.space", + "node212.va1.mempool.space", + "node213.va1.mempool.space", + "node214.va1.mempool.space", "node201.fra.mempool.space", "node202.fra.mempool.space", "node203.fra.mempool.space", "node204.fra.mempool.space", "node205.fra.mempool.space", "node206.fra.mempool.space", + "node207.fra.mempool.space", + "node208.fra.mempool.space", + "node209.fra.mempool.space", + "node210.fra.mempool.space", + "node211.fra.mempool.space", + "node212.fra.mempool.space", + "node213.fra.mempool.space", + "node214.fra.mempool.space", "node201.tk7.mempool.space", "node202.tk7.mempool.space", "node203.tk7.mempool.space", "node204.tk7.mempool.space", "node205.tk7.mempool.space", - "node206.tk7.mempool.space" + "node206.tk7.mempool.space", + "node207.tk7.mempool.space", + "node208.tk7.mempool.space", + "node209.tk7.mempool.space", + "node210.tk7.mempool.space", + "node211.tk7.mempool.space", + "node212.tk7.mempool.space", + "node213.tk7.mempool.space", + "node214.tk7.mempool.space" ] }, "REDIS": { diff --git a/production/mempool-config.signet-lightning.json b/production/mempool-config.signet-lightning.json index 229b226be..fdc96d256 100644 --- a/production/mempool-config.signet-lightning.json +++ b/production/mempool-config.signet-lightning.json @@ -18,30 +18,54 @@ "ESPLORA": { "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet", "FALLBACK": [ - "http://node201.va1.mempool.space:3003", - "http://node202.va1.mempool.space:3003", - "http://node203.va1.mempool.space:3003", - "http://node204.va1.mempool.space:3003", - "http://node205.va1.mempool.space:3003", - "http://node206.va1.mempool.space:3003", "http://node201.fmt.mempool.space:3003", "http://node202.fmt.mempool.space:3003", "http://node203.fmt.mempool.space:3003", "http://node204.fmt.mempool.space:3003", "http://node205.fmt.mempool.space:3003", "http://node206.fmt.mempool.space:3003", + "http://node201.va1.mempool.space:3003", + "http://node202.va1.mempool.space:3003", + "http://node203.va1.mempool.space:3003", + "http://node204.va1.mempool.space:3003", + "http://node205.va1.mempool.space:3003", + "http://node206.va1.mempool.space:3003", + "http://node207.va1.mempool.space:3003", + "http://node208.va1.mempool.space:3003", + "http://node209.va1.mempool.space:3003", + "http://node210.va1.mempool.space:3003", + "http://node211.va1.mempool.space:3003", + "http://node212.va1.mempool.space:3003", + "http://node213.va1.mempool.space:3003", + "http://node214.va1.mempool.space:3003", "http://node201.fra.mempool.space:3003", "http://node202.fra.mempool.space:3003", "http://node203.fra.mempool.space:3003", "http://node204.fra.mempool.space:3003", "http://node205.fra.mempool.space:3003", "http://node206.fra.mempool.space:3003", + "http://node207.fra.mempool.space:3003", + "http://node208.fra.mempool.space:3003", + "http://node209.fra.mempool.space:3003", + "http://node210.fra.mempool.space:3003", + "http://node211.fra.mempool.space:3003", + "http://node212.fra.mempool.space:3003", + "http://node213.fra.mempool.space:3003", + "http://node214.fra.mempool.space:3003", "http://node201.tk7.mempool.space:3003", "http://node202.tk7.mempool.space:3003", "http://node203.tk7.mempool.space:3003", "http://node204.tk7.mempool.space:3003", "http://node205.tk7.mempool.space:3003", - "http://node206.tk7.mempool.space:3003" + "http://node206.tk7.mempool.space:3003", + "http://node207.tk7.mempool.space:3003", + "http://node208.tk7.mempool.space:3003", + "http://node209.tk7.mempool.space:3003", + "http://node210.tk7.mempool.space:3003", + "http://node211.tk7.mempool.space:3003", + "http://node212.tk7.mempool.space:3003", + "http://node213.tk7.mempool.space:3003", + "http://node214.tk7.mempool.space:3003" ] }, "LIGHTNING": { diff --git a/production/mempool-config.signet.json b/production/mempool-config.signet.json index c4b84f11b..9151679bd 100644 --- a/production/mempool-config.signet.json +++ b/production/mempool-config.signet.json @@ -27,30 +27,54 @@ "ESPLORA": { "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet", "FALLBACK": [ - "http://node201.va1.mempool.space:3003", - "http://node202.va1.mempool.space:3003", - "http://node203.va1.mempool.space:3003", - "http://node204.va1.mempool.space:3003", - "http://node205.va1.mempool.space:3003", - "http://node206.va1.mempool.space:3003", "http://node201.fmt.mempool.space:3003", "http://node202.fmt.mempool.space:3003", "http://node203.fmt.mempool.space:3003", "http://node204.fmt.mempool.space:3003", "http://node205.fmt.mempool.space:3003", "http://node206.fmt.mempool.space:3003", + "http://node201.va1.mempool.space:3003", + "http://node202.va1.mempool.space:3003", + "http://node203.va1.mempool.space:3003", + "http://node204.va1.mempool.space:3003", + "http://node205.va1.mempool.space:3003", + "http://node206.va1.mempool.space:3003", + "http://node207.va1.mempool.space:3003", + "http://node208.va1.mempool.space:3003", + "http://node209.va1.mempool.space:3003", + "http://node210.va1.mempool.space:3003", + "http://node211.va1.mempool.space:3003", + "http://node212.va1.mempool.space:3003", + "http://node213.va1.mempool.space:3003", + "http://node214.va1.mempool.space:3003", "http://node201.fra.mempool.space:3003", "http://node202.fra.mempool.space:3003", "http://node203.fra.mempool.space:3003", "http://node204.fra.mempool.space:3003", "http://node205.fra.mempool.space:3003", "http://node206.fra.mempool.space:3003", + "http://node207.fra.mempool.space:3003", + "http://node208.fra.mempool.space:3003", + "http://node209.fra.mempool.space:3003", + "http://node210.fra.mempool.space:3003", + "http://node211.fra.mempool.space:3003", + "http://node212.fra.mempool.space:3003", + "http://node213.fra.mempool.space:3003", + "http://node214.fra.mempool.space:3003", "http://node201.tk7.mempool.space:3003", "http://node202.tk7.mempool.space:3003", "http://node203.tk7.mempool.space:3003", "http://node204.tk7.mempool.space:3003", "http://node205.tk7.mempool.space:3003", - "http://node206.tk7.mempool.space:3003" + "http://node206.tk7.mempool.space:3003", + "http://node207.tk7.mempool.space:3003", + "http://node208.tk7.mempool.space:3003", + "http://node209.tk7.mempool.space:3003", + "http://node210.tk7.mempool.space:3003", + "http://node211.tk7.mempool.space:3003", + "http://node212.tk7.mempool.space:3003", + "http://node213.tk7.mempool.space:3003", + "http://node214.tk7.mempool.space:3003" ] }, "DATABASE": { diff --git a/production/mempool-config.testnet-lightning.json b/production/mempool-config.testnet-lightning.json index 60ee128a2..2ab3c6f19 100644 --- a/production/mempool-config.testnet-lightning.json +++ b/production/mempool-config.testnet-lightning.json @@ -18,30 +18,54 @@ "ESPLORA": { "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet", "FALLBACK": [ - "http://node201.va1.mempool.space:3002", - "http://node202.va1.mempool.space:3002", - "http://node203.va1.mempool.space:3002", - "http://node204.va1.mempool.space:3002", - "http://node205.va1.mempool.space:3002", - "http://node206.va1.mempool.space:3002", "http://node201.fmt.mempool.space:3002", "http://node202.fmt.mempool.space:3002", "http://node203.fmt.mempool.space:3002", "http://node204.fmt.mempool.space:3002", "http://node205.fmt.mempool.space:3002", "http://node206.fmt.mempool.space:3002", + "http://node201.va1.mempool.space:3002", + "http://node202.va1.mempool.space:3002", + "http://node203.va1.mempool.space:3002", + "http://node204.va1.mempool.space:3002", + "http://node205.va1.mempool.space:3002", + "http://node206.va1.mempool.space:3002", + "http://node207.va1.mempool.space:3002", + "http://node208.va1.mempool.space:3002", + "http://node209.va1.mempool.space:3002", + "http://node210.va1.mempool.space:3002", + "http://node211.va1.mempool.space:3002", + "http://node212.va1.mempool.space:3002", + "http://node213.va1.mempool.space:3002", + "http://node214.va1.mempool.space:3002", "http://node201.fra.mempool.space:3002", "http://node202.fra.mempool.space:3002", "http://node203.fra.mempool.space:3002", "http://node204.fra.mempool.space:3002", "http://node205.fra.mempool.space:3002", "http://node206.fra.mempool.space:3002", + "http://node207.fra.mempool.space:3002", + "http://node208.fra.mempool.space:3002", + "http://node209.fra.mempool.space:3002", + "http://node210.fra.mempool.space:3002", + "http://node211.fra.mempool.space:3002", + "http://node212.fra.mempool.space:3002", + "http://node213.fra.mempool.space:3002", + "http://node214.fra.mempool.space:3002", "http://node201.tk7.mempool.space:3002", "http://node202.tk7.mempool.space:3002", "http://node203.tk7.mempool.space:3002", "http://node204.tk7.mempool.space:3002", "http://node205.tk7.mempool.space:3002", - "http://node206.tk7.mempool.space:3002" + "http://node206.tk7.mempool.space:3002", + "http://node207.tk7.mempool.space:3002", + "http://node208.tk7.mempool.space:3002", + "http://node209.tk7.mempool.space:3002", + "http://node210.tk7.mempool.space:3002", + "http://node211.tk7.mempool.space:3002", + "http://node212.tk7.mempool.space:3002", + "http://node213.tk7.mempool.space:3002", + "http://node214.tk7.mempool.space:3002" ] }, "LIGHTNING": { diff --git a/production/mempool-config.testnet.json b/production/mempool-config.testnet.json index b3d7cfadd..4a4b550c1 100644 --- a/production/mempool-config.testnet.json +++ b/production/mempool-config.testnet.json @@ -27,30 +27,54 @@ "ESPLORA": { "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet", "FALLBACK": [ - "http://node201.va1.mempool.space:3002", - "http://node202.va1.mempool.space:3002", - "http://node203.va1.mempool.space:3002", - "http://node204.va1.mempool.space:3002", - "http://node205.va1.mempool.space:3002", - "http://node206.va1.mempool.space:3002", "http://node201.fmt.mempool.space:3002", "http://node202.fmt.mempool.space:3002", "http://node203.fmt.mempool.space:3002", "http://node204.fmt.mempool.space:3002", "http://node205.fmt.mempool.space:3002", "http://node206.fmt.mempool.space:3002", + "http://node201.va1.mempool.space:3002", + "http://node202.va1.mempool.space:3002", + "http://node203.va1.mempool.space:3002", + "http://node204.va1.mempool.space:3002", + "http://node205.va1.mempool.space:3002", + "http://node206.va1.mempool.space:3002", + "http://node207.va1.mempool.space:3002", + "http://node208.va1.mempool.space:3002", + "http://node209.va1.mempool.space:3002", + "http://node210.va1.mempool.space:3002", + "http://node211.va1.mempool.space:3002", + "http://node212.va1.mempool.space:3002", + "http://node213.va1.mempool.space:3002", + "http://node214.va1.mempool.space:3002", "http://node201.fra.mempool.space:3002", "http://node202.fra.mempool.space:3002", "http://node203.fra.mempool.space:3002", "http://node204.fra.mempool.space:3002", "http://node205.fra.mempool.space:3002", "http://node206.fra.mempool.space:3002", + "http://node207.fra.mempool.space:3002", + "http://node208.fra.mempool.space:3002", + "http://node209.fra.mempool.space:3002", + "http://node210.fra.mempool.space:3002", + "http://node211.fra.mempool.space:3002", + "http://node212.fra.mempool.space:3002", + "http://node213.fra.mempool.space:3002", + "http://node214.fra.mempool.space:3002", "http://node201.tk7.mempool.space:3002", "http://node202.tk7.mempool.space:3002", "http://node203.tk7.mempool.space:3002", "http://node204.tk7.mempool.space:3002", "http://node205.tk7.mempool.space:3002", - "http://node206.tk7.mempool.space:3002" + "http://node206.tk7.mempool.space:3002", + "http://node207.tk7.mempool.space:3002", + "http://node208.tk7.mempool.space:3002", + "http://node209.tk7.mempool.space:3002", + "http://node210.tk7.mempool.space:3002", + "http://node211.tk7.mempool.space:3002", + "http://node212.tk7.mempool.space:3002", + "http://node213.tk7.mempool.space:3002", + "http://node214.tk7.mempool.space:3002" ] }, "DATABASE": { diff --git a/production/nginx-cache-heater b/production/nginx-cache-heater index 00fc6ea7a..4bbe8ee15 100755 --- a/production/nginx-cache-heater +++ b/production/nginx-cache-heater @@ -1,5 +1,5 @@ #!/usr/bin/env zsh -hostname=mempool.space +hostname=$(hostname) heat() { diff --git a/production/nginx/http-proxy-cache.conf b/production/nginx/http-proxy-cache.conf index 5505b094b..0024a3b30 100644 --- a/production/nginx/http-proxy-cache.conf +++ b/production/nginx/http-proxy-cache.conf @@ -1,8 +1,12 @@ # proxy cache -proxy_cache_path /var/cache/nginx/api keys_zone=api:20m levels=1:2 inactive=365d max_size=2000m; -proxy_cache_path /var/cache/nginx/unfurler keys_zone=unfurler:20m levels=1:2 inactive=365d max_size=2000m; -proxy_cache_path /var/cache/nginx/slurper keys_zone=slurper:20m levels=1:2 inactive=365d max_size=5000m; -proxy_cache_path /var/cache/nginx/services keys_zone=services:20m levels=1:2 inactive=365d max_size=100m; +proxy_cache_path /var/cache/nginx/services keys_zone=services:20m levels=1:2 inactive=30d max_size=200m; +proxy_cache_path /var/cache/nginx/apihot keys_zone=apihot:20m levels=1:2 inactive=60m max_size=20m; +proxy_cache_path /var/cache/nginx/apiwarm keys_zone=apiwarm:20m levels=1:2 inactive=24h max_size=200m; +proxy_cache_path /var/cache/nginx/apinormal keys_zone=apinormal:200m levels=1:2 inactive=30d max_size=2000m; +proxy_cache_path /var/cache/nginx/apicold keys_zone=apicold:200m levels=1:2 inactive=365d max_size=2000m; + +proxy_cache_path /var/cache/nginx/unfurler keys_zone=unfurler:200m levels=1:2 inactive=30d max_size=2000m; +proxy_cache_path /var/cache/nginx/slurper keys_zone=slurper:500m levels=1:2 inactive=365d max_size=5000m; proxy_cache_path /var/cache/nginx/markets keys_zone=markets:20m levels=1:2 inactive=365d max_size=100m; types_hash_max_size 4096; -proxy_buffer_size 8k; \ No newline at end of file +proxy_buffer_size 8k; diff --git a/production/nginx/location-api-v1-lightning.conf b/production/nginx/location-api-v1-lightning.conf index f90fd529a..625bebade 100644 --- a/production/nginx/location-api-v1-lightning.conf +++ b/production/nginx/location-api-v1-lightning.conf @@ -12,7 +12,7 @@ location @mempool-api-v1-lightning { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apiwarm; proxy_cache_valid 200 10s; proxy_redirect off; diff --git a/production/nginx/location-api.conf b/production/nginx/location-api.conf index f245154a0..bee4ce50d 100644 --- a/production/nginx/location-api.conf +++ b/production/nginx/location-api.conf @@ -77,7 +77,7 @@ location @mempool-api-v1-cache-forever { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apicold; proxy_cache_valid 200 30d; proxy_redirect off; @@ -94,9 +94,11 @@ location @mempool-api-v1-cache-hot { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apihot; proxy_cache_valid 200 1s; proxy_redirect off; + + expires 1s; } location @mempool-api-v1-cache-warm { @@ -109,7 +111,7 @@ location @mempool-api-v1-cache-warm { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apiwarm; proxy_cache_valid 200 10s; proxy_redirect off; } @@ -122,7 +124,7 @@ location @mempool-api-v1-cache-normal { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache api; + proxy_cache apinormal; proxy_cache_valid 200 2s; proxy_redirect off; @@ -167,7 +169,7 @@ location @esplora-api-cache-forever { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apicold; proxy_cache_valid 200 30d; proxy_redirect off; diff --git a/production/nginx/location-liquid-api.conf b/production/nginx/location-liquid-api.conf index 6c222c469..5286c1a72 100644 --- a/production/nginx/location-liquid-api.conf +++ b/production/nginx/location-liquid-api.conf @@ -75,7 +75,7 @@ location @mempool-liquid-api-v1-cache-forever { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apicold; proxy_cache_valid 200 30d; proxy_redirect off; @@ -92,7 +92,7 @@ location @mempool-liquid-api-v1-cache-warm { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apiwarm; proxy_cache_valid 200 10s; proxy_redirect off; } @@ -105,7 +105,7 @@ location @mempool-liquid-api-v1-cache-normal { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache api; + proxy_cache apinormal; proxy_cache_valid 200 10s; proxy_redirect off; @@ -150,7 +150,7 @@ location @esplora-liquid-api-cache-forever { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apicold; proxy_cache_valid 200 30d; proxy_redirect off; diff --git a/production/nginx/location-liquidtestnet-api.conf b/production/nginx/location-liquidtestnet-api.conf index 5d5be5d43..4f6615b40 100644 --- a/production/nginx/location-liquidtestnet-api.conf +++ b/production/nginx/location-liquidtestnet-api.conf @@ -79,7 +79,7 @@ location @mempool-liquidtestnet-api-v1-cache-forever { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apicold; proxy_cache_valid 200 30d; proxy_redirect off; @@ -96,7 +96,7 @@ location @mempool-liquidtestnet-api-v1-cache-warm { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apiwarm; proxy_cache_valid 200 10s; proxy_redirect off; } @@ -109,7 +109,7 @@ location @mempool-liquidtestnet-api-v1-cache-normal { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache api; + proxy_cache apinormal; proxy_cache_valid 200 10s; proxy_redirect off; @@ -154,7 +154,7 @@ location @esplora-liquidtestnet-api-cache-forever { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apicold; proxy_cache_valid 200 30d; proxy_redirect off; diff --git a/production/nginx/location-signet-api-v1-lightning.conf b/production/nginx/location-signet-api-v1-lightning.conf index ab14a170b..4fe0b6ddd 100644 --- a/production/nginx/location-signet-api-v1-lightning.conf +++ b/production/nginx/location-signet-api-v1-lightning.conf @@ -13,7 +13,7 @@ location @mempool-signet-api-v1-lightning { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apiwarm; proxy_cache_valid 200 10s; proxy_redirect off; diff --git a/production/nginx/location-signet-api.conf b/production/nginx/location-signet-api.conf index 8469043a8..eb8cbeb6a 100644 --- a/production/nginx/location-signet-api.conf +++ b/production/nginx/location-signet-api.conf @@ -79,7 +79,7 @@ location @mempool-signet-api-v1-cache-forever { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apicold; proxy_cache_valid 200 30d; proxy_redirect off; @@ -96,7 +96,7 @@ location @mempool-signet-api-v1-cache-warm { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apiwarm; proxy_cache_valid 200 10s; proxy_redirect off; } @@ -109,7 +109,7 @@ location @mempool-signet-api-v1-cache-normal { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache api; + proxy_cache apinormal; proxy_cache_valid 200 10s; proxy_redirect off; @@ -154,7 +154,7 @@ location @esplora-signet-api-cache-forever { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apicold; proxy_cache_valid 200 30d; proxy_redirect off; diff --git a/production/nginx/location-testnet-api-v1-lightning.conf b/production/nginx/location-testnet-api-v1-lightning.conf index cc7c617a6..316b37cc3 100644 --- a/production/nginx/location-testnet-api-v1-lightning.conf +++ b/production/nginx/location-testnet-api-v1-lightning.conf @@ -13,7 +13,7 @@ location @mempool-testnet-api-v1-lightning { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apiwarm; proxy_cache_valid 200 10s; proxy_redirect off; diff --git a/production/nginx/location-testnet-api.conf b/production/nginx/location-testnet-api.conf index 9f0c41147..cce3e585c 100644 --- a/production/nginx/location-testnet-api.conf +++ b/production/nginx/location-testnet-api.conf @@ -79,7 +79,7 @@ location @mempool-testnet-api-v1-cache-forever { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apicold; proxy_cache_valid 200 30d; proxy_redirect off; @@ -96,7 +96,7 @@ location @mempool-testnet-api-v1-cache-warm { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apiwarm; proxy_cache_valid 200 10s; proxy_redirect off; } @@ -109,7 +109,7 @@ location @mempool-testnet-api-v1-cache-normal { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache api; + proxy_cache apinormal; proxy_cache_valid 200 10s; proxy_redirect off; @@ -154,7 +154,7 @@ location @esplora-testnet-api-cache-forever { proxy_cache_background_update on; proxy_cache_use_stale updating; - proxy_cache api; + proxy_cache apicold; proxy_cache_valid 200 30d; proxy_redirect off;