Merge branch 'master' into mononaut/rbf-tracking-fixes

This commit is contained in:
softsimon 2024-09-13 17:10:12 +08:00 committed by GitHub
commit fc5af24b68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 507 additions and 271 deletions

View File

@ -13,7 +13,7 @@
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
"axios": "~1.7.4", "axios": "1.7.2",
"bitcoinjs-lib": "~6.1.3", "bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.19.2", "express": "~4.19.2",
@ -2278,9 +2278,10 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.4", "version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@ -9439,9 +9440,9 @@
"integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ==" "integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ=="
}, },
"axios": { "axios": {
"version": "1.7.4", "version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"requires": { "requires": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.0",

View File

@ -42,7 +42,7 @@
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
"axios": "~1.7.4", "axios": "1.7.2",
"bitcoinjs-lib": "~6.1.3", "bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.19.2", "express": "~4.19.2",

View File

@ -323,6 +323,7 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v1_taproot': 'v1_p2tr', 'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard', 'nonstandard': 'nonstandard',
'multisig': 'multisig', 'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return' 'nulldata': 'op_return'
}; };

View File

@ -20,6 +20,7 @@ import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository'; import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache'; import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp'; import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
class BitcoinRoutes { class BitcoinRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@ -86,7 +87,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json'); res.set('Content-Type', 'application/json');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -105,13 +106,13 @@ class BitcoinRoutes {
const result = mempoolBlocks.getMempoolBlocks(); const result = mempoolBlocks.getMempoolBlocks();
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private getTransactionTimes(req: Request, res: Response) { private getTransactionTimes(req: Request, res: Response) {
if (!Array.isArray(req.query.txId)) { if (!Array.isArray(req.query.txId)) {
res.status(500).send('Not an array'); handleError(req, res, 500, 'Not an array');
return; return;
} }
const txIds: string[] = []; const txIds: string[] = [];
@ -128,12 +129,12 @@ class BitcoinRoutes {
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> { private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
const txids_csv = req.query.txids; const txids_csv = req.query.txids;
if (!txids_csv || typeof txids_csv !== 'string') { if (!txids_csv || typeof txids_csv !== 'string') {
res.status(500).send('Invalid txids format'); handleError(req, res, 500, 'Invalid txids format');
return; return;
} }
const txids = txids_csv.split(','); const txids = txids_csv.split(',');
if (txids.length > 50) { if (txids.length > 50) {
res.status(400).send('Too many txids requested'); handleError(req, res, 400, 'Too many txids requested');
return; return;
} }
@ -141,13 +142,13 @@ class BitcoinRoutes {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends); res.json(batchedOutspends);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async $getCpfpInfo(req: Request, res: Response) { private async $getCpfpInfo(req: Request, res: Response) {
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
res.status(501).send(`Invalid transaction ID.`); handleError(req, res, 501, `Invalid transaction ID.`);
return; return;
} }
@ -180,7 +181,7 @@ class BitcoinRoutes {
try { try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) { } catch (e) {
res.status(500).send('failed to get CPFP info'); handleError(req, res, 500, 'failed to get CPFP info');
return; return;
} }
} }
@ -209,7 +210,7 @@ class BitcoinRoutes {
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
} }
res.status(statusCode).send(e instanceof Error ? e.message : e); handleError(req, res, statusCode, e instanceof Error ? e.message : e);
} }
} }
@ -223,7 +224,7 @@ class BitcoinRoutes {
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
} }
res.status(statusCode).send(e instanceof Error ? e.message : e); handleError(req, res, statusCode, e instanceof Error ? e.message : e);
} }
} }
@ -284,13 +285,13 @@ class BitcoinRoutes {
// Not modified // Not modified
// 422 Unprocessable Entity // 422 Unprocessable Entity
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
res.status(422).send(`Psbt had no missing nonWitnessUtxos.`); handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`);
} }
} catch (e: any) { } catch (e: any) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
res.status(404).send(e.message); handleError(req, res, 404, e.message);
} else { } else {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }
@ -304,7 +305,7 @@ class BitcoinRoutes {
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
} }
res.status(statusCode).send(e instanceof Error ? e.message : e); handleError(req, res, statusCode, e instanceof Error ? e.message : e);
} }
} }
@ -314,7 +315,7 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -336,7 +337,7 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block); res.json(block);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -346,7 +347,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(blockHeader); res.send(blockHeader);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -357,10 +358,11 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
return res.status(404).send(`audit not available`); handleError(req, res, 404, `audit not available`);
return;
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -371,7 +373,8 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
return res.status(404).send(`transaction audit not available`); handleError(req, res, 404, `transaction audit not available`);
return;
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
@ -388,42 +391,49 @@ class BitcoinRoutes {
return await this.getLegacyBlocks(req, res); return await this.getLegacyBlocks(req, res);
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getBlocksByBulk(req: Request, res: Response) { private async getBlocksByBulk(req: Request, res: Response) {
try { try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
return res.status(404).send(`This API is only available for Bitcoin networks`); handleError(req, res, 404, `This API is only available for Bitcoin networks`);
return;
} }
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) { if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`); handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
return;
} }
if (!Common.indexingEnabled()) { if (!Common.indexingEnabled()) {
return res.status(404).send(`Indexing is required for this API`); handleError(req, res, 404, `Indexing is required for this API`);
return;
} }
const from = parseInt(req.params.from, 10); const from = parseInt(req.params.from, 10);
if (!req.params.from || from < 0) { if (!req.params.from || from < 0) {
return res.status(400).send(`Parameter 'from' must be a block height (integer)`); handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`);
return;
} }
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10); const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
if (to < 0) { if (to < 0) {
return res.status(400).send(`Parameter 'to' must be a block height (integer)`); handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`);
return;
} }
if (from > to) { if (from > to) {
return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`); handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`);
return;
} }
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) { if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`); handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
return;
} }
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocksBetweenHeight(from, to)); res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -458,10 +468,10 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks); res.json(returnBlocks);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getBlockTransactions(req: Request, res: Response) { private async getBlockTransactions(req: Request, res: Response) {
try { try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@ -483,7 +493,7 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -492,13 +502,13 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10)); const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash); res.send(blockHash);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getAddress(req: Request, res: Response) { private async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
@ -507,15 +517,16 @@ class BitcoinRoutes {
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
return res.status(413).send(e instanceof Error ? e.message : e); handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
} }
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getAddressTransactions(req: Request, res: Response): Promise<void> { private async getAddressTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
@ -528,23 +539,23 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
res.status(413).send(e instanceof Error ? e.message : e); handleError(req, res, 413, e instanceof Error ? e.message : e);
return; return;
} }
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> { private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
res.status(405).send('Address summary lookups require mempool/electrs backend.'); handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.');
return; return;
} }
} }
private async getScriptHash(req: Request, res: Response) { private async getScriptHash(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
@ -555,15 +566,16 @@ class BitcoinRoutes {
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
return res.status(413).send(e instanceof Error ? e.message : e); handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
} }
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> { private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND === 'none') { if (config.MEMPOOL.BACKEND === 'none') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
@ -578,16 +590,16 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
res.status(413).send(e instanceof Error ? e.message : e); handleError(req, res, 413, e instanceof Error ? e.message : e);
return; return;
} }
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> { private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
res.status(405).send('Scripthash summary lookups require mempool/electrs backend.'); handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.');
return; return;
} }
} }
@ -597,7 +609,7 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(blockHash); res.send(blockHash);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -624,7 +636,7 @@ class BitcoinRoutes {
const rawMempool = await bitcoinApi.$getRawMempool(); const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool); res.send(rawMempool);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -632,12 +644,13 @@ class BitcoinRoutes {
try { try {
const result = blocks.getCurrentBlockHeight(); const result = blocks.getCurrentBlockHeight();
if (!result) { if (!result) {
return res.status(503).send(`Service Temporarily Unavailable`); handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
} }
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(result.toString()); res.send(result.toString());
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -647,7 +660,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -657,7 +670,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'application/octet-stream'); res.setHeader('content-type', 'application/octet-stream');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -666,7 +679,7 @@ class BitcoinRoutes {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -675,7 +688,7 @@ class BitcoinRoutes {
const result = await bitcoinClient.validateAddress(req.params.address); const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -688,7 +701,7 @@ class BitcoinRoutes {
replaces replaces
}); });
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -697,7 +710,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(false); const result = rbfCache.getRbfTrees(false);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -706,7 +719,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(true); const result = rbfCache.getRbfTrees(true);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -719,7 +732,7 @@ class BitcoinRoutes {
res.status(204).send(); res.status(204).send();
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -728,7 +741,7 @@ class BitcoinRoutes {
const result = await bitcoinApi.$getOutspends(req.params.txId); const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -738,10 +751,10 @@ class BitcoinRoutes {
if (da) { if (da) {
res.json(da); res.json(da);
} else { } else {
res.status(503).send(`Service Temporarily Unavailable`); handleError(req, res, 503, `Service Temporarily Unavailable`);
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -752,7 +765,7 @@ class BitcoinRoutes {
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error')); : (e.message || 'Error'));
} }
} }
@ -764,7 +777,7 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex); const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error')); : (e.message || 'Error'));
} }
} }
@ -776,8 +789,7 @@ class BitcoinRoutes {
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result); res.send(result);
} catch (e: any) { } catch (e: any) {
res.setHeader('content-type', 'text/plain'); handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error')); : (e.message || 'Error'));
} }
} }

View File

@ -219,10 +219,10 @@ class Blocks {
}; };
} }
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary { public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
return { return {
id: hash, id: hash,
transactions: Common.classifyTransactions(transactions), transactions: Common.classifyTransactions(transactions, height),
}; };
} }
@ -616,7 +616,7 @@ class Blocks {
// add CPFP // add CPFP
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []); const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
// classify // classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2); await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) { if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
const cpfpClusters = await CpfpRepository.$getClustersAt(height); const cpfpClusters = await CpfpRepository.$getClustersAt(height);
@ -653,7 +653,7 @@ class Blocks {
} }
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []); const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
// classify // classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {}; const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
for (const tx of classifiedTxs) { for (const tx of classifiedTxs) {
classifiedTxMap[tx.txid] = tx; classifiedTxMap[tx.txid] = tx;
@ -912,7 +912,7 @@ class Blocks {
} }
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta }))); const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions); const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
@ -1169,7 +1169,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => { transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0; let flags: number = 0;
try { try {
flags = Common.getTransactionFlags(tx); flags = Common.getTransactionFlags(tx, height);
} catch (e) { } catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e)); logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
} }
@ -1188,7 +1188,7 @@ class Blocks {
} else { } else {
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(hash, txs); summary = this.summarizeBlockTransactions(hash, height || 0, txs);
summaryVersion = 1; summaryVersion = 1;
} else { } else {
// Call Core RPC // Call Core RPC
@ -1324,7 +1324,7 @@ class Blocks {
let summaryVersion = 0; let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx)); const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs); summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
summaryVersion = 1; summaryVersion = 1;
} else { } else {
// Call Core RPC // Call Core RPC

View File

@ -10,7 +10,6 @@ import logger from '../logger';
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script'; import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
// Bitcoin Core default policy settings // Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000; const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000; const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@ -200,10 +199,13 @@ export class Common {
* *
* returns true early if any standardness rule is violated, otherwise false * returns true early if any standardness rule is violated, otherwise false
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced) * (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
*
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
* For now, just pull out individual rules into versioned functions where necessary.
*/ */
static isNonStandard(tx: TransactionExtended): boolean { static isNonStandard(tx: TransactionExtended, height?: number): boolean {
// version // version
if (tx.version > TX_MAX_STANDARD_VERSION) { if (this.isNonStandardVersion(tx, height)) {
return true; return true;
} }
@ -250,6 +252,8 @@ export class Common {
} }
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true; return true;
} else if (this.isNonStandardAnchor(tx, height)) {
return true;
} }
// TODO: bad-witness-nonstandard // TODO: bad-witness-nonstandard
} }
@ -335,6 +339,49 @@ export class Common {
return false; return false;
} }
// Individual versioned standardness rules
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
let TX_MAX_STANDARD_VERSION = 3;
if (
height != null
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
) {
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
TX_MAX_STANDARD_VERSION = 2;
}
if (tx.version > TX_MAX_STANDARD_VERSION) {
return true;
}
return false;
}
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
if (
height != null
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
) {
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
return true;
}
return false;
}
static getNonWitnessSize(tx: TransactionExtended): number { static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight; let weight = tx.weight;
let hasWitness = false; let hasWitness = false;
@ -415,7 +462,7 @@ export class Common {
return flags; return flags;
} }
static getTransactionFlags(tx: TransactionExtended): number { static getTransactionFlags(tx: TransactionExtended, height?: number): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n; let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF) // Update variable flags (CPFP, RBF)
@ -548,7 +595,7 @@ export class Common {
if (hasFakePubkey) { if (hasFakePubkey) {
flags |= TransactionFlags.fake_pubkey; flags |= TransactionFlags.fake_pubkey;
} }
// fast but bad heuristic to detect possible coinjoins // 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) // (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1; const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
@ -564,17 +611,17 @@ export class Common {
flags |= TransactionFlags.batch_payout; flags |= TransactionFlags.batch_payout;
} }
if (this.isNonStandard(tx)) { if (this.isNonStandard(tx, height)) {
flags |= TransactionFlags.nonstandard; flags |= TransactionFlags.nonstandard;
} }
return Number(flags); return Number(flags);
} }
static classifyTransaction(tx: TransactionExtended): TransactionClassified { static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
let flags = 0; let flags = 0;
try { try {
flags = Common.getTransactionFlags(tx); flags = Common.getTransactionFlags(tx, height);
} catch (e) { } catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e)); logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
} }
@ -585,8 +632,8 @@ export class Common {
}; };
} }
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] { static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
return txs.map(Common.classifyTransaction); return txs.map(tx => Common.classifyTransaction(tx, height));
} }
static stripTransaction(tx: TransactionExtended): TransactionStripped { static stripTransaction(tx: TransactionExtended): TransactionStripped {

View File

@ -1,6 +1,7 @@
import config from '../../config'; import config from '../../config';
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import channelsApi from './channels.api'; import channelsApi from './channels.api';
import { handleError } from '../../utils/api';
class ChannelsRoutes { class ChannelsRoutes {
constructor() { } constructor() { }
@ -22,7 +23,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search); const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -38,7 +39,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel); res.json(channel);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -53,11 +54,11 @@ class ChannelsRoutes {
const status: string = typeof req.query.status === 'string' ? req.query.status : ''; const status: string = typeof req.query.status === 'string' ? req.query.status : '';
if (index < -1) { if (index < -1) {
res.status(400).send('Invalid index'); handleError(req, res, 400, 'Invalid index');
return; return;
} }
if (['open', 'active', 'closed'].includes(status) === false) { if (['open', 'active', 'closed'].includes(status) === false) {
res.status(400).send('Invalid status'); handleError(req, res, 400, 'Invalid status');
return; return;
} }
@ -69,14 +70,14 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString()); res.header('X-Total-Count', channelsCount.toString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> { private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
try { try {
if (!Array.isArray(req.query.txId)) { if (!Array.isArray(req.query.txId)) {
res.status(400).send('Not an array'); handleError(req, res, 400, 'Not an array');
return; return;
} }
const txIds: string[] = []; const txIds: string[] = [];
@ -107,7 +108,7 @@ class ChannelsRoutes {
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -119,7 +120,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -132,7 +133,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }

View File

@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api'; import nodesApi from './nodes.api';
import channelsApi from './channels.api'; import channelsApi from './channels.api';
import statisticsApi from './statistics.api'; import statisticsApi from './statistics.api';
import { handleError } from '../../utils/api';
class GeneralLightningRoutes { class GeneralLightningRoutes {
constructor() { } constructor() { }
@ -27,7 +29,7 @@ class GeneralLightningRoutes {
channels: channels, channels: channels,
}); });
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -41,7 +43,7 @@ class GeneralLightningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics); res.json(statistics);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -50,7 +52,7 @@ class GeneralLightningRoutes {
const statistics = await statisticsApi.$getLatestStatistics(); const statistics = await statisticsApi.$getLatestStatistics();
res.json(statistics); res.json(statistics);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }

View File

@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
import nodesApi from './nodes.api'; import nodesApi from './nodes.api';
import DB from '../../database'; import DB from '../../database';
import { INodesRanking } from '../../mempool.interfaces'; import { INodesRanking } from '../../mempool.interfaces';
import { handleError } from '../../utils/api';
class NodesRoutes { class NodesRoutes {
constructor() { } constructor() { }
@ -31,7 +32,7 @@ class NodesRoutes {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search); const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes); res.json(nodes);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -181,13 +182,13 @@ class NodesRoutes {
} }
} catch (e) {} } catch (e) {}
} }
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes); res.json(nodes);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -195,7 +196,7 @@ class NodesRoutes {
try { try {
const node = await nodesApi.$getNode(req.params.public_key); const node = await nodesApi.$getNode(req.params.public_key);
if (!node) { if (!node) {
res.status(404).send('Node not found'); handleError(req, res, 404, 'Node not found');
return; return;
} }
res.header('Pragma', 'public'); res.header('Pragma', 'public');
@ -203,7 +204,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node); res.json(node);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -215,7 +216,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics); res.json(statistics);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -223,7 +224,7 @@ class NodesRoutes {
try { try {
const node = await nodesApi.$getFeeHistogram(req.params.public_key); const node = await nodesApi.$getFeeHistogram(req.params.public_key);
if (!node) { if (!node) {
res.status(404).send('Node not found'); handleError(req, res, 404, 'Node not found');
return; return;
} }
res.header('Pragma', 'public'); res.header('Pragma', 'public');
@ -231,7 +232,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node); res.json(node);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -247,7 +248,7 @@ class NodesRoutes {
topByChannels: topChannelsNodes, topByChannels: topChannelsNodes,
}); });
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -259,7 +260,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -271,7 +272,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -283,7 +284,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -295,7 +296,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs); res.json(nodesPerAs);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -307,7 +308,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes); res.json(worldNodes);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -322,7 +323,7 @@ class NodesRoutes {
); );
if (country.length === 0) { if (country.length === 0) {
res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`); handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`);
return; return;
} }
@ -335,7 +336,7 @@ class NodesRoutes {
nodes: nodes, nodes: nodes,
}); });
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -349,7 +350,7 @@ class NodesRoutes {
); );
if (isp.length === 0) { if (isp.length === 0) {
res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`); handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`);
return; return;
} }
@ -362,7 +363,7 @@ class NodesRoutes {
nodes: nodes, nodes: nodes,
}); });
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -374,7 +375,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs); res.json(nodesPerAs);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }

View File

@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
import config from '../../config'; import config from '../../config';
import elementsParser from './elements-parser'; import elementsParser from './elements-parser';
import icons from './icons'; import icons from './icons';
import { handleError } from '../../utils/api';
class LiquidRoutes { class LiquidRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@ -42,7 +43,7 @@ class LiquidRoutes {
res.setHeader('content-length', result.length); res.setHeader('content-length', result.length);
res.send(result); res.send(result);
} else { } else {
res.status(404).send('Asset icon not found'); handleError(req, res, 404, 'Asset icon not found');
} }
} }
@ -51,7 +52,7 @@ class LiquidRoutes {
if (result) { if (result) {
res.json(result); res.json(result);
} else { } else {
res.status(404).send('Asset icons not found'); handleError(req, res, 404, 'Asset icons not found');
} }
} }
@ -82,7 +83,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(pegs); res.json(pegs);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -94,7 +95,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(reserves); res.json(reserves);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -106,7 +107,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentSupply); res.json(currentSupply);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -118,7 +119,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentReserves); res.json(currentReserves);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -130,7 +131,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(auditStatus); res.json(auditStatus);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -142,7 +143,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses); res.json(federationAddresses);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -154,7 +155,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses); res.json(federationAddresses);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -166,7 +167,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos); res.json(federationUtxos);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -178,7 +179,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(expiredUtxos); res.json(expiredUtxos);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -190,7 +191,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos); res.json(federationUtxos);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -202,7 +203,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos); res.json(emergencySpentUtxos);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -214,7 +215,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos); res.json(emergencySpentUtxos);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -226,7 +227,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(recentPegs); res.json(recentPegs);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -238,7 +239,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsVolume); res.json(pegsVolume);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -250,7 +251,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsCount); res.json(pegsCount);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }

View File

@ -10,6 +10,7 @@ import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository'; import PricesRepository from '../../repositories/PricesRepository';
import AccelerationRepository from '../../repositories/AccelerationRepository'; import AccelerationRepository from '../../repositories/AccelerationRepository';
import accelerationApi from '../services/acceleration'; import accelerationApi from '../services/acceleration';
import { handleError } from '../../utils/api';
class MiningRoutes { class MiningRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@ -53,12 +54,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Prices are not available on testnets.'); handleError(req, res, 400, 'Prices are not available on testnets.');
return; return;
} }
const timestamp = parseInt(req.query.timestamp as string, 10) || 0; const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
const currency = req.query.currency as string; const currency = req.query.currency as string;
let response; let response;
if (timestamp && currency) { if (timestamp && currency) {
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency); response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
@ -71,7 +72,7 @@ class MiningRoutes {
} }
res.status(200).send(response); res.status(200).send(response);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -84,9 +85,9 @@ class MiningRoutes {
res.json(stats); res.json(stats);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message); handleError(req, res, 404, e.message);
} else { } else {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }
@ -103,9 +104,9 @@ class MiningRoutes {
res.json(poolBlocks); res.json(poolBlocks);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message); handleError(req, res, 404, e.message);
} else { } else {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }
@ -129,7 +130,7 @@ class MiningRoutes {
res.json(pools); res.json(pools);
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -143,7 +144,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats); res.json(stats);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -157,7 +158,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates); res.json(hashrates);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -172,9 +173,9 @@ class MiningRoutes {
res.json(hashrates); res.json(hashrates);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message); handleError(req, res, 404, e.message);
} else { } else {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }
@ -203,7 +204,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty, currentDifficulty: currentDifficulty,
}); });
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -217,7 +218,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -235,7 +236,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -249,7 +250,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards); res.json(blockRewards);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -263,7 +264,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates); res.json(blockFeeRates);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -281,7 +282,7 @@ class MiningRoutes {
weights: blockWeights weights: blockWeights
}); });
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -293,7 +294,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -317,7 +318,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate])); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -326,7 +327,7 @@ class MiningRoutes {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) { if (!audit) {
res.status(204).send(`This block has not been audited.`); handleError(req, res, 204, `This block has not been audited.`);
return; return;
} }
@ -335,7 +336,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit); res.json(audit);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -358,7 +359,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -371,7 +372,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15)); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -384,7 +385,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit || 'null'); res.json(audit || 'null');
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -394,12 +395,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.'); handleError(req, res, 400, 'Acceleration data is not available.');
return; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -409,13 +410,13 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.'); handleError(req, res, 400, 'Acceleration data is not available.');
return; return;
} }
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -425,12 +426,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.'); handleError(req, res, 400, 'Acceleration data is not available.');
return; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -440,12 +441,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.'); handleError(req, res, 400, 'Acceleration data is not available.');
return; return;
} }
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -455,12 +456,12 @@ class MiningRoutes {
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Acceleration data is not available.'); handleError(req, res, 400, 'Acceleration data is not available.');
return; return;
} }
res.status(200).send(accelerationApi.accelerations || []); res.status(200).send(accelerationApi.accelerations || []);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -472,7 +473,7 @@ class MiningRoutes {
accelerationApi.accelerationRequested(req.params.txid); accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send(); res.status(200).send();
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }

View File

@ -1106,7 +1106,7 @@ class BlocksRepository {
let summaryVersion = 0; let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx)); const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs); summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
summaryVersion = 1; summaryVersion = 1;
} else { } else {
// Call Core RPC // Call Core RPC

9
backend/src/utils/api.ts Normal file
View File

@ -0,0 +1,9 @@
import { Request, Response } from 'express';
export function handleError(req: Request, res: Response, statusCode: number, errorMessage: string | unknown): void {
if (req.accepts('json')) {
res.status(statusCode).json({ error: errorMessage });
} else {
res.status(statusCode).send(errorMessage);
}
}

View File

@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opN) { if (!opN) {
return; return;
} }
if (!opN.startsWith('OP_PUSHNUM_')) { if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opM) { if (!opM) {
return; return;
} }
if (!opM.startsWith('OP_PUSHNUM_')) { if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);

View File

@ -62,7 +62,7 @@
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"cypress": "^13.13.0", "cypress": "^13.14.0",
"cypress-fail-on-console-error": "~5.1.0", "cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1", "cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1", "mock-socket": "~9.3.1",
@ -8045,13 +8045,13 @@
"peer": true "peer": true
}, },
"node_modules/cypress": { "node_modules/cypress": {
"version": "13.13.0", "version": "13.14.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz",
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==", "integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@cypress/request": "^3.0.0", "@cypress/request": "^3.0.1",
"@cypress/xvfb": "^1.2.4", "@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1", "@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2", "@types/sizzle": "^2.3.2",
@ -12693,12 +12693,12 @@
} }
}, },
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.4", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dependencies": { "dependencies": {
"braces": "^3.0.1", "braces": "^3.0.3",
"picomatch": "^2.2.3" "picomatch": "^2.3.1"
}, },
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
@ -24137,12 +24137,12 @@
"peer": true "peer": true
}, },
"cypress": { "cypress": {
"version": "13.13.0", "version": "13.14.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz",
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==", "integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==",
"optional": true, "optional": true,
"requires": { "requires": {
"@cypress/request": "^3.0.0", "@cypress/request": "^3.0.1",
"@cypress/xvfb": "^1.2.4", "@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1", "@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2", "@types/sizzle": "^2.3.2",
@ -27621,12 +27621,12 @@
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
}, },
"micromatch": { "micromatch": {
"version": "4.0.4", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"requires": { "requires": {
"braces": "^3.0.1", "braces": "^3.0.3",
"picomatch": "^2.2.3" "picomatch": "^2.3.1"
} }
}, },
"miller-rabin": { "miller-rabin": {

View File

@ -115,7 +115,7 @@
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"cypress": "^13.13.0", "cypress": "^13.14.0",
"cypress-fail-on-console-error": "~5.1.0", "cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1", "cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1", "mock-socket": "~9.3.1",

View File

@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
return; return;
} }
const opN = ops.pop(); const opN = ops.pop();
if (!opN.startsWith('OP_PUSHNUM_')) { if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const n = parseInt(opN.match(/[0-9]+/)[0], 10); const n = parseInt(opN.match(/[0-9]+/)[0], 10);
@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
} }
} }
const opM = ops.pop(); const opM = ops.pop();
if (!opM.startsWith('OP_PUSHNUM_')) { if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const m = parseInt(opM.match(/[0-9]+/)[0], 10); const m = parseInt(opM.match(/[0-9]+/)[0], 10);

View File

@ -53,7 +53,7 @@
<span>Spiral</span> <span>Spiral</span>
</a> </a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry"> <a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76" class="image"> <svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="90" viewBox="0 -5 32 90" class="image">
<defs> <defs>
<style> <style>
.d { .d {
@ -130,14 +130,9 @@
</svg> </svg>
<span>Unchained</span> <span>Unchained</span>
</a> </a>
<a href="https://gemini.com/" target="_blank" title="Gemini"> <a href="https://bitkey.world/" target="_blank" title="Bitkey">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image"> <img class="image" src="/resources/profile/bitkey.svg" />
<rect style="fill: black" width="360" height="360" /> <span>Bitkey</span>
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
</a> </a>
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin"> <a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg"> <svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
@ -193,6 +188,19 @@
</svg> </svg>
<span>Exodus</span> <span>Exodus</span>
</a> </a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: black" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
</a>
<a href="https://leather.io/" target="_blank" title="Leather">
<img class="image" src="/resources/profile/leather.svg" />
<span>Leather</span>
</a>
</div> </div>
</div> </div>

View File

@ -251,3 +251,12 @@
width: 64px; width: 64px;
height: 64px; height: 64px;
} }
.enterprise-sponsor {
.wrapper {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 800px;
}
}

View File

@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
} }
handleVin() { handleVin() {
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]) 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) { if (address?.scripts.size) {
const script = address?.scripts.values().next().value; const script = address?.scripts.values().next().value;
if (script.template?.label) { if (script.template?.label) {

View File

@ -0,0 +1,7 @@
<div [formGroup]="amountForm" class="text-small text-center">
<select formControlName="mode" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 70px;" (change)="changeMode()">
<option value="btc" i18n="shared.btc|BTC">BTC</option>
<option value="sats" i18n="shared.sat|sat">sat</option>
<option value="fiat" i18n="shared.fiat|Fiat">Fiat</option>
</select>
</div>

View File

@ -0,0 +1,36 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { StorageService } from '../../services/storage.service';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-amount-selector',
templateUrl: './amount-selector.component.html',
styleUrls: ['./amount-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AmountSelectorComponent implements OnInit {
amountForm: UntypedFormGroup;
modes = ['btc', 'sats', 'fiat'];
constructor(
private formBuilder: UntypedFormBuilder,
private stateService: StateService,
private storageService: StorageService,
) { }
ngOnInit() {
this.amountForm = this.formBuilder.group({
mode: ['btc']
});
this.stateService.viewAmountMode$.subscribe((mode) => {
this.amountForm.get('mode')?.setValue(mode);
});
}
changeMode() {
const newMode = this.amountForm.get('mode')?.value;
this.storageService.setValue('view-amount-mode', newMode);
this.stateService.viewAmountMode$.next(newMode);
}
}

View File

@ -5,7 +5,7 @@
</div> </div>
<div class="faucet-container text-center"> <div class="faucet-container text-center">
@if (txid) { @if (txid) {
<div class="alert alert-success w-100 text-truncate"> <div class="alert alert-success w-100 text-truncate">
<fa-icon [icon]="['fas', 'circle-check']"></fa-icon> <fa-icon [icon]="['fas', 'circle-check']"></fa-icon>
@ -36,6 +36,13 @@
<app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login> <app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
</div> </div>
} }
@else if (error === 'account_limited') {
<div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle">
<span class="mb-2 mr-2">Your Twitter account does not allow you to access the faucet</span>
</div>
</div>
}
@else if (error) { @else if (error) {
<!-- User can request --> <!-- User can request -->
<app-mempool-error class="w-100" [error]="error"></app-mempool-error> <app-mempool-error class="w-100" [error]="error"></app-mempool-error>
@ -81,7 +88,7 @@
} }
<!-- Send back coins --> <!-- Send back coins -->
@if (status?.address) { @if (status?.address) {
<div class="mt-4 alert alert-info w-100">If you no longer need your testnet4 coins, please consider <a class="text-primary" [routerLink]="['/address/' | relativeUrl, status.address]"><u>sending them back</u></a> to replenish the faucet.</div> <div class="mt-4 alert alert-info w-100">If you no longer need your testnet4 coins, please consider <a class="text-primary" [routerLink]="['/address/' | relativeUrl, status.address]"><u>sending them back</u></a> to replenish the faucet.</div>
} }

View File

@ -19,7 +19,7 @@ export class FaucetComponent implements OnInit, OnDestroy {
error: string = ''; error: string = '';
user: any = undefined; user: any = undefined;
txid: string = ''; txid: string = '';
faucetStatusSubscription: Subscription; faucetStatusSubscription: Subscription;
status: { status: {
min: number; // minimum amount to request at once (in sats) min: number; // minimum amount to request at once (in sats)

View File

@ -747,7 +747,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
checkAccelerationEligibility() { checkAccelerationEligibility() {
if (this.tx) { if (this.tx) {
this.tx.flags = getTransactionFlags(this.tx); this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n; const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n;
const highSigop = (this.tx.sigops * 20) > this.tx.weight; const highSigop = (this.tx.sigops * 20) > this.tx.weight;
this.eligibleForAcceleration = !replaceableInputs && !highSigop; this.eligibleForAcceleration = !replaceableInputs && !highSigop;

View File

@ -551,23 +551,23 @@
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td> <td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
<td> <td>
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton"> <ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
@if (eta.blocks >= 7) { @if (network === 'liquid' || network === 'liquidtestnet') {
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
<a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
}
</span>
} @else if (network === 'liquid' || network === 'liquidtestnet') {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time> <app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
} @else { } @else {
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''"> <span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) ? 'etaDeepMempool d-flex justify-content-between' : ''">
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time> @if (eta.blocks >= 7) {
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) { <span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
<a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a> } @else {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
}
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) {
<div class="d-flex accelerate">
<a class="btn btn-sm accelerateDeepMempool btn-small-height" [class.disabled]="!eligibleForAcceleration" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
<a *ngIf="!eligibleForAcceleration" href="https://mempool.space/accelerator#why-cant-accelerate" target="_blank" class="info-badges ml-1" i18n-ngbTooltip="Mempool Accelerator&trade; tooltip" ngbTooltip="This transaction cannot be accelerated">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</div>
} }
</span>
<span class="eta justify-content-end">
</span> </span>
} }
</ng-container> </ng-container>

View File

@ -287,37 +287,21 @@
} }
.accelerate { .accelerate {
display: flex !important; @media (min-width: 850px) {
align-self: auto; margin-left: auto;
margin-left: auto; }
background-color: var(--tertiary);
@media (max-width: 849px) {
margin-left: 5px;
}
} }
.etaDeepMempool { .etaDeepMempool {
justify-content: flex-end;
flex-wrap: wrap; flex-wrap: wrap;
align-content: center;
@media (max-width: 995px) {
justify-content: left !important;
}
@media (max-width: 849px) { @media (max-width: 849px) {
justify-content: right !important; justify-content: right !important;
} }
} }
.accelerateDeepMempool { .accelerateDeepMempool {
align-self: auto;
margin-left: auto;
background-color: var(--tertiary); background-color: var(--tertiary);
@media (max-width: 995px) { margin-left: 5px;
margin-left: 0px;
}
@media (max-width: 849px) {
margin-left: 5px;
}
} }
.goggles-icon { .goggles-icon {
@ -335,4 +319,9 @@
.oobFees { .oobFees {
color: #905cf4; color: #905cf4;
}
.disabled {
opacity: 0.5;
pointer-events: none;
} }

View File

@ -901,7 +901,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit'); this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot'); this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf'); this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
this.tx.flags = getTransactionFlags(this.tx); this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : []; this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : [];
this.checkAccelerationEligibility(); this.checkAccelerationEligibility();
} else { } else {

View File

@ -17,6 +17,7 @@ export type AddressType = 'fee'
| 'v0_p2wsh' | 'v0_p2wsh'
| 'v1_p2tr' | 'v1_p2tr'
| 'confidential' | 'confidential'
| 'anchor'
| 'unknown' | 'unknown'
const ADDRESS_PREFIXES = { const ADDRESS_PREFIXES = {
@ -188,6 +189,12 @@ export class AddressTypeInfo {
const v = vin[0]; const v = vin[0];
this.processScript(new ScriptInfo('scriptpubkey', v.prevout.scriptpubkey, v.prevout.scriptpubkey_asm)); this.processScript(new ScriptInfo('scriptpubkey', v.prevout.scriptpubkey, v.prevout.scriptpubkey_asm));
} }
} else if (this.type === 'unknown') {
for (const v of vin) {
if (v.prevout?.scriptpubkey === '51024e73') {
this.type = 'anchor';
}
}
} }
// and there's nothing more to learn from processing inputs for other types // and there's nothing more to learn from processing inputs for other types
} }
@ -197,6 +204,10 @@ export class AddressTypeInfo {
if (!this.scripts.size) { if (!this.scripts.size) {
this.processScript(new ScriptInfo('scriptpubkey', output.scriptpubkey, output.scriptpubkey_asm)); this.processScript(new ScriptInfo('scriptpubkey', output.scriptpubkey, output.scriptpubkey_asm));
} }
} else if (this.type === 'unknown') {
if (output.scriptpubkey === '51024e73') {
this.type = 'anchor';
}
} }
} }

View File

@ -20,6 +20,9 @@
@case ('multisig') { @case ('multisig') {
<span i18n="address.bare-multisig">bare multisig</span> <span i18n="address.bare-multisig">bare multisig</span>
} }
@case ('anchor') {
<span>anchor</span>
}
@case (null) { @case (null) {
<span>unknown</span> <span>unknown</span>
} }

View File

@ -27,23 +27,27 @@
<div class="selector"> <div class="selector">
<app-rate-unit-selector></app-rate-unit-selector> <app-rate-unit-selector></app-rate-unit-selector>
</div> </div>
<div class="selector d-none" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'">
<app-amount-selector></app-amount-selector>
</div>
@if (!env.customize?.theme) { @if (!env.customize?.theme) {
<div class="selector d-none d-sm-flex"> <div class="selector d-none" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'">
<app-theme-selector></app-theme-selector> <app-theme-selector></app-theme-selector>
</div> </div>
} }
<a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-none d-sm-flex justify-content-center" [routerLink]="['/login']"> <a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-none justify-content-center" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'" [routerLink]="['/login']">
<span *ngIf="user" i18n="shared.my-account" class="nowrap">My Account</span> <span *ngIf="user" i18n="shared.my-account" class="nowrap">My Account</span>
<span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span> <span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span>
</a> </a>
</div> </div>
@if (!env.customize?.theme) { @if (!env.customize?.theme) {
<div class="selector d-flex d-sm-none justify-content-center ml-auto mr-auto mt-0"> <div class="selector d-flex justify-content-center ml-auto mr-auto mt-0" [ngClass]="isServicesPage ? 'd-lg-none' : 'd-md-none'">
<app-theme-selector></app-theme-selector> <app-amount-selector class="add-margin"></app-amount-selector>
<app-theme-selector class="add-margin"></app-theme-selector>
</div> </div>
} }
@if (!enterpriseInfo?.footer_img) { @if (!enterpriseInfo?.footer_img) {
<a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-0 mb-2" [routerLink]="['/login']"> <a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-flex justify-content-center ml-auto mr-auto mt-0 mb-2" [ngClass]="isServicesPage ? 'd-lg-none' : 'd-md-none'" [routerLink]="['/login']">
<span *ngIf="user" i18n="shared.my-account" class="nowrap">My Account</span> <span *ngIf="user" i18n="shared.my-account" class="nowrap">My Account</span>
<span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span> <span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span>
</a> </a>

View File

@ -76,6 +76,11 @@ footer .selector {
display: inline-block; display: inline-block;
} }
footer .add-margin {
margin-left: 5px;
margin-right: 5px;
}
footer .row.link-tree { footer .row.link-tree {
max-width: 1140px; max-width: 1140px;
margin: 0 auto; margin: 0 auto;
@ -154,7 +159,7 @@ footer .nowrap {
display: block; display: block;
} }
@media (min-width: 951px) { @media (min-width: 1020px) {
:host-context(.ltr-layout) .language-selector { :host-context(.ltr-layout) .language-selector {
float: right !important; float: right !important;
} }
@ -172,7 +177,24 @@ footer .nowrap {
} }
.services { .services {
@media (min-width: 951px) and (max-width: 1147px) { @media (min-width: 1300px) {
:host-context(.ltr-layout) .language-selector {
float: right !important;
}
:host-context(.rtl-layout) .language-selector {
float: left !important;
}
.explore-tagline-desktop {
display: block;
}
.explore-tagline-mobile {
display: none;
}
}
@media (max-width: 1300px) {
:host-context(.ltr-layout) .services .language-selector { :host-context(.ltr-layout) .services .language-selector {
float: none !important; float: none !important;
} }
@ -248,7 +270,7 @@ footer .nowrap {
} }
@media (max-width: 950px) { @media (max-width: 1019px) {
.main-logo { .main-logo {
width: 220px; width: 220px;
@ -287,7 +309,7 @@ footer .nowrap {
} }
} }
@media (max-width: 1147px) { @media (max-width: 1300px) {
.services.main-logo { .services.main-logo {
width: 220px; width: 220px;

View File

@ -166,6 +166,7 @@ export const ScriptTemplates: { [type: string]: (...args: any) => ScriptTemplate
ln_anchor: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }), ln_anchor: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }),
ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept Lightning Anchor' }), ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept Lightning Anchor' }),
multisig: (m: number, n: number) => ({ type: 'multisig', m, n, label: $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:` }), multisig: (m: number, n: number) => ({ type: 'multisig', m, n, label: $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:` }),
anchor: () => ({ type: 'anchor', label: 'anchor' }),
}; };
export class ScriptInfo { export class ScriptInfo {
@ -266,7 +267,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
if (!opN) { if (!opN) {
return; return;
} }
if (!opN.startsWith('OP_PUSHNUM_')) { if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
@ -286,7 +287,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
if (!opM) { if (!opM) {
return; return;
} }
if (!opM.startsWith('OP_PUSHNUM_')) { if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return; return;
} }
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);

View File

@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, 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, 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, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark} 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, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '../components/menu/menu.component'; import { MenuComponent } from '../components/menu/menu.component';
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component'; import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
@ -35,6 +35,7 @@ import { LanguageSelectorComponent } from '../components/language-selector/langu
import { FiatSelectorComponent } from '../components/fiat-selector/fiat-selector.component'; import { FiatSelectorComponent } from '../components/fiat-selector/fiat-selector.component';
import { RateUnitSelectorComponent } from '../components/rate-unit-selector/rate-unit-selector.component'; import { RateUnitSelectorComponent } from '../components/rate-unit-selector/rate-unit-selector.component';
import { ThemeSelectorComponent } from '../components/theme-selector/theme-selector.component'; import { ThemeSelectorComponent } from '../components/theme-selector/theme-selector.component';
import { AmountSelectorComponent } from '../components/amount-selector/amount-selector.component';
import { BrowserOnlyDirective } from './directives/browser-only.directive'; import { BrowserOnlyDirective } from './directives/browser-only.directive';
import { ServerOnlyDirective } from './directives/server-only.directive'; import { ServerOnlyDirective } from './directives/server-only.directive';
import { ColoredPriceDirective } from './directives/colored-price.directive'; import { ColoredPriceDirective } from './directives/colored-price.directive';
@ -131,6 +132,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
FiatSelectorComponent, FiatSelectorComponent,
ThemeSelectorComponent, ThemeSelectorComponent,
RateUnitSelectorComponent, RateUnitSelectorComponent,
AmountSelectorComponent,
ScriptpubkeyTypePipe, ScriptpubkeyTypePipe,
RelativeUrlPipe, RelativeUrlPipe,
NoSanitizePipe, NoSanitizePipe,
@ -278,6 +280,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
FiatSelectorComponent, FiatSelectorComponent,
RateUnitSelectorComponent, RateUnitSelectorComponent,
ThemeSelectorComponent, ThemeSelectorComponent,
AmountSelectorComponent,
ScriptpubkeyTypePipe, ScriptpubkeyTypePipe,
RelativeUrlPipe, RelativeUrlPipe,
Hex2asciiPipe, Hex2asciiPipe,
@ -440,5 +443,6 @@ export class SharedModule {
library.addIcons(faFaucetDrip); library.addIcons(faFaucetDrip);
library.addIcons(faTimeline); library.addIcons(faTimeline);
library.addIcons(faCircleXmark); library.addIcons(faCircleXmark);
library.addIcons(faCalendarCheck);
} }
} }

View File

@ -2,9 +2,9 @@ import { TransactionFlags } from './filters.utils';
import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils'; import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils';
import { Transaction } from '../interfaces/electrs.interface'; import { Transaction } from '../interfaces/electrs.interface';
import { CpfpInfo, RbfInfo, TransactionStripped } from '../interfaces/node-api.interface'; import { CpfpInfo, RbfInfo, TransactionStripped } from '../interfaces/node-api.interface';
import { StateService } from '../services/state.service';
// Bitcoin Core default policy settings // Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000; const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000; const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@ -89,10 +89,13 @@ export function isDERSig(w: string): boolean {
* *
* returns true early if any standardness rule is violated, otherwise false * returns true early if any standardness rule is violated, otherwise false
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced) * (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
*
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
* For now, just pull out individual rules into versioned functions where necessary.
*/ */
export function isNonStandard(tx: Transaction): boolean { export function isNonStandard(tx: Transaction, height?: number, network?: string): boolean {
// version // version
if (tx.version > TX_MAX_STANDARD_VERSION) { if (isNonStandardVersion(tx, height, network)) {
return true; return true;
} }
@ -139,6 +142,8 @@ export function isNonStandard(tx: Transaction): boolean {
} }
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true; return true;
} else if (isNonStandardAnchor(tx, height, network)) {
return true;
} }
// TODO: bad-witness-nonstandard // TODO: bad-witness-nonstandard
} }
@ -203,6 +208,51 @@ export function isNonStandard(tx: Transaction): boolean {
return false; return false;
} }
// Individual versioned standardness rules
const V3_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
function isNonStandardVersion(tx: Transaction, height?: number, network?: string): boolean {
let TX_MAX_STANDARD_VERSION = 3;
if (
height != null
&& network != null
&& V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
&& height <= V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
) {
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
TX_MAX_STANDARD_VERSION = 2;
}
if (tx.version > TX_MAX_STANDARD_VERSION) {
return true;
}
return false;
}
const ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
function isNonStandardAnchor(tx: Transaction, height?: number, network?: string): boolean {
if (
height != null
&& network != null
&& ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
&& height <= ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
) {
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
return true;
}
return false;
}
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode // 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. // followed by a data push between 2 and 40 bytes.
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240 // https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
@ -289,7 +339,7 @@ export function isBurnKey(pubkey: string): boolean {
].includes(pubkey); ].includes(pubkey);
} }
export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean): bigint { export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean, height?: number, network?: string): bigint {
let flags = tx.flags ? BigInt(tx.flags) : 0n; let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF) // Update variable flags (CPFP, RBF)
@ -439,7 +489,7 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
flags |= TransactionFlags.batch_payout; flags |= TransactionFlags.batch_payout;
} }
if (isNonStandard(tx)) { if (isNonStandard(tx, height, network)) {
flags |= TransactionFlags.nonstandard; flags |= TransactionFlags.nonstandard;
} }

View File

@ -0,0 +1,3 @@
<svg width="212" height="450" viewBox="0 0 212 450" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M114.005 2.14659C109.052 -0.71553 102.948 -0.715531 97.9948 2.14659L7.99481 54.1531C3.04732 57.012 0 62.2924 0 68.0065V172.043C0 177.758 3.04733 183.038 7.99482 185.897L69.0247 221.163C73.9722 224.022 77.0195 229.302 77.0195 235.016V433.998V437.998C77.0195 444.625 82.3921 449.998 89.0195 449.998H93.0195H118.981H123.031C129.658 449.998 135.031 444.625 135.031 437.998V233.751C135.444 228.531 138.395 223.809 142.975 221.163L204.005 185.897C208.953 183.038 212 177.758 212 172.043V68.0065C212 62.2924 208.953 57.012 204.005 54.1531L114.005 2.14659ZM112.07 68.1162C108.334 65.938 103.716 65.938 99.9803 68.1162L63.9718 89.1129C60.2841 91.2631 58.0164 95.2105 58.0164 99.4793V141.68C58.0164 145.948 60.2841 149.896 63.9718 152.046L99.9803 173.043C103.716 175.221 108.334 175.221 112.07 173.043L148.078 152.046C151.766 149.896 154.034 145.948 154.034 141.68V99.4793C154.034 95.2105 151.766 91.2631 148.078 89.1129L112.07 68.1162ZM172.046 338.527C164.047 333.861 154 339.631 154 348.892L154 356L154 412V419.108C154 428.369 164.047 434.14 172.046 429.473L196.046 415.473C199.733 413.322 202 409.376 202 405.108V362.892C202 358.624 199.733 354.678 196.046 352.527L172.046 338.527Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="387" height="377" viewBox="-58.05 -56.55 503.1 490.1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M249.327 137.966C283.745 132.618 333.52 96.2553 333.52 67.9135C333.52 59.3574 326.637 53.4752 316.576 53.4752C297.513 53.4752 265.212 82.3518 249.327 137.966ZM89.9409 278.606C44.9317 278.606 41.225 323.525 86.2343 323.525C106.356 323.525 130.714 315.504 143.422 301.065C124.889 285.023 109.533 278.606 89.9409 278.606ZM376.411 259.355C379.059 334.755 340.934 377 276.332 377C238.207 377 219.144 362.562 178.371 335.824C157.19 359.353 116.946 377 83.5867 377C-31.3192 377 -26.5536 229.943 90.4704 229.943C114.828 229.943 135.48 236.36 161.956 252.938L179.43 191.441C107.415 171.655 71.4077 116.041 106.886 36.3631H164.074C132.303 89.3035 154.013 133.153 194.256 137.966C215.967 60.4269 262.565 0 324.518 0C359.467 0 387.002 22.9943 387.002 64.705C387.002 131.549 300.161 186.094 234.5 191.441L207.494 287.162C238.207 322.99 323.459 357.749 323.459 259.355H376.411Z" fill="#F5F1ED"/>
</svg>

After

Width:  |  Height:  |  Size: 1011 B

View File

@ -15,6 +15,7 @@ rpcpassword=__BITCOIN_RPC_PASS__
whitelist=127.0.0.1 whitelist=127.0.0.1
whitelist=103.99.168.0/22 whitelist=103.99.168.0/22
whitelist=2401:b140::/32 whitelist=2401:b140::/32
blocksxor=0
#uacomment=@wiz #uacomment=@wiz
[main] [main]

View File

@ -392,9 +392,9 @@ DEBIAN_UNFURL_PKG+=(libxdamage-dev libxrandr-dev libgbm-dev libpango1.0-dev liba
# packages needed for mempool ecosystem # packages needed for mempool ecosystem
FREEBSD_PKG=() FREEBSD_PKG=()
FREEBSD_PKG+=(zsh sudo git git-lfs screen curl wget calc neovim) FREEBSD_PKG+=(zsh sudo git git-lfs screen curl wget calc neovim)
FREEBSD_PKG+=(openssh-portable py39-pip rust llvm10 jq base64 libzmq4) FREEBSD_PKG+=(openssh-portable py311-pip rust llvm18 jq base64 libzmq4)
FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf) FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf)
FREEBSD_PKG+=(nginx rsync py39-certbot-nginx mariadb1011-server keybase) FREEBSD_PKG+=(nginx rsync py311-certbot-nginx mariadb1011-server)
FREEBSD_PKG+=(geoipupdate redis) FREEBSD_PKG+=(geoipupdate redis)
FREEBSD_UNFURL_PKG=() FREEBSD_UNFURL_PKG=()

View File

@ -1,3 +1,5 @@
#!/usr/bin/env zsh #!/usr/bin/env zsh
rm $HOME/*/backend/mempool-config.json rm -f $HOME/*/backend/mempool-config.json
rm $HOME/*/frontend/mempool-frontend-config.json rm -f $HOME/*/frontend/mempool-frontend-config.json
rm -f $HOME/*/frontend/projects/mempool/mempool-frontend-config.json
exit 0