diff --git a/backend/README.md b/backend/README.md index cd96a672c..8b186c397 100644 --- a/backend/README.md +++ b/backend/README.md @@ -181,7 +181,7 @@ Create a new wallet, if needed: bitcoin-cli -regtest createwallet test ``` -Load wallet (this command may take a while if you have lot of UTXOs): +Load wallet (this command may take a while if you have a lot of UTXOs): ``` bitcoin-cli -regtest loadwallet test ``` @@ -233,9 +233,9 @@ By default, mining pools will be not automatically updated regularly (`config.ME To manually update your mining pools, you can use the `--update-pools` command line flag when you run the nodejs backend. For example `npm run start --update-pools`. This will trigger the mining pools update and automatically re-index appropriate blocks. -You can enabled the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` to `true` in your `mempool-config.json`. +You can enable the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` to `true` in your `mempool-config.json`. -When a `coinbase tag` or `coinbase address` change is detected, all blocks tagged to the `unknown` mining pools (starting from height 130635) will be deleted from the `blocks` table. Additionaly, all blocks which were tagged to the pool which has been updated will also be deleted from the `blocks` table. Of course, those blocks will be automatically reindexed. +When a `coinbase tag` or `coinbase address` change is detected, all blocks tagged to the `unknown` mining pools (starting from height 130635) will be deleted from the `blocks` table. Additionally, all blocks which were tagged to the pool which has been updated will also be deleted from the `blocks` table. Of course, those blocks will be automatically reindexed. ### Re-index tables diff --git a/backend/package-lock.json b/backend/package-lock.json index a81a848e6..570d825a3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -23,7 +23,7 @@ "rust-gbt": "file:./rust-gbt", "socks-proxy-agent": "~7.0.0", "typescript": "~4.9.3", - "ws": "~8.17.0" + "ws": "~8.17.1" }, "devDependencies": { "@babel/code-frame": "^7.18.6", @@ -7690,9 +7690,9 @@ } }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, @@ -13424,9 +13424,9 @@ } }, "ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "requires": {} }, "y18n": { diff --git a/backend/package.json b/backend/package.json index e4e4c9e2f..906b97523 100644 --- a/backend/package.json +++ b/backend/package.json @@ -52,7 +52,7 @@ "redis": "^4.6.6", "socks-proxy-agent": "~7.0.0", "typescript": "~4.9.3", - "ws": "~8.17.0" + "ws": "~8.17.1" }, "devDependencies": { "@babel/code-frame": "^7.18.6", diff --git a/backend/src/__tests__/api/common.ts b/backend/src/__tests__/api/common.ts index 10d6df868..74a7db88f 100644 --- a/backend/src/__tests__/api/common.ts +++ b/backend/src/__tests__/api/common.ts @@ -4,21 +4,37 @@ import { MempoolTransactionExtended } from '../../mempool.interfaces'; const randomTransactions = require('./test-data/transactions-random.json'); const replacedTransactions = require('./test-data/transactions-replaced.json'); const rbfTransactions = require('./test-data/transactions-rbfs.json'); +const nonStandardTransactions = require('./test-data/non-standard-txs.json'); -describe('Mempool Utils', () => { - test('should detect RBF transactions with fast method', () => { +describe('Common', () => { + describe('RBF', () => { const newTransactions = rbfTransactions.concat(randomTransactions); - const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions); - expect(Object.values(result).length).toEqual(2); - expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); - expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); + test('should detect RBF transactions with fast method', () => { + const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions); + expect(Object.values(result).length).toEqual(2); + expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); + expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); + }); + + test('should detect RBF transactions with scalable method', () => { + const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true); + expect(Object.values(result).length).toEqual(2); + expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); + expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); + }); }); - test.only('should detect RBF transactions with scalable method', () => { - const newTransactions = rbfTransactions.concat(randomTransactions); - const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true); - expect(Object.values(result).length).toEqual(2); - expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); - expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); + describe('Mempool Goggles', () => { + test('should detect nonstandard transactions', () => { + nonStandardTransactions.forEach((tx) => { + expect(Common.isNonStandard(tx)).toEqual(true); + }); + }); + + test('should not misclassify as nonstandard transactions', () => { + randomTransactions.forEach((tx) => { + expect(Common.isNonStandard(tx)).toEqual(false); + }); + }); }); }); diff --git a/backend/src/__tests__/api/test-data/non-standard-txs.json b/backend/src/__tests__/api/test-data/non-standard-txs.json new file mode 100644 index 000000000..286fe2945 --- /dev/null +++ b/backend/src/__tests__/api/test-data/non-standard-txs.json @@ -0,0 +1,52 @@ +[ + { + "txid": "50136231cb7eeeffb17fc41d1cca213426abe5bf3760e3d6421cad0c0edad367", + "version": 1, + "locktime": 0, + "vin": [ + { + "txid": "c7f86fb7b830124057475b282809f3474ef3565daa3de0b599980fb9e84ab019", + "vout": 4217, + "prevout": { + "scriptpubkey": "001466197b5eadd8067ec194a457e1044b6d1fbdd3b3", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 66197b5eadd8067ec194a457e1044b6d1fbdd3b3", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "bc1qvcvhkh4dmqr8asv553t7zpztd50mm5ang4na33", + "value": 106 + }, + "scriptsig": "", + "scriptsig_asm": "", + "witness": [ + "3043021f2af6060a142c6cfd7428adad6a50745d2424813d7ced5c0bbcca85e70de1be022021440ca1c8c3ed49ecd1b64dca6911adcd430c5d3dd60d77ffe0072953999f5b01", + "02ead5c34e3d2c506574b562f857576e11380b6ba15d9f0ad7b7303fdaa9c1513d" + ], + "is_coinbase": false, + "sequence": 4294967295 + } + ], + "vout": [ + { + "scriptpubkey": "6a023a29", + "scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_2 3a29", + "scriptpubkey_type": "op_return", + "value": 0 + }, + { + "scriptpubkey": "6a036d7648", + "scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_3 6d7648", + "scriptpubkey_type": "op_return", + "value": 0 + } + ], + "size": 186, + "weight": 420, + "sigops": 1, + "fee": 106, + "status": { + "confirmed": true, + "block_height": 836361, + "block_hash": "0000000000000000000341cc26cda4af82cd25f7063c448772228cbf2836915b", + "block_time": 1711448028 + } + } +] \ No newline at end of file diff --git a/backend/src/__tests__/api/test-data/transactions-random.json b/backend/src/__tests__/api/test-data/transactions-random.json index 6bf4a0edd..ceab6e3c6 100644 --- a/backend/src/__tests__/api/test-data/transactions-random.json +++ b/backend/src/__tests__/api/test-data/transactions-random.json @@ -273,5 +273,63 @@ }, "bestDescendant": null, "cpfpChecked": true + }, + { + "txid": "20b984492b5264162a4c92c9a34bc7fa08b67d669de7b4c5982ad3cb28aaecf6", + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "3adda6afd547193793c248e667c2b7dbf26d705003de65e3a25e5be698286aef", + "vout": 2, + "prevout": { + "scriptpubkey": "0014989cf12774fc705609610c7b9419f2d1c4807644", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 989cf12774fc705609610c7b9419f2d1c4807644", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "bc1qnzw0zfm5l3c9vztpp3aegx0j68zgqajyffr2r6", + "value": 27619 + }, + "scriptsig": "", + "scriptsig_asm": "", + "witness": [ + "304402205d7f1e0d928982645c2bcc4c730c4545c382d6520c2a14eebc71594702cd06b302200511d452ce51c79017536f50acb115eefe7c04506ad12b9307d2b5d56b999beb01", + "03716cb4f0430fe69c596a12c6680c55803150645989b406772838d548cde7cca5" + ], + "is_coinbase": false, + "sequence": 4294967295 + } + ], + "vout": [ + { + "scriptpubkey": "6a5d0614c0a2331441", + "scriptpubkey_asm": "OP_RETURN OP_PUSHNUM_13 OP_PUSHBYTES_6 14c0a2331441", + "scriptpubkey_type": "op_return", + "value": 0 + }, + { + "scriptpubkey": "5114d71c6c3ea7ba7e6ee477a0bfd82c20c78997882c", + "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_20 d71c6c3ea7ba7e6ee477a0bfd82c20c78997882c", + "scriptpubkey_type": "unknown", + "scriptpubkey_address": "bc1p6uwxc048hflxaerh5zlastpqc7ye0zpvq7gq2a", + "value": 546 + }, + { + "scriptpubkey": "0014989cf12774fc705609610c7b9419f2d1c4807644", + "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 989cf12774fc705609610c7b9419f2d1c4807644", + "scriptpubkey_type": "v0_p2wpkh", + "scriptpubkey_address": "bc1qnzw0zfm5l3c9vztpp3aegx0j68zgqajyffr2r6", + "value": 23073 + } + ], + "size": 240, + "weight": 633, + "sigops": 1, + "fee": 4000, + "status": { + "confirmed": true, + "block_height": 848136, + "block_hash": "00000000000000000002c69c7a3010fcd596c0c7451c23e7cd1f5e19ebf8ee6d", + "block_time": 1718517071 + } } ] \ No newline at end of file diff --git a/backend/src/__tests__/gbt/gbt-tests.ts b/backend/src/__tests__/gbt/gbt-tests.ts index 8a3995f71..561963aaa 100644 --- a/backend/src/__tests__/gbt/gbt-tests.ts +++ b/backend/src/__tests__/gbt/gbt-tests.ts @@ -13,7 +13,7 @@ const vectorBuffer: Buffer = fs.readFileSync(path.join(__dirname, './', './test- describe('Rust GBT', () => { test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => { - const rustGbt = new GbtGenerator(); + const rustGbt = new GbtGenerator(4_000_000, 8); const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer); const result = await rustGbt.make(mempool, [], maxUid); diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index c3304b432..0724065ae 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -107,8 +107,14 @@ class BitcoinApi implements AbstractBitcoinApi { .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx); } - $getTxsForBlock(hash: string): Promise { - throw new Error('Method getTxsForBlock not supported by the Bitcoin RPC API.'); + async $getTxsForBlock(hash: string): Promise { + const verboseBlock: IBitcoinApi.VerboseBlock = await this.bitcoindClient.getBlock(hash, 2); + const transactions: IEsploraApi.Transaction[] = []; + for (const tx of verboseBlock.tx) { + const converted = await this.$convertTransaction(tx, true); + transactions.push(converted); + } + return transactions; } $getRawBlock(hash: string): Promise { diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 2c9338814..04f380418 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -258,9 +258,15 @@ export class Common { let opreturnCount = 0; for (const vout of tx.vout) { // scriptpubkey - if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) { + if (['nonstandard', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) { // (non-standard output type) return true; + } else if (vout.scriptpubkey_type === 'unknown') { + // undefined segwit version/length combinations are actually standard in outputs + // https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/interpreter.cpp#L1950-L1951 + if (vout.scriptpubkey.startsWith('00') || !this.isWitnessProgram(vout.scriptpubkey)) { + return true; + } } else if (vout.scriptpubkey_type === 'multisig') { if (!DEFAULT_PERMIT_BAREMULTISIG) { // bare-multisig @@ -308,6 +314,27 @@ export class Common { return false; } + // A witness program is any valid scriptpubkey that consists of a 1-byte push opcode + // followed by a data push between 2 and 40 bytes. + // https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240 + static isWitnessProgram(scriptpubkey: string): false | { version: number, program: string } { + if (scriptpubkey.length < 8 || scriptpubkey.length > 84) { + return false; + } + const version = parseInt(scriptpubkey.slice(0,2), 16); + if (version !== 0 && version < 0x51 || version > 0x60) { + return false; + } + const push = parseInt(scriptpubkey.slice(2,4), 16); + if (push + 2 === (scriptpubkey.length / 2)) { + return { + version: version ? version - 0x50 : 0, + program: scriptpubkey.slice(4), + }; + } + return false; + } + static getNonWitnessSize(tx: TransactionExtended): number { let weight = tx.weight; let hasWitness = false; @@ -877,9 +904,10 @@ export class Common { let medianFee = 0; let medianWeight = 0; - // calculate the "medianFee" as the average fee rate of the middle 10000 weight units of transactions - const leftBound = 1995000; - const rightBound = 2005000; + // calculate the "medianFee" as the average fee rate of the middle 0.25% weight units of transactions + const halfWidth = config.MEMPOOL.BLOCK_WEIGHT_UNITS / 800; + const leftBound = Math.floor((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) - halfWidth); + const rightBound = Math.ceil((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) + halfWidth); for (let i = 0; i < sortedTxs.length && weightCount < rightBound; i++) { const left = weightCount; const right = weightCount + sortedTxs[i].weight; diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 55f613c0d..3c1c434bd 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -16,7 +16,7 @@ class MempoolBlocks { private mempoolBlockDeltas: MempoolBlockDelta[] = []; private txSelectionWorker: Worker | null = null; private rustInitialized: boolean = false; - private rustGbtGenerator: GbtGenerator = new GbtGenerator(); + private rustGbtGenerator: GbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT); private nextUid: number = 1; private uidMap: Map = new Map(); // map short numerical uids to full txids @@ -230,7 +230,7 @@ class MempoolBlocks { private resetRustGbt(): void { this.rustInitialized = false; - this.rustGbtGenerator = new GbtGenerator(); + this.rustGbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT); } public async $rustMakeBlockTemplates(txids: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise { @@ -262,7 +262,7 @@ class MempoolBlocks { }); // run the block construction algorithm in a separate thread, and wait for a result - const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator(); + const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT); try { const { blocks, blockWeights, rates, clusters, overflow } = this.convertNapiResultTxids( await rustGbt.make(transactions as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid), diff --git a/backend/src/api/services/acceleration.ts b/backend/src/api/services/acceleration.ts index eba5ab7e6..7debe7119 100644 --- a/backend/src/api/services/acceleration.ts +++ b/backend/src/api/services/acceleration.ts @@ -31,10 +31,7 @@ export interface AccelerationHistory { feeDelta: number, blockHash: string, blockHeight: number, - pools: { - pool_unique_id: number, - username: string, - }[], + pools: number[]; }; class AccelerationApi { diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index dc17f1b3f..3c8c06ecf 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -1300,7 +1300,7 @@ class WebsocketHandler { // and zips it together into a valid JSON object private serializeResponse(response): string { return '{' - + Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ') + + Object.keys(response).filter(key => response[key] != null).map(key => `"${key}": ${response[key]}`).join(', ') + '}'; } diff --git a/backend/src/repositories/AccelerationRepository.ts b/backend/src/repositories/AccelerationRepository.ts index eea60fcff..0da66228c 100644 --- a/backend/src/repositories/AccelerationRepository.ts +++ b/backend/src/repositories/AccelerationRepository.ts @@ -308,10 +308,10 @@ class AccelerationRepository { } const accelerationSummaries = accelerations.map(acc => ({ ...acc, - pools: acc.pools.map(pool => pool.pool_unique_id), + pools: acc.pools, })) for (const acc of accelerations) { - if (blockTxs[acc.txid] && acc.pools.some(pool => pool.pool_unique_id === block.extras.pool.id)) { + if (blockTxs[acc.txid] && acc.pools.includes(block.extras.pool.id)) { const tx = blockTxs[acc.txid]; const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions); accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost)); diff --git a/backend/src/tasks/price-feeds/free-currency-api.ts b/backend/src/tasks/price-feeds/free-currency-api.ts index 2a21a16d2..48e511aa8 100644 --- a/backend/src/tasks/price-feeds/free-currency-api.ts +++ b/backend/src/tasks/price-feeds/free-currency-api.ts @@ -76,7 +76,7 @@ class FreeCurrencyApi implements ConversionFeed { } public async $fetchConversionRates(date: string): Promise { - const response = await query(`${this.API_URL_PREFIX}historical?date=${date}&apikey=${this.API_KEY}`); + const response = await query(`${this.API_URL_PREFIX}historical?date=${date}&apikey=${this.API_KEY}`, true); if (response && response['data'] && (response['data'][date] || this.PAID)) { if (this.PAID) { response['data'] = this.convertData(response['data']); diff --git a/backend/src/tasks/price-updater.ts b/backend/src/tasks/price-updater.ts index 16fe713b7..467669a6f 100644 --- a/backend/src/tasks/price-updater.ts +++ b/backend/src/tasks/price-updater.ts @@ -59,7 +59,7 @@ class PriceUpdater { private currencyConversionFeed: ConversionFeed | undefined; private newCurrencies: string[] = ['BGN', 'BRL', 'CNY', 'CZK', 'DKK', 'HKD', 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'ZAR']; private lastTimeConversionsRatesFetched: number = 0; - private latestConversionsRatesFromFeed: ConversionRates = {}; + private latestConversionsRatesFromFeed: ConversionRates = { USD: -1 }; private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined; constructor() { @@ -157,9 +157,9 @@ class PriceUpdater { try { this.latestConversionsRatesFromFeed = await this.currencyConversionFeed.$fetchLatestConversionRates(); this.lastTimeConversionsRatesFetched = Math.round(new Date().getTime() / 1000); - logger.debug(`Fetched currencies conversion rates from external API: ${JSON.stringify(this.latestConversionsRatesFromFeed)}`); + logger.debug(`Fetched currencies conversion rates from conversions API: ${JSON.stringify(this.latestConversionsRatesFromFeed)}`); } catch (e) { - logger.err(`Cannot fetch conversion rates from the API. Reason: ${(e instanceof Error ? e.message : e)}`); + logger.err(`Cannot fetch conversion rates from conversions API. Reason: ${(e instanceof Error ? e.message : e)}`); } } @@ -408,17 +408,17 @@ class PriceUpdater { try { const remainingQuota = await this.currencyConversionFeed?.$getQuota(); if (remainingQuota['month']['remaining'] < 500) { // We need some calls left for the daily updates - logger.debug(`Not enough currency API credit to insert missing prices in ${priceTimesToFill.length} rows (${remainingQuota['month']['remaining']} calls left).`, logger.tags.mining); + logger.debug(`Not enough conversions API credit to insert missing prices in ${priceTimesToFill.length} rows (${remainingQuota['month']['remaining']} calls left).`, logger.tags.mining); this.additionalCurrenciesHistoryInserted = true; // Do not try again until next day return; } } catch (e) { - logger.err(`Cannot fetch currency API credit, insertion of missing prices aborted. Reason: ${(e instanceof Error ? e.message : e)}`); + logger.err(`Cannot fetch conversions API credit, insertion of missing prices aborted. Reason: ${(e instanceof Error ? e.message : e)}`); return; } this.additionalCurrenciesHistoryRunning = true; - logger.debug(`Fetching missing conversion rates from external API to fill ${priceTimesToFill.length} rows`, logger.tags.mining); + logger.debug(`Inserting missing historical conversion rates using conversions API to fill ${priceTimesToFill.length} rows`, logger.tags.mining); let conversionRates: { [timestamp: number]: ConversionRates } = {}; let totalInserted = 0; @@ -430,10 +430,23 @@ class PriceUpdater { const month = new Date(priceTime.time * 1000).getMonth(); const yearMonthTimestamp = new Date(year, month, 1).getTime() / 1000; if (conversionRates[yearMonthTimestamp] === undefined) { - conversionRates[yearMonthTimestamp] = await this.currencyConversionFeed?.$fetchConversionRates(`${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01`) || { USD: -1 }; - if (conversionRates[yearMonthTimestamp]['USD'] < 0) { - logger.err(`Cannot fetch conversion rates from the API for ${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01. Aborting insertion of missing prices.`, logger.tags.mining); - this.lastFailedHistoricalRun = Math.round(new Date().getTime() / 1000); + try { + if (year === new Date().getFullYear() && month === new Date().getMonth()) { // For rows in the current month, we use the latest conversion rates + conversionRates[yearMonthTimestamp] = this.latestConversionsRatesFromFeed; + } else { + conversionRates[yearMonthTimestamp] = await this.currencyConversionFeed?.$fetchConversionRates(`${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-15`) || { USD: -1 }; + } + + if (conversionRates[yearMonthTimestamp]['USD'] < 0) { + throw new Error('Incorrect USD conversion rate'); + } + } catch (e) { + if ((e instanceof Error ? e.message : '').includes('429')) { // Continue 60 seconds later if and only if error is 429 + this.lastFailedHistoricalRun = Math.round(new Date().getTime() / 1000); + logger.info(`Got a 429 error from conversions API. This is expected to happen a few times during the initial historical price insertion, process will resume in 60 seconds.`, logger.tags.mining); + } else { + logger.err(`Cannot fetch conversion rates from conversions API for ${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01, trying again next day. Error: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); + } break; } } diff --git a/backend/src/utils/axios-query.ts b/backend/src/utils/axios-query.ts index 0a155fd55..e641d3ce9 100644 --- a/backend/src/utils/axios-query.ts +++ b/backend/src/utils/axios-query.ts @@ -5,7 +5,7 @@ import config from '../config'; import logger from '../logger'; import * as https from 'https'; -export async function query(path): Promise { +export async function query(path, throwOnFail: boolean = false): Promise { type axiosOptions = { headers: { 'User-Agent': string @@ -21,6 +21,7 @@ export async function query(path): Promise { timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000 }; let retry = 0; + let lastError: any = null; while (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) { try { @@ -50,6 +51,7 @@ export async function query(path): Promise { } return data.data; } catch (e) { + lastError = e; logger.warn(`Could not connect to ${path} (Attempt ${retry + 1}/${config.MEMPOOL.EXTERNAL_MAX_RETRY}). Reason: ` + (e instanceof Error ? e.message : e)); retry++; } @@ -59,5 +61,10 @@ export async function query(path): Promise { } logger.err(`Could not connect to ${path}. All ${config.MEMPOOL.EXTERNAL_MAX_RETRY} attempts failed`); + + if (throwOnFail && lastError) { + throw lastError; + } + return undefined; } diff --git a/contributors/mackalex.txt b/contributors/mackalex.txt new file mode 100644 index 000000000..667e31a22 --- /dev/null +++ b/contributors/mackalex.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 June 18th, 2024. + +Signed: mackalex diff --git a/docker/frontend/entrypoint.sh b/docker/frontend/entrypoint.sh index a57c599b4..5e79b262f 100644 --- a/docker/frontend/entrypoint.sh +++ b/docker/frontend/entrypoint.sh @@ -16,6 +16,7 @@ fi # Runtime overrides - read env vars defined in docker compose +__MAINNET_ENABLED__=${MAINNET_ENABLED:=true} __TESTNET_ENABLED__=${TESTNET_ENABLED:=false} __SIGNET_ENABLED__=${SIGNET_ENABLED:=false} __LIQUID_ENABLED__=${LIQUID_ENABLED:=false} @@ -28,6 +29,7 @@ __NGINX_PORT__=${NGINX_PORT:=8999} __BLOCK_WEIGHT_UNITS__=${BLOCK_WEIGHT_UNITS:=4000000} __MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8} __BASE_MODULE__=${BASE_MODULE:=mempool} +__ROOT_NETWORK__=${ROOT_NETWORK:=} __MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space} __LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network} __MINING_DASHBOARD__=${MINING_DASHBOARD:=true} @@ -42,6 +44,7 @@ __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} # Export as environment variables to be used by envsubst +export __MAINNET_ENABLED__ export __TESTNET_ENABLED__ export __SIGNET_ENABLED__ export __LIQUID_ENABLED__ @@ -54,6 +57,7 @@ export __NGINX_PORT__ export __BLOCK_WEIGHT_UNITS__ export __MEMPOOL_BLOCKS_AMOUNT__ export __BASE_MODULE__ +export __ROOT_NETWORK__ export __MEMPOOL_WEBSITE_URL__ export __LIQUID_WEBSITE_URL__ export __MINING_DASHBOARD__ diff --git a/frontend/cypress/e2e/liquid/liquid.spec.ts b/frontend/cypress/e2e/liquid/liquid.spec.ts index b355af0d2..c7d2a92ee 100644 --- a/frontend/cypress/e2e/liquid/liquid.spec.ts +++ b/frontend/cypress/e2e/liquid/liquid.spec.ts @@ -72,20 +72,6 @@ describe('Liquid', () => { }); }); - it('renders unconfidential addresses correctly on mobile', () => { - cy.viewport('iphone-6'); - cy.visit(`${basePath}/address/ex1qqmmjdwrlg59c8q4l75sj6wedjx57tj5grt8pat`); - cy.waitForSkeletonGone(); - //TODO: Add proper IDs for these selectors - const firstRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(1) > .table > tbody'; - const thirdRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(3)'; - cy.get(firstRowSelector).invoke('css', 'width').then(firstRowWidth => { - cy.get(thirdRowSelector).invoke('css', 'width').then(thirdRowWidth => { - expect(parseInt(firstRowWidth)).to.be.lessThan(parseInt(thirdRowWidth)); - }); - }); - }); - describe('peg in/peg out', () => { it('loads peg in addresses', () => { cy.visit(`${basePath}/tx/fe764f7bedfc2a37b29d9c8aef67d64a57d253a6b11c5a55555cfd5826483a58`); diff --git a/frontend/mempool-frontend-config.sample.json b/frontend/mempool-frontend-config.sample.json index c111f35af..cafedd63f 100644 --- a/frontend/mempool-frontend-config.sample.json +++ b/frontend/mempool-frontend-config.sample.json @@ -4,6 +4,7 @@ "SIGNET_ENABLED": false, "LIQUID_ENABLED": false, "LIQUID_TESTNET_ENABLED": false, + "MAINNET_ENABLED": true, "ITEMS_PER_PAGE": 10, "KEEP_BLOCKS_AMOUNT": 8, "NGINX_PROTOCOL": "http", @@ -12,6 +13,7 @@ "BLOCK_WEIGHT_UNITS": 4000000, "MEMPOOL_BLOCKS_AMOUNT": 8, "BASE_MODULE": "mempool", + "ROOT_NETWORK": "", "MEMPOOL_WEBSITE_URL": "https://mempool.space", "LIQUID_WEBSITE_URL": "https://liquid.network", "MINING_DASHBOARD": true, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e790f7ba0..effc6874a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,7 +32,6 @@ "bootstrap": "~4.6.2", "browserify": "^17.0.0", "clipboard": "^2.0.11", - "cypress": "^13.11.0", "domino": "^2.1.6", "echarts": "~5.5.0", "esbuild": "^0.21.1", @@ -63,7 +62,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.11.0", + "cypress": "^13.12.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", @@ -6106,11 +6105,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -8029,9 +8028,9 @@ "peer": true }, "node_modules/cypress": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.11.0.tgz", - "integrity": "sha512-NXXogbAxVlVje4XHX+Cx5eMFZv4Dho/2rIcdBHg9CNPFUGZdM4cRdgIgM7USmNYsC12XY0bZENEQ+KBk72fl+A==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.12.0.tgz", + "integrity": "sha512-udzS2JilmI9ApO/UuqurEwOvThclin5ntz7K0BtnHBs+tg2Bl9QShLISXpSEMDv/u8b6mqdoAdyKeZiSqKWL8g==", "hasInstallScript": true, "optional": true, "dependencies": { @@ -10152,9 +10151,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -22636,11 +22635,11 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "brorand": { @@ -24112,9 +24111,9 @@ "peer": true }, "cypress": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.11.0.tgz", - "integrity": "sha512-NXXogbAxVlVje4XHX+Cx5eMFZv4Dho/2rIcdBHg9CNPFUGZdM4cRdgIgM7USmNYsC12XY0bZENEQ+KBk72fl+A==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.12.0.tgz", + "integrity": "sha512-udzS2JilmI9ApO/UuqurEwOvThclin5ntz7K0BtnHBs+tg2Bl9QShLISXpSEMDv/u8b6mqdoAdyKeZiSqKWL8g==", "optional": true, "requires": { "@cypress/request": "^3.0.0", @@ -25757,9 +25756,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "requires": { "to-regex-range": "^5.0.1" } diff --git a/frontend/package.json b/frontend/package.json index f94dd8759..1a54c930d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -115,7 +115,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.11.0", + "cypress": "^13.12.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", diff --git a/frontend/proxy.conf.local-esplora.js b/frontend/proxy.conf.local-esplora.js index 0cdc9d459..905910294 100644 --- a/frontend/proxy.conf.local-esplora.js +++ b/frontend/proxy.conf.local-esplora.js @@ -78,6 +78,18 @@ PROXY_CONFIG.push(...[ "^/testnet": "" }, }, + /* Optional proxy to route dev to official acceleration services + { + context: ['/api/v1/services/accelerator/**'], + target: `https://mempool.space/api/v1/services/accelerator/`, + secure: false, + changeOrigin: true, + proxyTimeout: 30000, + pathRewrite: { + "^/api/v1/services/accelerator": "" + }, + }, + */ { context: ['/api/v1/services/**'], target: `http://localhost:9000`, diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index c0d39bbc8..7d1396145 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -189,7 +189,7 @@ - + @@ -201,7 +201,7 @@ @@ -214,7 +214,7 @@ @@ -355,7 +355,7 @@ @@ -369,7 +369,7 @@
- + {{ contributor.name }} @@ -381,7 +381,7 @@
- + {{ contributor.name }} diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index d1c15f838..aecff02fc 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -10,9 +10,6 @@ margin: 25px; line-height: 32px; } - .unknown { - border: 1px solid #b4b4b4; - } .image.not-rounded { border-radius: 0; 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 5f9017bbd..c003fe5ca 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 @@ -96,10 +96,16 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy { share(), ); - this.minedAccelerations$ = this.accelerations$.pipe( - map(accelerations => { - return accelerations.filter(acc => ['completed_provisional', 'completed'].includes(acc.status)); - }) + this.minedAccelerations$ = this.stateService.chainTip$.pipe( + distinctUntilChanged(), + switchMap(() => { + return this.serviceApiServices.getAccelerationHistory$({ status: 'completed', pageLength: 6 }).pipe( + catchError(() => { + return of([]); + }), + ); + }), + share(), ); this.blocks$ = combineLatest([ diff --git a/frontend/src/app/components/address-labels/address-labels.component.html b/frontend/src/app/components/address-labels/address-labels.component.html index dfc6647f4..b055cf606 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.html +++ b/frontend/src/app/components/address-labels/address-labels.component.html @@ -4,7 +4,7 @@ {{ label }}
@@ -15,6 +15,6 @@ {{ label }} \ No newline at end of file diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index 2365c167f..72a58bfca 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -1,7 +1,7 @@ import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; import { Vin, Vout } from '../../interfaces/electrs.interface'; import { StateService } from '../../services/state.service'; -import { parseMultisigScript } from '../../bitcoin.utils'; +import { AddressType, AddressTypeInfo } from '../../shared/address-utils'; @Component({ selector: 'app-address-labels', @@ -12,9 +12,11 @@ import { parseMultisigScript } from '../../bitcoin.utils'; export class AddressLabelsComponent implements OnChanges { network = ''; + @Input() address: AddressTypeInfo; @Input() vin: Vin; @Input() vout: Vout; @Input() channel: any; + @Input() class: string = ''; label?: string; @@ -27,10 +29,10 @@ export class AddressLabelsComponent implements OnChanges { ngOnChanges() { if (this.channel) { this.handleChannel(); + } else if (this.address) { + this.handleAddress(); } else if (this.vin) { this.handleVin(); - } else if (this.vout) { - this.handleVout(); } } @@ -41,74 +43,22 @@ export class AddressLabelsComponent implements OnChanges { this.label = `Channel ${type}: ${leftNodeName} <> ${rightNodeName}`; } + handleAddress() { + if (this.address?.scripts.size) { + const script = this.address?.scripts.values().next().value; + if (script.template?.label) { + this.label = script.template.label; + } + } + } + handleVin() { - if (this.vin.inner_witnessscript_asm) { - if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0 || this.vin.inner_witnessscript_asm.indexOf('OP_PUSHNUM_15 OP_CHECKMULTISIG OP_IFDUP OP_NOTIF OP_PUSHBYTES_2') === 1259) { - if (this.vin.witness.length > 11) { - this.label = 'Liquid Peg Out'; - } else { - this.label = 'Emergency Liquid Peg Out'; - } - return; + const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]) + if (address?.scripts.size) { + const script = address?.scripts.values().next().value; + if (script.template?.label) { + this.label = script.template.label; } - - const topElement = this.vin.witness[this.vin.witness.length - 2]; - if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(this.vin.inner_witnessscript_asm)) { - // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs - if (topElement === '01') { - // top element is '01' to get in the revocation path - this.label = 'Revoked Lightning Force Close'; - } else { - // top element is '', this is a delayed to_local output - this.label = 'Lightning Force Close'; - } - return; - } else if ( - /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm) || - /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm) - ) { - // https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs - // https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs - if (topElement.length === 66) { - // top element is a public key - this.label = 'Revoked Lightning HTLC'; - } else if (topElement) { - // top element is a preimage - this.label = 'Lightning HTLC'; - } else { - // top element is '' to get in the expiry of the script - this.label = 'Expired Lightning HTLC'; - } - return; - } else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) { - // https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors - if (topElement) { - // top element is a signature - this.label = 'Lightning Anchor'; - } else { - // top element is '', it has been swept after 16 blocks - this.label = 'Swept Lightning Anchor'; - } - return; - } - - this.detectMultisig(this.vin.inner_witnessscript_asm); } - - this.detectMultisig(this.vin.inner_redeemscript_asm); - - this.detectMultisig(this.vin.prevout.scriptpubkey_asm); - } - - detectMultisig(script: string) { - const ms = parseMultisigScript(script); - - if (ms) { - this.label = $localize`:@@address-label.multisig:Multisig ${ms.m}:multisigM: of ${ms.n}:multisigN:`; - } - } - - handleVout() { - this.detectMultisig(this.vout.scriptpubkey_asm); } } diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 26e5d8203..f0e9012c2 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -3,7 +3,13 @@

Address

@@ -14,40 +20,47 @@
-
- - - - - - - - - - - - - - - - - - - - - -
Unconfidential - - - -
Total received
Total sent
Balance
-
-
-
-
- + @if (isMobile) { +
+ + + + + + + @if (network === 'liquid' || network === 'liquidtestnet') { + + } @else { + + } + + +
-
+ } @else { +
+ + + + + @if (network === 'liquid' || network === 'liquidtestnet') { + + } @else { + + } + +
+
+
+ + + + + + +
+
+ }
@@ -76,8 +89,8 @@

  - {{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transaction - {{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transactions + {{ (transactions?.length | number) || '?' }} of {{ mempoolStats.tx_count + chainStats.tx_count | number }} transaction + {{ (transactions?.length | number) || '?' }} of {{ mempoolStats.tx_count + chainStats.tx_count | number }} transactions

@@ -182,3 +195,57 @@
+ + + + + Balance + + + + + + + unconfirmed balance + + + + + + + UTXOs + {{ chainStats.utxos }} + + + + + + unconfirmed UTXOs + {{ mempoolStats.utxos > 0 ? '+' : ''}}{{ mempoolStats.utxos }} + + + + + + Volume + + + + + + + Type + + + + + + + Unconfidential + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/components/address/address.component.scss b/frontend/src/app/components/address/address.component.scss index da615376c..8e04ffe8b 100644 --- a/frontend/src/app/components/address/address.component.scss +++ b/frontend/src/app/components/address/address.component.scss @@ -1,16 +1,14 @@ .qr-wrapper { + position: absolute; + top: 30px; + right: 0px; + border: solid 10px var(--active-bg); + border-radius: 5px; background-color: #fff; padding: 10px; padding-bottom: 5px; - display: inline-block; -} - -.qrcode-col { - margin: 20px auto 10px; - text-align: center; - @media (min-width: 992px){ - margin: 0px auto 0px; - } + display: block; + z-index: 99; } .fiat { @@ -25,10 +23,14 @@ tr td { &:last-child { text-align: right; - @media (min-width: 576px) { + @media (min-width: 768px) { text-align: left; } } + + &.wrap-cell { + white-space: normal; + } } } @@ -78,10 +80,10 @@ h1 { top: 9px; position: relative; @media (min-width: 576px) { + max-width: calc(100% - 180px); top: 11px; } @media (min-width: 768px) { - max-width: calc(100% - 180px); top: 17px; } } @@ -96,17 +98,6 @@ h1 { .liquid-address { .address-table { table-layout: fixed; - - tr td:first-child { - width: 170px; - } - tr td:last-child { - width: 80%; - } - } - - .qrcode-col { - flex-grow: 0.5; } } diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index e79ad45e2..477954805 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -1,8 +1,8 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { ElectrsApiService } from '../../services/electrs-api.service'; import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; -import { Address, ScriptHash, Transaction } from '../../interfaces/electrs.interface'; +import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface'; import { WebsocketService } from '../../services/websocket.service'; import { StateService } from '../../services/state.service'; import { AudioService } from '../../services/audio.service'; @@ -11,6 +11,83 @@ import { of, merge, Subscription, Observable } from 'rxjs'; import { SeoService } from '../../services/seo.service'; import { seoDescriptionNetwork } from '../../shared/common.utils'; import { AddressInformation } from '../../interfaces/node-api.interface'; +import { AddressTypeInfo } from '../../shared/address-utils'; + +class AddressStats implements ChainStats { + address: string; + scriptpubkey?: string; + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; + + constructor (stats: ChainStats, address: string, scriptpubkey?: string) { + Object.assign(this, stats); + this.address = address; + this.scriptpubkey = scriptpubkey; + } + + public addTx(tx: Transaction): void { + for (const vin of tx.vin) { + if (vin.prevout?.scriptpubkey_address === this.address || (this.scriptpubkey === vin.prevout?.scriptpubkey)) { + this.spendTxo(vin.prevout.value); + } + } + for (const vout of tx.vout) { + if (vout.scriptpubkey_address === this.address || (this.scriptpubkey === vout.scriptpubkey)) { + this.fundTxo(vout.value); + } + } + this.tx_count++; + } + + public removeTx(tx: Transaction): void { + for (const vin of tx.vin) { + if (vin.prevout?.scriptpubkey_address === this.address || (this.scriptpubkey === vin.prevout?.scriptpubkey)) { + this.unspendTxo(vin.prevout.value); + } + } + for (const vout of tx.vout) { + if (vout.scriptpubkey_address === this.address || (this.scriptpubkey === vout.scriptpubkey)) { + this.unfundTxo(vout.value); + } + } + this.tx_count--; + } + + private fundTxo(value: number): void { + this.funded_txo_sum += value; + this.funded_txo_count++; + } + + private unfundTxo(value: number): void { + this.funded_txo_sum -= value; + this.funded_txo_count--; + } + + private spendTxo(value: number): void { + this.spent_txo_sum += value; + this.spent_txo_count++; + } + + private unspendTxo(value: number): void { + this.spent_txo_sum -= value; + this.spent_txo_count--; + } + + get balance(): number { + return this.funded_txo_sum - this.spent_txo_sum; + } + + get volume(): number { + return this.funded_txo_sum + this.spent_txo_sum; + } + + get utxos(): number { + return this.funded_txo_count - this.spent_txo_count; + } +} @Component({ selector: 'app-address', @@ -20,6 +97,9 @@ import { AddressInformation } from '../../interfaces/node-api.interface'; export class AddressComponent implements OnInit, OnDestroy { network = ''; + isMobile: boolean; + showQR: boolean = false; + address: Address; addressString: string; isLoadingAddress = true; @@ -33,11 +113,14 @@ export class AddressComponent implements OnInit, OnDestroy { blockTxSubscription: Subscription; addressLoadingStatus$: Observable; addressInfo: null | AddressInformation = null; + addressTypeInfo: null | AddressTypeInfo; fullyLoaded = false; - txCount = 0; - received = 0; - sent = 0; + chainStats: AddressStats; + mempoolStats: AddressStats; + + exampleChannel?: any; + now = Date.now() / 1000; balancePeriod: 'all' | '1m' = 'all'; @@ -55,10 +138,12 @@ export class AddressComponent implements OnInit, OnDestroy { private seoService: SeoService, ) { } - ngOnInit() { + ngOnInit(): void { this.stateService.networkChanged$.subscribe((network) => this.network = network); this.websocketService.want(['blocks']); + this.onResize(); + this.addressLoadingStatus$ = this.route.paramMap .pipe( switchMap(() => this.stateService.loadingIndicators$), @@ -75,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy { this.isLoadingTransactions = true; this.transactions = null; this.addressInfo = null; + this.exampleChannel = null; document.body.scrollTo(0, 0); this.addressString = params.get('id') || ''; if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) { @@ -83,6 +169,8 @@ export class AddressComponent implements OnInit, OnDestroy { this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`); + this.addressTypeInfo = new AddressTypeInfo(this.stateService.network || 'mainnet', this.addressString); + return merge( of(true), this.stateService.connectionState$ @@ -175,11 +263,19 @@ export class AddressComponent implements OnInit, OnDestroy { }); this.transactions = this.tempTransactions; - if (this.transactions.length === this.txCount) { + if (this.transactions.length === (this.mempoolStats.tx_count + this.chainStats.tx_count)) { this.fullyLoaded = true; } this.isLoadingTransactions = false; + let addressVin: Vin[] = []; + for (const tx of this.transactions) { + addressVin = addressVin.concat(tx.vin.filter(v => v.prevout?.scriptpubkey_address === this.address.address)); + } + this.addressTypeInfo.processInputs(addressVin); + // hack to trigger change detection + this.addressTypeInfo = this.addressTypeInfo.clone(); + if (!this.showBalancePeriod()) { this.setBalancePeriod('all'); } else { @@ -196,11 +292,13 @@ export class AddressComponent implements OnInit, OnDestroy { this.mempoolTxSubscription = this.stateService.mempoolTransactions$ .subscribe(tx => { this.addTransaction(tx); + this.mempoolStats.addTx(tx); }); this.mempoolRemovedTxSubscription = this.stateService.mempoolRemovedTransactions$ .subscribe(tx => { this.removeTransaction(tx); + this.mempoolStats.removeTx(tx); }); this.blockTxSubscription = this.stateService.blockTransactions$ @@ -209,12 +307,14 @@ export class AddressComponent implements OnInit, OnDestroy { if (tx) { tx.status = transaction.status; this.transactions = this.transactions.slice(); + this.mempoolStats.removeTx(transaction); this.audioService.playSound('magic'); } else { if (this.addTransaction(transaction, false)) { this.audioService.playSound('magic'); } } + this.chainStats.addTx(transaction); }); } @@ -225,7 +325,6 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions.unshift(transaction); this.transactions = this.transactions.slice(); - this.txCount++; if (playSound) { if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) { @@ -235,17 +334,6 @@ export class AddressComponent implements OnInit, OnDestroy { } } - transaction.vin.forEach((vin) => { - if (vin?.prevout?.scriptpubkey_address === this.address.address) { - this.sent += vin.prevout.value; - } - }); - transaction.vout.forEach((vout) => { - if (vout?.scriptpubkey_address === this.address.address) { - this.received += vout.value; - } - }); - return true; } @@ -257,23 +345,11 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions.splice(index, 1); this.transactions = this.transactions.slice(); - this.txCount--; - - transaction.vin.forEach((vin) => { - if (vin?.prevout?.scriptpubkey_address === this.address.address) { - this.sent -= vin.prevout.value; - } - }); - transaction.vout.forEach((vout) => { - if (vout?.scriptpubkey_address === this.address.address) { - this.received -= vout.value; - } - }); return true; } - loadMore() { + loadMore(): void { if (this.isLoadingTransactions || this.fullyLoaded) { return; } @@ -301,10 +377,9 @@ export class AddressComponent implements OnInit, OnDestroy { }); } - updateChainStats() { - this.received = this.address.chain_stats.funded_txo_sum + this.address.mempool_stats.funded_txo_sum; - this.sent = this.address.chain_stats.spent_txo_sum + this.address.mempool_stats.spent_txo_sum; - this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count; + updateChainStats(): void { + this.chainStats = new AddressStats(this.address.chain_stats, this.address.address); + this.mempoolStats = new AddressStats(this.address.mempool_stats, this.address.address); } setBalancePeriod(period: 'all' | '1m'): boolean { @@ -319,7 +394,12 @@ export class AddressComponent implements OnInit, OnDestroy { ); } - ngOnDestroy() { + @HostListener('window:resize', ['$event']) + onResize(): void { + this.isMobile = window.innerWidth < 768; + } + + ngOnDestroy(): void { this.mainSubscription.unsubscribe(); this.mempoolTxSubscription.unsubscribe(); this.mempoolRemovedTxSubscription.unsubscribe(); diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index c83459375..6c8693d22 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -411,7 +411,7 @@ - + + {{ blockAudit.feeDelta < 0 ? '+' : '' }}{{ (-blockAudit.feeDelta * 100) | amountShortener: 2 }}% diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index d762a879e..01702487f 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -8,7 +8,7 @@ import { StateService } from '../../services/state.service'; import { SeoService } from '../../services/seo.service'; import { WebsocketService } from '../../services/websocket.service'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; +import { Acceleration, BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; import { ApiService } from '../../services/api.service'; import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; import { detectWebGL } from '../../shared/graphs.utils'; @@ -44,6 +44,7 @@ export class BlockComponent implements OnInit, OnDestroy { latestBlocks: BlockExtended[] = []; oobFees: number = 0; strippedTransactions: TransactionStripped[]; + accelerations: Acceleration[]; overviewTransitionDirection: string; isLoadingOverview = true; error: any; @@ -68,6 +69,7 @@ export class BlockComponent implements OnInit, OnDestroy { mode: 'projected' | 'actual' = 'projected'; overviewSubscription: Subscription; + accelerationsSubscription: Subscription; keyNavigationSubscription: Subscription; blocksSubscription: Subscription; cacheBlocksSubscription: Subscription; @@ -183,6 +185,9 @@ export class BlockComponent implements OnInit, OnDestroy { } else { this.isLoadingBlock = true; this.isLoadingOverview = true; + this.strippedTransactions = undefined; + this.blockAudit = undefined; + this.accelerations = undefined; let blockInCache: BlockExtended; if (isBlockHeight) { @@ -294,158 +299,36 @@ export class BlockComponent implements OnInit, OnDestroy { this.overviewError = err; return of(null); }) - ), - this.stateService.env.ACCELERATOR === true && block.height > 819500 - ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) - .pipe(catchError(() => { - return of([]); - })) - : of([]) + ) ]); }) ) - .subscribe(([transactions, blockAudit, accelerations]) => { + .subscribe(([transactions, blockAudit]) => { if (transactions) { this.strippedTransactions = transactions; } else { this.strippedTransactions = []; } + this.blockAudit = blockAudit; - const acceleratedInBlock = {}; - for (const acc of accelerations) { - if (acc.pools?.some(pool => pool === this.block?.extras?.pool.id || pool?.['pool_unique_id'] === this.block?.extras?.pool.id)) { - acceleratedInBlock[acc.txid] = acc; - } - } - for (const tx of transactions) { - if (acceleratedInBlock[tx.txid]) { - tx.acc = true; - const acceleration = acceleratedInBlock[tx.txid]; - const boostCost = acceleration.boostCost || acceleration.bidBoost; - const acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; - if (acceleratedFeeRate > tx.rate) { - tx.rate = acceleratedFeeRate; - } - } else { - tx.acc = false; - } - } - - this.blockAudit = null; - if (transactions && blockAudit) { - const inTemplate = {}; - const inBlock = {}; - const isAdded = {}; - const isPrioritized = {}; - const isCensored = {}; - const isMissing = {}; - const isSelected = {}; - const isFresh = {}; - const isSigop = {}; - const isRbf = {}; - const isAccelerated = {}; - this.numMissing = 0; - this.numUnexpected = 0; - - if (blockAudit?.template) { - for (const tx of blockAudit.template) { - inTemplate[tx.txid] = true; - if (tx.acc) { - isAccelerated[tx.txid] = true; - } - } - for (const tx of transactions) { - inBlock[tx.txid] = true; - } - for (const txid of blockAudit.addedTxs) { - isAdded[txid] = true; - } - for (const txid of blockAudit.prioritizedTxs || []) { - isPrioritized[txid] = true; - } - for (const txid of blockAudit.missingTxs) { - isCensored[txid] = true; - } - for (const txid of blockAudit.freshTxs || []) { - isFresh[txid] = true; - } - for (const txid of blockAudit.sigopTxs || []) { - isSigop[txid] = true; - } - for (const txid of blockAudit.fullrbfTxs || []) { - isRbf[txid] = true; - } - for (const txid of blockAudit.acceleratedTxs || []) { - isAccelerated[txid] = true; - } - // set transaction statuses - for (const tx of blockAudit.template) { - tx.context = 'projected'; - if (isCensored[tx.txid]) { - tx.status = 'censored'; - } else if (inBlock[tx.txid]) { - tx.status = 'found'; - } else { - if (isFresh[tx.txid]) { - if (tx.rate - (tx.fee / tx.vsize) >= 0.1) { - tx.status = 'freshcpfp'; - } else { - tx.status = 'fresh'; - } - } else if (isSigop[tx.txid]) { - tx.status = 'sigop'; - } else if (isRbf[tx.txid]) { - tx.status = 'rbf'; - } else { - tx.status = 'missing'; - } - isMissing[tx.txid] = true; - this.numMissing++; - } - if (isAccelerated[tx.txid]) { - tx.status = 'accelerated'; - } - } - for (const [index, tx] of transactions.entries()) { - tx.context = 'actual'; - if (index === 0) { - tx.status = null; - } else if (isAdded[tx.txid]) { - tx.status = 'added'; - } else if (isPrioritized[tx.txid]) { - tx.status = 'prioritized'; - } else if (inTemplate[tx.txid]) { - tx.status = 'found'; - } else if (isRbf[tx.txid]) { - tx.status = 'rbf'; - } else { - tx.status = 'selected'; - isSelected[tx.txid] = true; - this.numUnexpected++; - } - if (isAccelerated[tx.txid]) { - tx.status = 'accelerated'; - } - } - for (const tx of transactions) { - inBlock[tx.txid] = true; - } - - blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - (this.block?.extras.totalFees + this.oobFees)) / blockAudit.expectedFees : 0; - blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block?.weight) / blockAudit.expectedWeight : 0; - blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block?.tx_count) / blockAudit.template.length : 0; - this.blockAudit = blockAudit; - this.setAuditAvailable(true); - } else { - this.setAuditAvailable(false); - } - } else { - this.setAuditAvailable(false); - } - + this.setupBlockAudit(); this.isLoadingOverview = false; - this.setupBlockGraphs(); - this.cd.markForCheck(); + }); + + this.accelerationsSubscription = this.block$.pipe( + switchMap((block) => { + return this.stateService.env.ACCELERATOR === true && block.height > 819500 + ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) + .pipe(catchError(() => { + return of([]); + })) + : of([]); + }) + ).subscribe((accelerations) => { + this.accelerations = accelerations; + if (accelerations.length) { + this.setupBlockAudit(); + } }); this.oobSubscription = this.block$.pipe( @@ -609,6 +492,147 @@ export class BlockComponent implements OnInit, OnDestroy { } } + setupBlockAudit(): void { + const transactions = this.strippedTransactions || []; + const blockAudit = this.blockAudit; + const accelerations = this.accelerations || []; + + const acceleratedInBlock = {}; + for (const acc of accelerations) { + if (acc.pools?.some(pool => pool === this.block?.extras?.pool.id)) { + acceleratedInBlock[acc.txid] = acc; + } + } + + for (const tx of transactions) { + if (acceleratedInBlock[tx.txid]) { + tx.acc = true; + const acceleration = acceleratedInBlock[tx.txid]; + const boostCost = acceleration.boostCost || acceleration.bidBoost; + const acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; + if (acceleratedFeeRate > tx.rate) { + tx.rate = acceleratedFeeRate; + } + } else { + tx.acc = false; + } + } + + if (transactions && blockAudit) { + const inTemplate = {}; + const inBlock = {}; + const isAdded = {}; + const isPrioritized = {}; + const isCensored = {}; + const isMissing = {}; + const isSelected = {}; + const isFresh = {}; + const isSigop = {}; + const isRbf = {}; + const isAccelerated = {}; + this.numMissing = 0; + this.numUnexpected = 0; + + if (blockAudit?.template) { + for (const tx of blockAudit.template) { + inTemplate[tx.txid] = true; + if (tx.acc) { + isAccelerated[tx.txid] = true; + } + } + for (const tx of transactions) { + inBlock[tx.txid] = true; + } + for (const txid of blockAudit.addedTxs) { + isAdded[txid] = true; + } + for (const txid of blockAudit.prioritizedTxs || []) { + isPrioritized[txid] = true; + } + for (const txid of blockAudit.missingTxs) { + isCensored[txid] = true; + } + for (const txid of blockAudit.freshTxs || []) { + isFresh[txid] = true; + } + for (const txid of blockAudit.sigopTxs || []) { + isSigop[txid] = true; + } + for (const txid of blockAudit.fullrbfTxs || []) { + isRbf[txid] = true; + } + for (const txid of blockAudit.acceleratedTxs || []) { + isAccelerated[txid] = true; + } + // set transaction statuses + for (const tx of blockAudit.template) { + tx.context = 'projected'; + if (isCensored[tx.txid]) { + tx.status = 'censored'; + } else if (inBlock[tx.txid]) { + tx.status = 'found'; + } else { + if (isFresh[tx.txid]) { + if (tx.rate - (tx.fee / tx.vsize) >= 0.1) { + tx.status = 'freshcpfp'; + } else { + tx.status = 'fresh'; + } + } else if (isSigop[tx.txid]) { + tx.status = 'sigop'; + } else if (isRbf[tx.txid]) { + tx.status = 'rbf'; + } else { + tx.status = 'missing'; + } + isMissing[tx.txid] = true; + this.numMissing++; + } + if (isAccelerated[tx.txid]) { + tx.status = 'accelerated'; + } + } + for (const [index, tx] of transactions.entries()) { + tx.context = 'actual'; + if (index === 0) { + tx.status = null; + } else if (isAdded[tx.txid]) { + tx.status = 'added'; + } else if (isPrioritized[tx.txid]) { + tx.status = 'prioritized'; + } else if (inTemplate[tx.txid]) { + tx.status = 'found'; + } else if (isRbf[tx.txid]) { + tx.status = 'rbf'; + } else { + tx.status = 'selected'; + isSelected[tx.txid] = true; + this.numUnexpected++; + } + if (isAccelerated[tx.txid]) { + tx.status = 'accelerated'; + } + } + for (const tx of transactions) { + inBlock[tx.txid] = true; + } + + blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - (this.block?.extras.totalFees + this.oobFees)) / blockAudit.expectedFees : 0; + blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block?.weight) / blockAudit.expectedWeight : 0; + blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block?.tx_count) / blockAudit.template.length : 0; + this.blockAudit = blockAudit; + this.setAuditAvailable(true); + } else { + this.setAuditAvailable(false); + } + } else { + this.setAuditAvailable(false); + } + + this.setupBlockGraphs(); + this.cd.markForCheck(); + } + setupBlockGraphs(): void { if (this.blockAudit || this.strippedTransactions) { this.blockGraphProjected.forEach(graph => { diff --git a/frontend/src/app/components/clipboard/clipboard.component.html b/frontend/src/app/components/clipboard/clipboard.component.html index ec8802634..d23ccdf8c 100644 --- a/frontend/src/app/components/clipboard/clipboard.component.html +++ b/frontend/src/app/components/clipboard/clipboard.component.html @@ -1,7 +1,7 @@ @@ -9,7 +9,7 @@ diff --git a/frontend/src/app/components/clipboard/clipboard.component.ts b/frontend/src/app/components/clipboard/clipboard.component.ts index 7fbffdca3..6e577d8b3 100644 --- a/frontend/src/app/components/clipboard/clipboard.component.ts +++ b/frontend/src/app/components/clipboard/clipboard.component.ts @@ -13,11 +13,17 @@ export class ClipboardComponent implements AfterViewInit { @ViewChild('buttonWrapper') buttonWrapper: ElementRef; @Input() button = false; @Input() class = 'btn btn-secondary ml-1'; - @Input() size: 'small' | 'normal' = 'normal'; + @Input() size: 'small' | 'normal' | 'large' = 'normal'; @Input() text: string; @Input() leftPadding = true; copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`; + widths = { + small: '10', + normal: '13', + large: '18', + }; + clipboard: any; constructor() { } diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 8a67d2b4c..591a8730e 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -62,12 +62,12 @@ } -