Compare commits

..

3 Commits

Author SHA1 Message Date
orangesurf
00cb2ffc75 Update urls 2024-12-17 15:20:19 +01:00
orangesurf
160b9217b9 add frontend config 2024-12-17 10:08:05 +01:00
orangesurf
37acc4493a check in new resources 2024-12-17 09:54:46 +01:00
70 changed files with 628 additions and 2392 deletions

View File

@@ -3,10 +3,6 @@ import logger from '../../logger';
import bitcoinClient from './bitcoin-client';
import config from '../../config';
const BLOCKHASH_REGEX = /^[a-f0-9]{64}$/i;
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
/**
* Define a set of routes used by the accelerator server
* Those routes are not designed to be public
@@ -14,7 +10,7 @@ const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
class BitcoinBackendRoutes {
private static tag = 'BitcoinBackendRoutes';
public initRoutes(app: Application): void {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
@@ -30,10 +26,10 @@ class BitcoinBackendRoutes {
/**
* Disable caching for bitcoin core routes
*
* @param req
* @param res
* @param next
*
* @param req
* @param res
* @param next
*/
private disableCache(req: Request, res: Response, next: NextFunction): void {
res.setHeader('Pragma', 'no-cache');
@@ -44,16 +40,16 @@ class BitcoinBackendRoutes {
/**
* Exeption handler to return proper details to the accelerator server
*
* @param e
* @param fnName
* @param res
*
* @param e
* @param fnName
* @param res
*/
private static handleException(e: any, fnName: string, res: Response): void {
if (typeof(e.code) === 'number') {
res.status(400).send(JSON.stringify(e, ['code']));
} else {
const err = `unknown exception in ${fnName}`;
res.status(400).send(JSON.stringify(e, ['code', 'message']));
} else {
const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`;
logger.err(err, BitcoinBackendRoutes.tag);
res.status(500).send(err);
}
@@ -62,13 +58,13 @@ class BitcoinBackendRoutes {
private async $getMempoolEntry(req: Request, res: Response): Promise<void> {
const txid = req.query.txid;
try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return;
}
const mempoolEntry = await bitcoinClient.getMempoolEntry(txid);
if (!mempoolEntry) {
res.status(404).send();
res.status(404).send(`no mempool entry found for txid ${txid}`);
return;
}
res.status(200).send(mempoolEntry);
@@ -80,13 +76,13 @@ class BitcoinBackendRoutes {
private async $decodeRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx;
try {
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) {
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`);
if (typeof(rawTx) !== 'string') {
res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
return;
}
const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx);
if (!decodedTx) {
res.status(400).send(`unable to decode rawTx`);
res.status(400).send(`unable to decode rawTx ${rawTx}`);
return;
}
res.status(200).send(decodedTx);
@@ -99,23 +95,23 @@ class BitcoinBackendRoutes {
const txid = req.query.txid;
const verbose = req.query.verbose;
try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return;
}
if (typeof(verbose) !== 'string') {
res.status(400).send(`invalid param verbose. must be a string representing an integer`);
res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`);
return;
}
const verboseNumber = parseInt(verbose, 10);
if (typeof(verboseNumber) !== 'number') {
res.status(400).send(`invalid param verbose. must be a valid integer`);
res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`);
return;
}
const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber);
if (!decodedTx) {
res.status(400).send(`unable to get raw transaction`);
res.status(400).send(`unable to get raw transaction for txid ${txid}`);
return;
}
res.status(200).send(decodedTx);
@@ -127,13 +123,13 @@ class BitcoinBackendRoutes {
private async $sendRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx;
try {
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) {
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`);
if (typeof(rawTx) !== 'string') {
res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
return;
}
const txHex = await bitcoinClient.sendRawTransaction(rawTx);
if (!txHex) {
res.status(400).send(`unable to send rawTx`);
res.status(400).send(`unable to send rawTx ${rawTx}`);
return;
}
res.status(200).send(txHex);
@@ -145,13 +141,13 @@ class BitcoinBackendRoutes {
private async $testMempoolAccept(req: Request, res: Response): Promise<void> {
const rawTxs = req.body.rawTxs;
try {
if (typeof(rawTxs) !== 'object' || !Array.isArray(rawTxs) || rawTxs.some((tx) => typeof(tx) !== 'string' || !RAW_TX_REGEX.test(tx))) {
res.status(400).send(`invalid param rawTxs. must be an array of strings of hexadecimal characters`);
if (typeof(rawTxs) !== 'object') {
res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`);
return;
}
const txHex = await bitcoinClient.testMempoolAccept(rawTxs);
if (typeof(txHex) !== 'object' || txHex.length === 0) {
res.status(400).send(`testmempoolaccept failed for raw txs, got an empty result`);
res.status(400).send(`testmempoolaccept failed for raw txs ${JSON.stringify(rawTxs)}, got an empty result`);
return;
}
res.status(200).send(txHex);
@@ -164,18 +160,18 @@ class BitcoinBackendRoutes {
const txid = req.query.txid;
const verbose = req.query.verbose;
try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return;
}
if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) {
res.status(400).send(`invalid param verbose. must be a string ('true' | 'false')`);
res.status(400).send(`invalid param verbose ${verbose}. must be a string ('true' | 'false')`);
return;
}
const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false);
if (!ancestors) {
res.status(400).send(`unable to get mempool ancestors`);
res.status(400).send(`unable to get mempool ancestors for txid ${txid}`);
return;
}
res.status(200).send(ancestors);
@@ -188,23 +184,23 @@ class BitcoinBackendRoutes {
const blockHash = req.query.hash;
const verbosity = req.query.verbosity;
try {
if (typeof(blockHash) !== 'string' || blockHash.length !== 64 || !BLOCKHASH_REGEX.test(blockHash)) {
res.status(400).send(`invalid param blockHash. must be 64 hexadecimal characters`);
if (typeof(blockHash) !== 'string' || blockHash.length !== 64) {
res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`);
return;
}
if (typeof(verbosity) !== 'string') {
res.status(400).send(`invalid param verbosity. must be a string representing an integer`);
res.status(400).send(`invalid param verbosity ${verbosity}. must be a string representing an integer`);
return;
}
const verbosityNumber = parseInt(verbosity, 10);
if (typeof(verbosityNumber) !== 'number') {
res.status(400).send(`invalid param verbosity. must be a valid integer`);
res.status(400).send(`invalid param verbosity ${verbosity}. must be a valid integer`);
return;
}
const block = await bitcoinClient.getBlock(blockHash, verbosityNumber);
if (!block) {
res.status(400).send(`unable to get block`);
res.status(400).send(`unable to get block for block hash ${blockHash}`);
return;
}
res.status(200).send(block);
@@ -217,18 +213,18 @@ class BitcoinBackendRoutes {
const blockHeight = req.query.height;
try {
if (typeof(blockHeight) !== 'string') {
res.status(400).send(`invalid param blockHeight, must be a string representing an integer`);
res.status(400).send(`invalid param blockHeight ${blockHeight}, must be a string representing an integer`);
return;
}
const blockHeightNumber = parseInt(blockHeight, 10);
if (typeof(blockHeightNumber) !== 'number') {
res.status(400).send(`invalid param blockHeight. must be a valid integer`);
res.status(400).send(`invalid param blockHeight ${blockHeight}. must be a valid integer`);
return;
}
const block = await bitcoinClient.getBlockHash(blockHeightNumber);
if (!block) {
res.status(400).send(`unable to get block hash`);
res.status(400).send(`unable to get block hash for block height ${blockHeightNumber}`);
return;
}
res.status(200).send(block);
@@ -251,4 +247,4 @@ class BitcoinBackendRoutes {
}
}
export default new BitcoinBackendRoutes;
export default new BitcoinBackendRoutes

View File

@@ -22,11 +22,6 @@ import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i;
const ADDRESS_REGEX = /^[a-z0-9]{2,120}$/i;
const SCRIPT_HASH_REGEX = /^([a-f0-9]{2})+$/i;
class BitcoinRoutes {
public initRoutes(app: Application) {
app
@@ -54,8 +49,6 @@ class BitcoinRoutes {
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
.post(config.MEMPOOL.API_URL_PREFIX + 'prevouts', this.$getPrevouts)
.post(config.MEMPOOL.API_URL_PREFIX + 'cpfp', this.getCpfpLocalTxs)
// Temporarily add txs/package endpoint for all backends until esplora supports it
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
;
@@ -97,7 +90,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json');
res.send(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get init data');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -116,7 +109,7 @@ class BitcoinRoutes {
const result = mempoolBlocks.getMempoolBlocks();
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get mempool blocks');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -128,10 +121,7 @@ class BitcoinRoutes {
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
const txid = req.query.txId[_txId].toString();
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
txIds.push(req.query.txId[_txId].toString());
}
}
@@ -150,22 +140,18 @@ class BitcoinRoutes {
handleError(req, res, 400, 'Too many txids requested');
return;
}
if (txids.some((txid) => !TXID_REGEX.test(txid))) {
handleError(req, res, 400, 'Invalid txids format');
return;
}
try {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends);
} catch (e) {
handleError(req, res, 500, 'Failed to get batched outspends');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async $getCpfpInfo(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID.`);
return;
}
@@ -198,7 +184,7 @@ class BitcoinRoutes {
try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) {
handleError(req, res, 500, 'Failed to get CPFP info');
handleError(req, res, 500, 'failed to get CPFP info');
return;
}
}
@@ -219,10 +205,6 @@ class BitcoinRoutes {
}
private async getTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true);
res.json(transaction);
@@ -230,18 +212,12 @@ class BitcoinRoutes {
let statusCode = 500;
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
}
handleError(req, res, statusCode, 'Failed to get transaction');
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
}
}
private async getRawTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);
res.setHeader('content-type', 'text/plain');
@@ -250,10 +226,8 @@ class BitcoinRoutes {
let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
}
handleError(req, res, statusCode, 'Failed to get raw transaction');
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
}
}
@@ -318,18 +292,14 @@ class BitcoinRoutes {
}
} catch (e: any) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
handleError(req, res, 404, notFoundError);
handleError(req, res, 404, e.message);
} else {
handleError(req, res, 500, 'Failed to process PSBT');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}
private async getTransactionStatus(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction.status);
@@ -337,54 +307,36 @@ class BitcoinRoutes {
let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
}
handleError(req, res, statusCode, 'Failed to get transaction status');
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
}
}
private async getStrippedBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions);
} catch (e) {
handleError(req, res, 500, 'Failed to get block summary');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getStrippedBlockTransaction(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
if (!TXID_REGEX.test(req.params.txid)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
if (!transaction) {
handleError(req, res, 404, `Transaction not found in summary`);
handleError(req, res, 404, `transaction not found in summary`);
return;
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transaction);
} catch (e) {
handleError(req, res, 500, 'Failed to get transaction from summary');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const block = await blocks.$getBlock(req.params.hash);
@@ -396,69 +348,53 @@ class BitcoinRoutes {
} else if (blockAge > 30 * day) {
cacheDuration = 10 * day;
} else {
cacheDuration = 600;
cacheDuration = 600
}
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block);
} catch (e) {
handleError(req, res, 500, 'Failed to get block');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getBlockHeader(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash);
res.setHeader('content-type', 'text/plain');
res.send(blockHeader);
} catch (e) {
handleError(req, res, 500, 'Failed to get block header');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getBlockAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
handleError(req, res, 404, `Audit not available`);
handleError(req, res, 404, `audit not available`);
return;
}
} catch (e) {
handleError(req, res, 500, 'Failed to get block audit summary');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async $getBlockTxAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
if (!TXID_REGEX.test(req.params.txid)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
handleError(req, res, 404, `Transaction audit not available`);
handleError(req, res, 404, `transaction audit not available`);
return;
}
} catch (e) {
handleError(req, res, 500, 'Failed to get transaction audit summary');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
@@ -472,7 +408,7 @@ class BitcoinRoutes {
return await this.getLegacyBlocks(req, res);
}
} catch (e) {
handleError(req, res, 500, 'Failed to get blocks');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -514,7 +450,7 @@ class BitcoinRoutes {
res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) {
handleError(req, res, 500, 'Failed to get blocks');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -549,15 +485,11 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks);
} catch (e) {
handleError(req, res, 500, 'Failed to get blocks');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@@ -578,7 +510,7 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
handleError(req, res, 500, 'Failed to get block transactions');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -587,7 +519,7 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash);
} catch (e) {
handleError(req, res, 500, 'Failed to get block at height');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -596,20 +528,16 @@ class BitcoinRoutes {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try {
const addressData = await bitcoinApi.$getAddress(req.params.address);
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message);
handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
}
handleError(req, res, 500, 'Failed to get address');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -618,10 +546,6 @@ class BitcoinRoutes {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try {
let lastTxId: string = '';
@@ -632,10 +556,10 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message);
handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
}
handleError(req, res, 500, 'Failed to get address transactions');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -651,10 +575,6 @@ class BitcoinRoutes {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
return;
}
try {
// electrum expects scripthashes in little-endian
@@ -663,10 +583,10 @@ class BitcoinRoutes {
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message);
handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
}
handleError(req, res, 500, 'Failed to get script hash');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -675,10 +595,6 @@ class BitcoinRoutes {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return;
}
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
return;
}
try {
// electrum expects scripthashes in little-endian
@@ -691,10 +607,10 @@ class BitcoinRoutes {
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message);
handleError(req, res, 413, e instanceof Error ? e.message : e);
return;
}
handleError(req, res, 500, 'Failed to get script hash transactions');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -707,10 +623,10 @@ class BitcoinRoutes {
private async getAddressPrefix(req: Request, res: Response) {
try {
const addressPrefix = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(addressPrefix);
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(blockHash);
} catch (e) {
handleError(req, res, 500, 'Failed to get address prefix');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -751,7 +667,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain');
res.send(result.toString());
} catch (e) {
handleError(req, res, 500, 'Failed to get height at tip');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -761,55 +677,39 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain');
res.send(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get hash at tip');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getRawBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const result = await bitcoinApi.$getRawBlock(req.params.hash);
res.setHeader('content-type', 'application/octet-stream');
res.send(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get raw block');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getTxIdsForBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get txids for block');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async validateAddress(req: Request, res: Response) {
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try {
const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to validate address');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getRbfHistory(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const replacements = rbfCache.getRbfTree(req.params.txId) || null;
const replaces = rbfCache.getReplaces(req.params.txId) || null;
@@ -818,7 +718,7 @@ class BitcoinRoutes {
replaces
});
} catch (e) {
handleError(req, res, 500, 'Failed to get rbf history');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -827,7 +727,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(false);
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get rbf trees');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -836,15 +736,11 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(true);
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get full rbf replacements');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getCachedTx(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const result = rbfCache.getTx(req.params.txId);
if (result) {
@@ -853,20 +749,16 @@ class BitcoinRoutes {
res.status(204).send();
}
} catch (e) {
handleError(req, res, 500, 'Failed to get cached tx');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getTransactionOutspends(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get transaction outspends');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -879,7 +771,7 @@ class BitcoinRoutes {
handleError(req, res, 503, `Service Temporarily Unavailable`);
}
} catch (e) {
handleError(req, res, 500, 'Failed to get difficulty change');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -890,8 +782,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to send raw transaction');
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
@@ -902,8 +794,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to send raw transaction');
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
@@ -914,8 +806,8 @@ class BitcoinRoutes {
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to test transactions');
handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
@@ -927,97 +819,11 @@ class BitcoinRoutes {
const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined);
res.send(result);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to submit package');
handleError(req, res, 400, e.message && e.code ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
private async $getPrevouts(req: Request, res: Response) {
try {
const outpoints = req.body;
if (!Array.isArray(outpoints) || outpoints.some((item) => !/^[a-fA-F0-9]{64}$/.test(item.txid) || typeof item.vout !== 'number')) {
handleError(req, res, 400, 'Invalid outpoints format');
return;
}
if (outpoints.length > 100) {
handleError(req, res, 400, 'Too many outpoints requested');
return;
}
const result = Array(outpoints.length).fill(null);
const memPool = mempool.getMempool();
for (let i = 0; i < outpoints.length; i++) {
const outpoint = outpoints[i];
let prevout: IEsploraApi.Vout | null = null;
let unconfirmed: boolean | null = null;
const mempoolTx = memPool[outpoint.txid];
if (mempoolTx) {
if (outpoint.vout < mempoolTx.vout.length) {
prevout = mempoolTx.vout[outpoint.vout];
unconfirmed = true;
}
} else {
try {
const rawPrevout = await bitcoinClient.getTxOut(outpoint.txid, outpoint.vout, false);
if (rawPrevout) {
prevout = {
value: Math.round(rawPrevout.value * 100000000),
scriptpubkey: rawPrevout.scriptPubKey.hex,
scriptpubkey_asm: rawPrevout.scriptPubKey.asm ? transactionUtils.convertScriptSigAsm(rawPrevout.scriptPubKey.hex) : '',
scriptpubkey_type: transactionUtils.translateScriptPubKeyType(rawPrevout.scriptPubKey.type),
scriptpubkey_address: rawPrevout.scriptPubKey && rawPrevout.scriptPubKey.address ? rawPrevout.scriptPubKey.address : '',
};
unconfirmed = false;
}
} catch (e) {
// Ignore bitcoin client errors, just leave prevout as null
}
}
if (prevout) {
result[i] = { prevout, unconfirmed };
}
}
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get prevouts');
}
}
private getCpfpLocalTxs(req: Request, res: Response) {
try {
const transactions = req.body;
if (!Array.isArray(transactions) || transactions.some(tx =>
!tx || typeof tx !== 'object' ||
!/^[a-fA-F0-9]{64}$/.test(tx.txid) ||
typeof tx.weight !== 'number' ||
typeof tx.sigops !== 'number' ||
typeof tx.fee !== 'number' ||
!Array.isArray(tx.vin) ||
!Array.isArray(tx.vout)
)) {
handleError(req, res, 400, 'Invalid transactions format');
return;
}
if (transactions.length > 1) {
handleError(req, res, 400, 'More than one transaction is not supported yet');
return;
}
const cpfpInfo = calculateMempoolTxCpfp(transactions[0], mempool.getMempool(), true);
res.json([cpfpInfo]);
} catch (e) {
handleError(req, res, 500, 'Failed to calculate CPFP info');
}
}
}
export default new BitcoinRoutes();

View File

@@ -167,10 +167,8 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran
/**
* Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for
* that transaction (and all others in the same cluster)
* If the passed transaction is not guaranteed to be in the mempool, set localTx to true: this will
* prevent updating the CPFP data of other transactions in the cluster
*/
export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }, localTx: boolean = false): CpfpInfo {
export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) {
tx.cpfpDirty = false;
return {
@@ -200,27 +198,18 @@ export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool:
totalFee += tx.fees.base;
}
const effectiveFeePerVsize = totalFee / totalVsize;
if (localTx) {
tx.effectiveFeePerVsize = effectiveFeePerVsize;
tx.ancestors = Array.from(cluster.get(tx.txid)?.ancestors.values() || []).map(ancestor => ({ txid: ancestor.txid, weight: ancestor.weight, fee: ancestor.fees.base }));
tx.descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !cluster.get(tx.txid)?.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
tx.bestDescendant = null;
} else {
for (const tx of cluster.values()) {
mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
mempool[tx.txid].bestDescendant = null;
mempool[tx.txid].cpfpChecked = true;
mempool[tx.txid].cpfpDirty = true;
mempool[tx.txid].cpfpUpdated = Date.now();
}
tx = mempool[tx.txid];
for (const tx of cluster.values()) {
mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
mempool[tx.txid].bestDescendant = null;
mempool[tx.txid].cpfpChecked = true;
mempool[tx.txid].cpfpDirty = true;
mempool[tx.txid].cpfpUpdated = Date.now();
}
tx = mempool[tx.txid];
return {
ancestors: tx.ancestors || [],
bestDescendant: tx.bestDescendant || null,

View File

@@ -3,8 +3,6 @@ import { Application, Request, Response } from 'express';
import channelsApi from './channels.api';
import { handleError } from '../../utils/api';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
class ChannelsRoutes {
constructor() { }
@@ -25,7 +23,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels);
} catch (e) {
handleError(req, res, 500, 'Failed to search channels by id');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -41,7 +39,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel);
} catch (e) {
handleError(req, res, 500, 'Failed to get channel');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -72,7 +70,7 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString());
res.json(channels);
} catch (e) {
handleError(req, res, 500, 'Failed to get channels for node');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -85,10 +83,7 @@ class ChannelsRoutes {
const txIds: string[] = [];
for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') {
const txid = req.query.txId[_txId].toString();
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
txIds.push(req.query.txId[_txId].toString());
}
}
const channels = await channelsApi.$getChannelsByTransactionId(txIds);
@@ -113,7 +108,7 @@ class ChannelsRoutes {
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get channels by transaction ids');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -125,7 +120,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels);
} catch (e) {
handleError(req, res, 500, 'Failed to get penalty closed channels');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -138,7 +133,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels);
} catch (e) {
handleError(req, res, 500, 'Failed to get channel geodata');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}

View File

@@ -29,7 +29,7 @@ class GeneralLightningRoutes {
channels: channels,
});
} catch (e) {
handleError(req, res, 500, 'Failed to search for nodes and channels');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -43,7 +43,7 @@ class GeneralLightningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics);
} catch (e) {
handleError(req, res, 500, 'Failed to get lightning statistics');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -52,7 +52,7 @@ class GeneralLightningRoutes {
const statistics = await statisticsApi.$getLatestStatistics();
res.json(statistics);
} catch (e) {
handleError(req, res, 500, 'Failed to get lightning statistics');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}

View File

@@ -32,7 +32,7 @@ class NodesRoutes {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes);
} catch (e) {
handleError(req, res, 500, 'Failed to search for node');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -188,7 +188,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes);
} catch (e) {
handleError(req, res, 500, 'Failed to get node group');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -204,7 +204,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} catch (e) {
handleError(req, res, 500, 'Failed to get node');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -216,7 +216,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical node stats');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -232,7 +232,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node);
} catch (e) {
handleError(req, res, 500, 'Failed to get fee histogram');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -248,7 +248,7 @@ class NodesRoutes {
topByChannels: topChannelsNodes,
});
} catch (e) {
handleError(req, res, 500, 'Failed to get nodes ranking');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -260,7 +260,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
handleError(req, res, 500, 'Failed to get top nodes by capacity');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -272,7 +272,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
handleError(req, res, 500, 'Failed to get top nodes by channels');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -284,7 +284,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes);
} catch (e) {
handleError(req, res, 500, 'Failed to get oldest nodes');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -296,7 +296,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs);
} catch (e) {
handleError(req, res, 500, 'Failed to get ISP ranking');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -308,7 +308,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes);
} catch (e) {
handleError(req, res, 500, 'Failed to get world nodes');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -336,7 +336,7 @@ class NodesRoutes {
nodes: nodes,
});
} catch (e) {
handleError(req, res, 500, 'Failed to get nodes per country');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -363,7 +363,7 @@ class NodesRoutes {
nodes: nodes,
});
} catch (e) {
handleError(req, res, 500, 'Failed to get nodes per ISP');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -375,7 +375,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs);
} catch (e) {
handleError(req, res, 500, 'Failed to get nodes per country');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}

View File

@@ -83,7 +83,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(pegs);
} catch (e) {
handleError(req, res, 500, 'Failed to get pegs by month');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -95,7 +95,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(reserves);
} catch (e) {
handleError(req, res, 500, 'Failed to get reserves by month');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -107,7 +107,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentSupply);
} catch (e) {
handleError(req, res, 500, 'Failed to get pegs');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -119,7 +119,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentReserves);
} catch (e) {
handleError(req, res, 500, 'Failed to get reserves');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -131,7 +131,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(auditStatus);
} catch (e) {
handleError(req, res, 500, 'Failed to get federation audit status');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -143,7 +143,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses);
} catch (e) {
handleError(req, res, 500, 'Failed to get federation addresses');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -155,7 +155,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses);
} catch (e) {
handleError(req, res, 500, 'Failed to get federation addresses');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -167,7 +167,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos);
} catch (e) {
handleError(req, res, 500, 'Failed to get federation utxos');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -179,7 +179,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(expiredUtxos);
} catch (e) {
handleError(req, res, 500, 'Failed to get expired utxos');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -191,7 +191,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos);
} catch (e) {
handleError(req, res, 500, 'Failed to get federation utxos number');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -203,7 +203,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos);
} catch (e) {
handleError(req, res, 500, 'Failed to get emergency spent utxos');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -215,7 +215,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos);
} catch (e) {
handleError(req, res, 500, 'Failed to get emergency spent utxos stats');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -227,7 +227,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(recentPegs);
} catch (e) {
handleError(req, res, 500, 'Failed to get pegs list');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -239,7 +239,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsVolume);
} catch (e) {
handleError(req, res, 500, 'Failed to get pegs volume daily');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -251,7 +251,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsCount);
} catch (e) {
handleError(req, res, 500, 'Failed to get pegs count');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}

View File

@@ -72,7 +72,7 @@ class MiningRoutes {
}
res.status(200).send(response);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical prices');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -87,7 +87,7 @@ class MiningRoutes {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message);
} else {
handleError(req, res, 500, 'Failed to get pool');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}
@@ -106,7 +106,7 @@ class MiningRoutes {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message);
} else {
handleError(req, res, 500, 'Failed to get blocks for pool');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}
@@ -130,7 +130,7 @@ class MiningRoutes {
res.json(pools);
}
} catch (e) {
handleError(req, res, 500, 'Failed to get pools');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -144,7 +144,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats);
} catch (e) {
handleError(req, res, 500, 'Failed to get pools');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -158,7 +158,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates);
} catch (e) {
handleError(req, res, 500, 'Failed to get pools historical hashrate');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -175,7 +175,7 @@ class MiningRoutes {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message);
} else {
handleError(req, res, 500, 'Failed to get pool historical hashrate');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}
@@ -204,7 +204,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty,
});
} catch (e) {
handleError(req, res, 500, 'Failed to get historical hashrate');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -218,7 +218,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical block fees');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -236,7 +236,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical block fees');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -250,7 +250,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical block rewards');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -264,7 +264,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates);
} catch (e) {
handleError(req, res, 500, 'Failed to get historical block fee rates');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -282,7 +282,7 @@ class MiningRoutes {
weights: blockWeights
});
} catch (e) {
handleError(req, res, 500, 'Failed to get historical block size and weight');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -294,7 +294,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) {
handleError(req, res, 500, 'Failed to get historical difficulty adjustments');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -304,7 +304,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(response);
} catch (e) {
handleError(req, res, 500, 'Failed to get reward stats');
res.status(500).end();
}
}
@@ -318,7 +318,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) {
handleError(req, res, 500, 'Failed to get historical blocks health');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -336,7 +336,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit);
} catch (e) {
handleError(req, res, 500, 'Failed to get block audit');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -359,7 +359,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get height from timestamp');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -372,7 +372,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) {
handleError(req, res, 500, 'Failed to get block audit scores');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -385,7 +385,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit || 'null');
} catch (e) {
handleError(req, res, 500, 'Failed to get block audit score');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -400,7 +400,7 @@ class MiningRoutes {
}
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} catch (e) {
handleError(req, res, 500, 'Failed to get accelerations by pool');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -416,7 +416,7 @@ class MiningRoutes {
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} catch (e) {
handleError(req, res, 500, 'Failed to get accelerations by height');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -431,7 +431,7 @@ class MiningRoutes {
}
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} catch (e) {
handleError(req, res, 500, 'Failed to get recent accelerations');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -446,7 +446,7 @@ class MiningRoutes {
}
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} catch (e) {
handleError(req, res, 500, 'Failed to get acceleration totals');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -461,7 +461,7 @@ class MiningRoutes {
}
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
} catch (e) {
handleError(req, res, 500, 'Failed to get active accelerations');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
@@ -473,7 +473,7 @@ class MiningRoutes {
accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send();
} catch (e) {
handleError(req, res, 500, 'Failed to request acceleration');
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
}

View File

@@ -1,7 +1,6 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import WalletApi from './wallets';
import { handleError } from '../../utils/api';
class ServicesRoutes {
public initRoutes(app: Application): void {
@@ -19,7 +18,7 @@ class ServicesRoutes {
const wallet = await WalletApi.getWallet(walletId);
res.status(200).send(wallet);
} catch (e) {
handleError(req, res, 500, 'Failed to get wallet');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}

View File

@@ -1,7 +1,7 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import statisticsApi from './statistics-api';
import { handleError } from '../../utils/api';
class StatisticsRoutes {
public initRoutes(app: Application) {
app
@@ -65,7 +65,7 @@ class StatisticsRoutes {
}
res.json(result);
} catch (e) {
handleError(req, res, 500, 'Failed to get statistics');
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}

View File

@@ -420,29 +420,6 @@ class TransactionUtils {
return { prioritized, deprioritized };
}
// Copied from https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/bitcoin/bitcoin-api.ts#L324
public translateScriptPubKeyType(outputType: string): string {
const map = {
'pubkey': 'p2pk',
'pubkeyhash': 'p2pkh',
'scripthash': 'p2sh',
'witness_v0_keyhash': 'v0_p2wpkh',
'witness_v0_scripthash': 'v0_p2wsh',
'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard',
'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return'
};
if (map[outputType]) {
return map[outputType];
} else {
return 'unknown';
}
}
}
export default new TransactionUtils();

View File

@@ -324,9 +324,7 @@ class Server {
setUpHttpApiRoutes(): void {
bitcoinRoutes.initRoutes(this.app);
if (config.MEMPOOL.OFFICIAL) {
bitcoinCoreRoutes.initRoutes(this.app);
}
bitcoinCoreRoutes.initRoutes(this.app);
pricesRoutes.initRoutes(this.app);
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
statisticsRoutes.initRoutes(this.app);

View File

@@ -0,0 +1,51 @@
{
"theme": "wiz",
"enterprise": "river",
"branding": {
"name": "river",
"title": "river",
"site_id": 22,
"header_img": "/resources/riverlogo.svg",
"footer_img": "/resources/riverlogo.svg"
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"wallet": "RIVER"
}
},
{
"component": "twitter",
"mobileOrder": 5,
"props": {
"handle": "River"
}
},
{
"component": "wallet",
"mobileOrder": 2,
"props": {
"wallet": "RIVER",
"period": "all"
}
},
{
"component": "blocks"
},
{
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"wallet": "RIVER"
}
}
]
}
}

View File

@@ -23,9 +23,9 @@
"@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.7.2",
"@fortawesome/fontawesome-svg-core": "~6.7.2",
"@fortawesome/free-solid-svg-icons": "~6.7.2",
"@fortawesome/fontawesome-common-types": "~6.6.0",
"@fortawesome/fontawesome-svg-core": "~6.6.0",
"@fortawesome/free-solid-svg-icons": "~6.6.0",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0",
@@ -35,6 +35,7 @@
"domino": "^2.1.6",
"echarts": "~5.5.0",
"esbuild": "^0.24.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",
@@ -61,7 +62,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.17.0",
"cypress": "^13.15.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",
@@ -3112,10 +3113,9 @@
}
},
"node_modules/@cypress/request": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz",
"integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==",
"license": "Apache-2.0",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz",
"integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==",
"optional": true,
"dependencies": {
"aws-sign2": "~0.7.0",
@@ -3131,9 +3131,9 @@
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"performance-now": "^2.1.0",
"qs": "6.13.1",
"qs": "6.13.0",
"safe-buffer": "^5.1.2",
"tough-cookie": "^5.0.0",
"tough-cookie": "^4.1.3",
"tunnel-agent": "^0.6.0",
"uuid": "^8.3.2"
},
@@ -3141,22 +3141,6 @@
"node": ">= 6"
}
},
"node_modules/@cypress/request/node_modules/qs": {
"version": "6.13.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz",
"integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==",
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/@cypress/schematic": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-2.5.0.tgz",
@@ -3690,33 +3674,30 @@
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
"license": "MIT",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
"@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
"license": "(CC-BY-4.0 AND MIT)",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
"@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
@@ -5692,7 +5673,6 @@
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"safer-buffer": "~2.1.0"
@@ -5727,7 +5707,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.8"
@@ -5848,7 +5827,6 @@
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": "*"
@@ -5858,7 +5836,6 @@
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
"integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
"license": "MIT",
"optional": true
},
"node_modules/axios": {
@@ -6016,7 +5993,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"tweetnacl": "^0.14.3"
@@ -7092,7 +7068,6 @@
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
"license": "Apache-2.0",
"optional": true
},
"node_modules/chai": {
@@ -7195,16 +7170,15 @@
}
},
"node_modules/ci-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz",
"integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==",
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/sibiraj-s"
}
],
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8"
@@ -7979,14 +7953,13 @@
"peer": true
},
"node_modules/cypress": {
"version": "13.17.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz",
"integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==",
"version": "13.15.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz",
"integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@cypress/request": "^3.0.6",
"@cypress/request": "^3.0.4",
"@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2",
@@ -7997,7 +7970,6 @@
"cachedir": "^2.3.0",
"chalk": "^4.1.0",
"check-more-types": "^2.24.0",
"ci-info": "^4.0.0",
"cli-cursor": "^3.1.0",
"cli-table3": "~0.6.1",
"commander": "^6.2.1",
@@ -8012,6 +7984,7 @@
"figures": "^3.2.0",
"fs-extra": "^9.1.0",
"getos": "^3.2.1",
"is-ci": "^3.0.1",
"is-installed-globally": "~0.4.0",
"lazy-ass": "^1.6.0",
"listr2": "^3.8.3",
@@ -8026,7 +7999,6 @@
"semver": "^7.5.3",
"supports-color": "^8.1.1",
"tmp": "~0.2.3",
"tree-kill": "1.2.2",
"untildify": "^4.0.0",
"yauzl": "^2.10.0"
},
@@ -8229,7 +8201,6 @@
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
"license": "MIT",
"optional": true,
"dependencies": {
"assert-plus": "^1.0.0"
@@ -8716,7 +8687,6 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
"license": "MIT",
"optional": true,
"dependencies": {
"jsbn": "~0.1.0",
@@ -9935,7 +9905,6 @@
"engines": [
"node >=0.6.0"
],
"license": "MIT",
"optional": true
},
"node_modules/falafel": {
@@ -9952,6 +9921,11 @@
"node": ">=0.4.0"
}
},
"node_modules/fancy-canvas": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-0.2.2.tgz",
"integrity": "sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA=="
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -10219,7 +10193,6 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": "*"
@@ -10427,7 +10400,6 @@
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
"license": "MIT",
"optional": true,
"dependencies": {
"assert-plus": "^1.0.0"
@@ -10882,7 +10854,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz",
"integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==",
"license": "MIT",
"optional": true,
"dependencies": {
"assert-plus": "^1.0.0",
@@ -11249,6 +11220,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-ci": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
"integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
"optional": true,
"dependencies": {
"ci-info": "^3.2.0"
},
"bin": {
"is-ci": "bin.js"
}
},
"node_modules/is-core-module": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
@@ -11498,7 +11481,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
"license": "MIT",
"optional": true
},
"node_modules/is-unicode-supported": {
@@ -11563,7 +11545,6 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
"license": "MIT",
"optional": true
},
"node_modules/istanbul-lib-coverage": {
@@ -11697,7 +11678,6 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"license": "MIT",
"optional": true
},
"node_modules/jsesc": {
@@ -11726,7 +11706,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"license": "(AFL-2.1 OR BSD-3-Clause)",
"optional": true
},
"node_modules/json-schema-traverse": {
@@ -11744,7 +11723,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC",
"optional": true
},
"node_modules/json5": {
@@ -11805,7 +11783,6 @@
"engines": [
"node >=0.6.0"
],
"license": "MIT",
"optional": true,
"dependencies": {
"assert-plus": "1.0.0",
@@ -12129,6 +12106,14 @@
}
}
},
"node_modules/lightweight-charts": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-3.8.0.tgz",
"integrity": "sha512-7yFGnYuE1RjRJG9RwUTBz5wvF1QtjBOSW4FFlikr8Dh+/TDNt4ci+HsWSYmStgQUpawpvkCJ3j5/W25GppGj9Q==",
"dependencies": {
"fancy-canvas": "0.2.2"
}
},
"node_modules/limiter": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
@@ -14125,7 +14110,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": {
@@ -14556,6 +14540,12 @@
"node": ">= 0.10"
}
},
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
"optional": true
},
"node_modules/public-encrypt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
@@ -14671,6 +14661,12 @@
"node": ">=0.4.x"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"optional": true
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -16032,7 +16028,6 @@
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"asn1": "~0.2.3",
@@ -16582,26 +16577,6 @@
"readable-stream": "3"
}
},
"node_modules/tldts": {
"version": "6.1.70",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz",
"integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tldts-core": "^6.1.70"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.70",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz",
"integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==",
"license": "MIT",
"optional": true
},
"node_modules/tlite": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/tlite/-/tlite-0.1.9.tgz",
@@ -16646,16 +16621,27 @@
}
},
"node_modules/tough-cookie": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz",
"integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==",
"license": "BSD-3-Clause",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"optional": true,
"dependencies": {
"tldts": "^6.1.32"
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=16"
"node": ">=6"
}
},
"node_modules/tough-cookie/node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"optional": true,
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/transform-ast": {
@@ -16824,7 +16810,6 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"safe-buffer": "^5.0.1"
@@ -16837,7 +16822,6 @@
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
"license": "Unlicense",
"optional": true
},
"node_modules/type": {
@@ -17146,6 +17130,16 @@
"querystring": "0.2.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"optional": true,
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/url/node_modules/punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
@@ -17213,7 +17207,6 @@
"engines": [
"node >=0.6.0"
],
"license": "MIT",
"optional": true,
"dependencies": {
"assert-plus": "^1.0.0",
@@ -20355,9 +20348,9 @@
}
},
"@cypress/request": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz",
"integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz",
"integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==",
"optional": true,
"requires": {
"aws-sign2": "~0.7.0",
@@ -20373,22 +20366,11 @@
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"performance-now": "^2.1.0",
"qs": "6.13.1",
"qs": "6.13.0",
"safe-buffer": "^5.1.2",
"tough-cookie": "^5.0.0",
"tough-cookie": "^4.1.3",
"tunnel-agent": "^0.6.0",
"uuid": "^8.3.2"
},
"dependencies": {
"qs": {
"version": "6.13.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz",
"integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==",
"optional": true,
"requires": {
"side-channel": "^1.0.6"
}
}
}
},
"@cypress/schematic": {
@@ -20667,24 +20649,24 @@
}
},
"@fortawesome/fontawesome-common-types": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg=="
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw=="
},
"@fortawesome/fontawesome-svg-core": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
"requires": {
"@fortawesome/fontawesome-common-types": "6.7.2"
"@fortawesome/fontawesome-common-types": "6.6.0"
}
},
"@fortawesome/free-solid-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
"requires": {
"@fortawesome/fontawesome-common-types": "6.7.2"
"@fortawesome/fontawesome-common-types": "6.6.0"
}
},
"@goto-bus-stop/common-shake": {
@@ -23316,9 +23298,9 @@
"integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="
},
"ci-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz",
"integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==",
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
"optional": true
},
"cipher-base": {
@@ -23914,12 +23896,12 @@
"peer": true
},
"cypress": {
"version": "13.17.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz",
"integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==",
"version": "13.15.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz",
"integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==",
"optional": true,
"requires": {
"@cypress/request": "^3.0.6",
"@cypress/request": "^3.0.4",
"@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2",
@@ -23930,7 +23912,6 @@
"cachedir": "^2.3.0",
"chalk": "^4.1.0",
"check-more-types": "^2.24.0",
"ci-info": "^4.0.0",
"cli-cursor": "^3.1.0",
"cli-table3": "~0.6.1",
"commander": "^6.2.1",
@@ -23945,6 +23926,7 @@
"figures": "^3.2.0",
"fs-extra": "^9.1.0",
"getos": "^3.2.1",
"is-ci": "^3.0.1",
"is-installed-globally": "~0.4.0",
"lazy-ass": "^1.6.0",
"listr2": "^3.8.3",
@@ -23959,7 +23941,6 @@
"semver": "^7.5.3",
"supports-color": "^8.1.1",
"tmp": "~0.2.3",
"tree-kill": "1.2.2",
"untildify": "^4.0.0",
"yauzl": "^2.10.0"
},
@@ -25452,6 +25433,11 @@
"object-keys": "^1.0.6"
}
},
"fancy-canvas": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-0.2.2.tgz",
"integrity": "sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA=="
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -26387,6 +26373,15 @@
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
"integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ=="
},
"is-ci": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
"integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
"optional": true,
"requires": {
"ci-info": "^3.2.0"
}
},
"is-core-module": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
@@ -27020,6 +27015,14 @@
"webpack-sources": "^3.0.0"
}
},
"lightweight-charts": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-3.8.0.tgz",
"integrity": "sha512-7yFGnYuE1RjRJG9RwUTBz5wvF1QtjBOSW4FFlikr8Dh+/TDNt4ci+HsWSYmStgQUpawpvkCJ3j5/W25GppGj9Q==",
"requires": {
"fancy-canvas": "0.2.2"
}
},
"limiter": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
@@ -28803,6 +28806,12 @@
"event-stream": "=3.3.4"
}
},
"psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
"optional": true
},
"public-encrypt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
@@ -28894,6 +28903,12 @@
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
},
"querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"optional": true
},
"queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -30358,21 +30373,6 @@
}
}
},
"tldts": {
"version": "6.1.70",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz",
"integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==",
"optional": true,
"requires": {
"tldts-core": "^6.1.70"
}
},
"tldts-core": {
"version": "6.1.70",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz",
"integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==",
"optional": true
},
"tlite": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/tlite/-/tlite-0.1.9.tgz",
@@ -30405,12 +30405,23 @@
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
"tough-cookie": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz",
"integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"optional": true,
"requires": {
"tldts": "^6.1.32"
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"dependencies": {
"universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"optional": true
}
}
},
"transform-ast": {
@@ -30746,6 +30757,16 @@
}
}
},
"url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"optional": true,
"requires": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@@ -76,9 +76,9 @@
"@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.7.2",
"@fortawesome/fontawesome-svg-core": "~6.7.2",
"@fortawesome/free-solid-svg-icons": "~6.7.2",
"@fortawesome/fontawesome-common-types": "~6.6.0",
"@fortawesome/fontawesome-svg-core": "~6.6.0",
"@fortawesome/free-solid-svg-icons": "~6.6.0",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0",
@@ -87,6 +87,7 @@
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.5.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",
@@ -114,7 +115,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.17.0",
"cypress": "^13.15.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",

View File

@@ -439,39 +439,4 @@ export const fiatCurrencies = {
code: 'ZAR',
indexed: true,
},
};
export interface Timezone {
offset: string;
name: string;
}
export const timezones: Timezone[] = [
{ offset: '-12', name: 'Anywhere on Earth (AoE)' },
{ offset: '-11', name: 'Samoa Standard Time (SST)' },
{ offset: '-10', name: 'Hawaii Standard Time (HST)' },
{ offset: '-9', name: 'Alaska Standard Time (AKST)' },
{ offset: '-8', name: 'Pacific Standard Time (PST)' },
{ offset: '-7', name: 'Mountain Standard Time (MST)' },
{ offset: '-6', name: 'Central Standard Time (CST)' },
{ offset: '-5', name: 'Eastern Standard Time (EST)' },
{ offset: '-4', name: 'Atlantic Standard Time (AST)' },
{ offset: '-3', name: 'Argentina Time (ART)' },
{ offset: '-2', name: 'Fernando de Noronha Time (FNT)' },
{ offset: '-1', name: 'Azores Time (AZOT)' },
{ offset: '+0', name: 'Greenwich Mean Time (GMT)' },
{ offset: '+1', name: 'Central European Time (CET)' },
{ offset: '+2', name: 'Eastern European Time (EET)' },
{ offset: '+3', name: 'Moscow Standard Time (MSK)' },
{ offset: '+4', name: 'Armenia Time (AMT)' },
{ offset: '+5', name: 'Pakistan Standard Time (PKT)' },
{ offset: '+6', name: 'Xinjiang Time (XJT)' },
{ offset: '+7', name: 'Indochina Time (ICT)' },
{ offset: '+8', name: 'Hong Kong Time (HKT)' },
{ offset: '+9', name: 'Japan Standard Time (JST)' },
{ offset: '+10', name: 'Australian Eastern Standard Time (AEST)' },
{ offset: '+11', name: 'Norfolk Time (NFT)' },
{ offset: '+12', name: 'New Zealand Standard Time (NZST)' },
{ offset: '+13', name: 'Tonga Time (TOT)' },
{ offset: '+14', name: 'Line Islands Time (LINT)' }
];
};

View File

@@ -612,18 +612,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.processing = false;
return;
}
const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
if (!verificationToken) {
console.error(`SCA verification failed`);
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
this.servicesApiService.accelerateWithGooglePay$(
this.tx.txid,
tokenResult.token,
verificationToken,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD
@@ -751,32 +743,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
);
}
/**
* Required in SCA Mandated Regions: Learn more at https://developer.squareup.com/docs/sca-overview
*/
async $verifyBuyer(payments, token, details, amount) {
const verificationDetails = {
amount: amount,
currencyCode: 'USD',
intent: 'CHARGE',
billingContact: {
givenName: details.card?.billing?.givenName,
familyName: details.card?.billing?.familyName,
phone: details.card?.billing?.phone,
addressLines: details.card?.billing?.addressLines,
city: details.card?.billing?.city,
state: details.card?.billing?.state,
countryCode: details.card?.billing?.countryCode,
},
};
const verificationResults = await payments.verifyBuyer(
token,
verificationDetails,
);
return verificationResults.token;
}
/**
* BTCPay
*/

View File

@@ -49,7 +49,7 @@
</div>
</td>
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [hideTimeSince]="true"></app-timestamp>
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
</td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<a

View File

@@ -56,7 +56,8 @@
</ng-template>
</td>
<td class="timestamp text-left">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="utxo.blocktime"></app-timestamp>
&lrm;{{ utxo.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="utxo.blocktime"></app-time>)</i></div>
</td>
<td class="expires-in text-left" [ngStyle]="{ 'color': getGradientColor(utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate) }">
{{ utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate < 0 ? -(utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate) : utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate }} <span i18n="shared.blocks" class="symbol">blocks</span>

View File

@@ -53,7 +53,8 @@
</ng-container>
</td>
<td class="timestamp text-left">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="peg.blocktime"></app-timestamp>
&lrm;{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="peg.blocktime"></app-time>)</i></div>
</td>
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}">
<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>

View File

@@ -194,7 +194,7 @@
<a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a>
</td>
<td class="timestamp">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [hideTimeSince]="true"></app-timestamp>
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
</td>
<td class="mined">
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [showTooltip]="true"></app-time>

View File

@@ -1,8 +0,0 @@
<div [formGroup]="timezoneForm" class="text-small text-center">
<select formControlName="mode" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 110px;" (change)="changeMode()">
<option value="local">UTC{{ localTimezoneOffset !== '+0' ? localTimezoneOffset : '' }} {{ localTimezoneName ? '- ' + localTimezoneName : '' }}</option>
<option value="+0" *ngIf="localTimezoneOffset !== '+0'">UTC - Greenwich Mean Time (GMT)</option>
<option disabled>────</option>
<option *ngFor="let timezone of timezones" [value]="timezone.offset">UTC{{ timezone.offset !== '+0' ? timezone.offset : '' }} - {{ timezone.name }}</option>
</select>
</div>

View File

@@ -1,58 +0,0 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { StorageService } from '@app/services/storage.service';
import { StateService } from '@app/services/state.service';
import { timezones } from '@app/app.constants';
@Component({
selector: 'app-timezone-selector',
templateUrl: './timezone-selector.component.html',
styleUrls: ['./timezone-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimezoneSelectorComponent implements OnInit {
timezoneForm: UntypedFormGroup;
timezones = timezones;
localTimezoneOffset: string = '';
localTimezoneName: string;
constructor(
private formBuilder: UntypedFormBuilder,
private stateService: StateService,
private storageService: StorageService,
) { }
ngOnInit() {
this.setLocalTimezone();
this.timezoneForm = this.formBuilder.group({
mode: ['local'],
});
this.stateService.timezone$.subscribe((mode) => {
this.timezoneForm.get('mode')?.setValue(mode);
});
}
changeMode() {
const newMode = this.timezoneForm.get('mode')?.value;
this.storageService.setValue('timezone-preference', newMode);
this.stateService.timezone$.next(newMode);
}
setLocalTimezone() {
const offset = new Date().getTimezoneOffset();
const sign = offset <= 0 ? "+" : "-";
const absOffset = Math.abs(offset);
const hours = String(Math.floor(absOffset / 60));
const minutes = String(absOffset % 60).padStart(2, '0');
if (minutes === '00') {
this.localTimezoneOffset = `${sign}${hours}`;
} else {
this.localTimezoneOffset = `${sign}${hours.padStart(2, '0')}:${minutes}`;
}
const timezone = this.timezones.find(tz => tz.offset === this.localTimezoneOffset);
this.timezones = this.timezones.filter(tz => tz.offset !== this.localTimezoneOffset && tz.offset !== '+0');
this.localTimezoneName = timezone ? timezone.name : '';
}
}

View File

@@ -88,7 +88,7 @@
<div class="field narrower mt-2">
<div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
<div class="value">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="tx.status.block_time" [hideTimeSince]="true"></app-timestamp>
&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true" [showTooltip]="true"></app-time>)</i>
</div>

View File

@@ -1,56 +0,0 @@
<br>
<div class="title">
<h2 class="text-left" i18n="transaction.related-transactions|CPFP List">Related Transactions</h2>
</div>
<div class="box cpfp-details">
<table class="table table-fixed table-borderless table-striped">
<thead>
<tr>
<th i18n="transactions-list.vout.scriptpubkey-type">Type</th>
<th class="txids" i18n="dashboard.latest-transactions.txid">TXID</th>
<th *only-vsize class="d-none d-lg-table-cell" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</th>
<th *only-weight class="d-none d-lg-table-cell" i18n="transaction.weight|Transaction Weight">Weight</th>
<th i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
<th class="d-none d-lg-table-cell"></th>
</tr>
</thead>
<tbody>
<ng-template [ngIf]="cpfpInfo?.descendants?.length">
<tr *ngFor="let cpfpTx of cpfpInfo.descendants">
<td><span class="badge badge-primary" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td>
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
</td>
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight | wuBytes: 2"></td>
<td><app-fee-rate [fee]="cpfpTx.fee" [weight]="cpfpTx.weight"></app-fee-rate></td>
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) > roundToOneDecimal(tx)" class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
<ng-template [ngIf]="cpfpInfo?.bestDescendant">
<tr>
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td class="txids">
<app-truncate [text]="cpfpInfo.bestDescendant.txid" [link]="['/tx' | relativeUrl, cpfpInfo.bestDescendant.txid]"></app-truncate>
</td>
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight / 4 | vbytes: 2"></td>
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight | wuBytes: 2"></td>
<td><app-fee-rate [fee]="cpfpInfo.bestDescendant.fee" [weight]="cpfpInfo.bestDescendant.weight"></app-fee-rate></td>
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
<ng-template [ngIf]="cpfpInfo?.ancestors?.length">
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
<td class="txids">
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
</td>
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight | wuBytes: 2"></td>
<td><app-fee-rate [fee]="cpfpTx.fee" [weight]="cpfpTx.weight"></app-fee-rate></td>
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) < roundToOneDecimal(tx)" class="arrow-red" [icon]="['fas', 'angle-double-down']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
</tbody>
</table>
</div>

View File

@@ -1,32 +0,0 @@
.title {
h2 {
line-height: 1;
margin: 0;
padding-bottom: 5px;
}
}
.cpfp-details {
.txids {
width: 60%;
}
@media (max-width: 500px) {
.txids {
width: 40%;
}
}
}
.arrow-green {
color: var(--success);
}
.arrow-red {
color: var(--red);
}
.badge {
position: relative;
top: -1px;
}

View File

@@ -1,22 +0,0 @@
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
import { CpfpInfo } from '@interfaces/node-api.interface';
import { Transaction } from '@interfaces/electrs.interface';
@Component({
selector: 'app-cpfp-info',
templateUrl: './cpfp-info.component.html',
styleUrls: ['./cpfp-info.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CpfpInfoComponent implements OnInit {
@Input() cpfpInfo: CpfpInfo;
@Input() tx: Transaction;
constructor() {}
ngOnInit(): void {}
roundToOneDecimal(cpfpTx: any): number {
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
}
}

View File

@@ -61,7 +61,10 @@
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="tx.status.block_time"></app-timestamp>
&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
<div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
</div>
</td>
</tr>
} @else {

View File

@@ -1,205 +0,0 @@
<div class="container-xl">
@if (!transaction) {
<h1 style="margin-top: 19px;" i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</h1>
<form [formGroup]="pushTxForm" (submit)="decodeTransaction()" novalidate>
<div class="mb-3">
<textarea formControlName="txRaw" class="form-control" rows="5" i18n-placeholder="transaction.hex" placeholder="Transaction hex"></textarea>
</div>
<button [disabled]="isLoading" type="submit" class="btn btn-primary mr-2" i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</button>
<input type="checkbox" [checked]="!offlineMode" id="offline-mode" (change)="onOfflineModeChange($event)">
<label class="label" for="offline-mode">
<span i18n="transaction.fetch-prevout-data">Fetch prevout data</span>
</label>
<p *ngIf="error" class="red-color d-inline">Error decoding transaction, reason: {{ error }}</p>
</form>
}
@if (transaction && !error && !isLoading) {
<div class="title-block">
<h1 i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</h1>
<span class="tx-link">
<span class="txid">
<app-truncate [text]="transaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, transaction.txid]" [disabled]="!successBroadcast">
<app-clipboard [text]="transaction.txid"></app-clipboard>
</app-truncate>
</span>
</span>
<div class="container-buttons">
<button *ngIf="!successBroadcast" [disabled]="isLoadingBroadcast" type="button" class="btn btn-sm btn-primary" i18n="transaction.broadcast|Broadcast" (click)="postTx()">Broadcast</button>
<button *ngIf="successBroadcast" type="button" class="btn btn-sm btn-success no-cursor" i18n="transaction.broadcasted|Broadcasted">Broadcasted</button>
<button class="btn btn-sm" style="margin-left: 10px; padding: 0;" (click)="resetForm()">&#10005;</button>
</div>
</div>
<p class="red-color d-inline">{{ errorBroadcast }}</p>
<div class="clearfix"></div>
@if (!hasPrevouts) {
<div class="alert alert-mempool">
@if (offlineMode) {
<span><strong>Prevouts are not loaded, some fields like fee rate cannot be displayed.</strong></span>
} @else {
<span><strong>Error loading prevouts</strong>. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }}</span>
}
</div>
}
@if (errorCpfpInfo) {
<div class="alert alert-mempool">
<span><strong>Error loading CPFP data</strong>. Reason: {{ errorCpfpInfo }}</span>
</div>
}
<app-transaction-details
[network]="stateService.network"
[tx]="transaction"
[isLoadingTx]="false"
[isMobile]="isMobile"
[isLoadingFirstSeen]="false"
[featuresEnabled]="true"
[filters]="filters"
[hasEffectiveFeeRate]="false"
[cpfpInfo]="null"
[ETA$]="ETA$"
[hasEffectiveFeeRate]="hasEffectiveFeeRate"
[cpfpInfo]="cpfpInfo"
[hasCpfp]="hasCpfp"
(toggleCpfp$)="this.showCpfpDetails = !this.showCpfpDetails"
></app-transaction-details>
<app-cpfp-info *ngIf="showCpfpDetails" [cpfpInfo]="cpfpInfo" [tx]="transaction"></app-cpfp-info>
<br>
<ng-container *ngIf="flowEnabled; else flowPlaceholder">
<div class="title float-left">
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
</div>
<button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="toggleGraph()" i18n="hide-diagram">Hide diagram</button>
<div class="clearfix"></div>
<div class="box">
<div class="graph-container" #graphContainer>
<tx-bowtie-graph
[tx]="transaction"
[cached]="true"
[width]="graphWidth"
[height]="graphHeight"
[lineLimit]="inOutLimit"
[maxStrands]="graphExpanded ? maxInOut : 24"
[network]="stateService.network"
[tooltip]="true"
[connectors]="true"
[inputIndex]="null" [outputIndex]="null"
>
</tx-bowtie-graph>
</div>
<div class="toggle-wrapper" *ngIf="maxInOut > 24">
<button class="btn btn-sm btn-primary graph-toggle" (click)="expandGraph();" *ngIf="!graphExpanded; else collapseBtn"><span i18n="show-more">Show more</span></button>
<ng-template #collapseBtn>
<button class="btn btn-sm btn-primary graph-toggle" (click)="collapseGraph();"><span i18n="show-less">Show less</span></button>
</ng-template>
</div>
</div>
<br>
</ng-container>
<ng-template #flowPlaceholder>
<div class="box hidden">
<div class="graph-container" #graphContainer>
</div>
</div>
</ng-template>
<div class="subtitle-block">
<div class="title">
<h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
</div>
<div class="title-buttons">
<button *ngIf="!flowEnabled" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show-diagram">Show diagram</button>
<button type="button" class="btn btn-outline-info btn-sm" (click)="txList.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
</div>
</div>
<app-transactions-list #txList [transactions]="[transaction]" [transactionPage]="true" [txPreview]="true"></app-transactions-list>
<div class="title text-left">
<h2 i18n="transaction.details|Transaction Details">Details</h2>
</div>
<div class="box">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="block.size">Size</td>
<td [innerHTML]="'&lrm;' + (transaction.size | bytes: 2)"></td>
</tr>
<tr>
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td [innerHTML]="'&lrm;' + (transaction.weight / 4 | vbytes: 2)"></td>
</tr>
<tr *ngIf="adjustedVsize">
<td><ng-container i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize</ng-container>
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-is-adjusted-vsize">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</td>
<td [innerHTML]="'&lrm;' + (adjustedVsize | vbytes: 2)"></td>
</tr>
<tr>
<td i18n="block.weight">Weight</td>
<td [innerHTML]="'&lrm;' + (transaction.weight | wuBytes: 2)"></td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="transaction.version">Version</td>
<td [innerHTML]="'&lrm;' + (transaction.version | number)"></td>
</tr>
<tr>
<td i18n="transaction.locktime">Locktime</td>
<td [innerHTML]="'&lrm;' + (transaction.locktime | number)"></td>
</tr>
<tr *ngIf="transaction.sigops >= 0">
<td><ng-container i18n="transaction.sigops|Transaction Sigops">Sigops</ng-container>
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-are-sigops">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</td>
<td [innerHTML]="'&lrm;' + (transaction.sigops | number)"></td>
</tr>
<tr>
<td i18n="transaction.hex">Transaction hex</td>
<td><app-clipboard [text]="pushTxForm.get('txRaw').value" [leftPadding]="false"></app-clipboard></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
}
@if (isLoading) {
<div class="text-center">
<div class="spinner-border text-light mt-2 mb-2"></div>
<h3 i18n="transaction.error.loading-prevouts">
Loading {{ isLoadingPrevouts ? 'transaction prevouts' : isLoadingCpfpInfo ? 'CPFP' : '' }}
</h3>
</div>
}
</div>

View File

@@ -1,194 +0,0 @@
.label {
margin: 0 5px;
}
.container-buttons {
align-self: center;
}
.title-block {
flex-wrap: wrap;
align-items: baseline;
@media (min-width: 650px) {
flex-direction: row;
}
h1 {
margin: 0rem;
margin-right: 15px;
line-height: 1;
}
}
.tx-link {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: baseline;
width: 0;
max-width: 100%;
margin-right: 0px;
margin-bottom: 0px;
margin-top: 8px;
@media (min-width: 651px) {
flex-grow: 1;
margin-bottom: 0px;
margin-right: 1em;
top: 1px;
position: relative;
}
@media (max-width: 650px) {
width: 100%;
order: 3;
}
.txid {
width: 200px;
min-width: 200px;
flex-grow: 1;
}
}
.container-xl {
margin-bottom: 40px;
}
.row {
flex-direction: column;
@media (min-width: 850px) {
flex-direction: row;
}
}
.box.hidden {
visibility: hidden;
height: 0px;
padding-top: 0px;
padding-bottom: 0px;
margin-top: 0px;
margin-bottom: 0px;
}
.graph-container {
position: relative;
width: 100%;
background: var(--stat-box-bg);
padding: 10px 0;
padding-bottom: 0;
}
.toggle-wrapper {
width: 100%;
text-align: center;
margin: 1.25em 0 0;
}
.graph-toggle {
margin: auto;
}
.table {
tr td {
padding: 0.75rem 0.5rem;
@media (min-width: 576px) {
padding: 0.75rem 0.75rem;
}
&:last-child {
text-align: right;
@media (min-width: 850px) {
text-align: left;
}
}
.btn {
display: block;
}
&.wrap-cell {
white-space: normal;
}
}
}
.effective-fee-container {
display: block;
@media (min-width: 768px){
display: inline-block;
}
@media (max-width: 425px){
display: flex;
flex-direction: column;
}
}
.effective-fee-rating {
@media (max-width: 767px){
margin-right: 0px !important;
}
}
.title {
h2 {
line-height: 1;
margin: 0;
padding-bottom: 5px;
}
}
.btn-outline-info {
margin-top: 5px;
@media (min-width: 768px){
margin-top: 0px;
}
}
.flow-toggle {
margin-top: -5px;
margin-left: 10px;
@media (min-width: 768px){
display: inline-block;
margin-top: 0px;
margin-bottom: 0px;
}
}
.subtitle-block {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
.title {
flex-shrink: 0;
}
.title-buttons {
flex-shrink: 1;
text-align: right;
.btn {
margin-top: 0;
margin-bottom: 8px;
margin-left: 8px;
}
}
}
.cpfp-details {
.txids {
width: 60%;
}
@media (max-width: 500px) {
.txids {
width: 40%;
}
}
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
.no-cursor {
cursor: default !important;
pointer-events: none;
}

View File

@@ -1,328 +0,0 @@
import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy } from '@angular/core';
import { Transaction, Vout } from '@interfaces/electrs.interface';
import { StateService } from '../../services/state.service';
import { Filter, toFilters } from '../../shared/filters.utils';
import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils';
import { ETA, EtaService } from '../../services/eta.service';
import { combineLatest, firstValueFrom, map, Observable, startWith, Subscription } from 'rxjs';
import { WebsocketService } from '../../services/websocket.service';
import { ActivatedRoute, Router } from '@angular/router';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { ApiService } from '../../services/api.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { CpfpInfo } from '../../interfaces/node-api.interface';
@Component({
selector: 'app-transaction-raw',
templateUrl: './transaction-raw.component.html',
styleUrls: ['./transaction-raw.component.scss'],
})
export class TransactionRawComponent implements OnInit, OnDestroy {
pushTxForm: UntypedFormGroup;
isLoading: boolean;
isLoadingPrevouts: boolean;
isLoadingCpfpInfo: boolean;
offlineMode: boolean = false;
transaction: Transaction;
error: string;
errorPrevouts: string;
errorCpfpInfo: string;
hasPrevouts: boolean;
missingPrevouts: string[];
isLoadingBroadcast: boolean;
errorBroadcast: string;
successBroadcast: boolean;
isMobile: boolean;
@ViewChild('graphContainer')
graphContainer: ElementRef;
graphExpanded: boolean = false;
graphWidth: number = 1068;
graphHeight: number = 360;
inOutLimit: number = 150;
maxInOut: number = 0;
flowPrefSubscription: Subscription;
hideFlow: boolean = this.stateService.hideFlow.value;
flowEnabled: boolean;
adjustedVsize: number;
filters: Filter[] = [];
hasEffectiveFeeRate: boolean;
fetchCpfp: boolean;
cpfpInfo: CpfpInfo | null;
hasCpfp: boolean = false;
showCpfpDetails = false;
ETA$: Observable<ETA | null>;
mempoolBlocksSubscription: Subscription;
constructor(
public route: ActivatedRoute,
public router: Router,
public stateService: StateService,
public etaService: EtaService,
public electrsApi: ElectrsApiService,
public websocketService: WebsocketService,
public formBuilder: UntypedFormBuilder,
public seoService: SeoService,
public apiService: ApiService,
public relativeUrlPipe: RelativeUrlPipe,
) {}
ngOnInit(): void {
this.seoService.setTitle($localize`:@@meta.title.preview-tx:Preview Transaction`);
this.seoService.setDescription($localize`:@@meta.description.preview-tx:Preview a transaction to the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network using the transaction's raw hex data.`);
this.websocketService.want(['blocks', 'mempool-blocks']);
this.pushTxForm = this.formBuilder.group({
txRaw: ['', Validators.required],
});
}
async decodeTransaction(): Promise<void> {
this.resetState();
this.isLoading = true;
try {
const tx = decodeRawTransaction(this.pushTxForm.get('txRaw').value, this.stateService.network);
await this.fetchPrevouts(tx);
await this.fetchCpfpInfo(tx);
this.processTransaction(tx);
} catch (error) {
this.error = error.message;
} finally {
this.isLoading = false;
}
}
async fetchPrevouts(transaction: Transaction): Promise<void> {
if (this.offlineMode) {
return;
}
const prevoutsToFetch = transaction.vin.map((input) => ({ txid: input.txid, vout: input.vout }));
if (!prevoutsToFetch.length || transaction.vin[0].is_coinbase) {
this.hasPrevouts = true;
return;
}
try {
this.missingPrevouts = [];
this.isLoadingPrevouts = true;
const prevouts: { prevout: Vout, unconfirmed: boolean }[] = await firstValueFrom(this.apiService.getPrevouts$(prevoutsToFetch));
if (prevouts?.length !== prevoutsToFetch.length) {
throw new Error();
}
transaction.vin = transaction.vin.map((input, index) => {
if (prevouts[index]) {
input.prevout = prevouts[index].prevout;
addInnerScriptsToVin(input);
} else {
this.missingPrevouts.push(`${input.txid}:${input.vout}`);
}
return input;
});
if (this.missingPrevouts.length) {
throw new Error(`Some prevouts do not exist or are already spent (${this.missingPrevouts.length})`);
}
transaction.fee = transaction.vin.some(input => input.is_coinbase)
? 0
: transaction.vin.reduce((fee, input) => {
return fee + (input.prevout?.value || 0);
}, 0) - transaction.vout.reduce((sum, output) => sum + output.value, 0);
transaction.feePerVsize = transaction.fee / (transaction.weight / 4);
transaction.sigops = countSigops(transaction);
this.hasPrevouts = true;
this.isLoadingPrevouts = false;
this.fetchCpfp = prevouts.some(prevout => prevout?.unconfirmed);
} catch (error) {
console.log(error);
this.errorPrevouts = error?.error?.error || error?.message;
this.isLoadingPrevouts = false;
}
}
async fetchCpfpInfo(transaction: Transaction): Promise<void> {
// Fetch potential cpfp data if all prevouts were parsed successfully and at least one of them is unconfirmed
if (this.hasPrevouts && this.fetchCpfp) {
try {
this.isLoadingCpfpInfo = true;
const cpfpInfo: CpfpInfo[] = await firstValueFrom(this.apiService.getCpfpLocalTx$([{
txid: transaction.txid,
weight: transaction.weight,
sigops: transaction.sigops,
fee: transaction.fee,
vin: transaction.vin,
vout: transaction.vout
}]));
if (cpfpInfo?.[0]?.ancestors?.length) {
const { ancestors, effectiveFeePerVsize } = cpfpInfo[0];
transaction.effectiveFeePerVsize = effectiveFeePerVsize;
this.cpfpInfo = { ancestors, effectiveFeePerVsize };
this.hasCpfp = true;
this.hasEffectiveFeeRate = true;
}
this.isLoadingCpfpInfo = false;
} catch (error) {
this.errorCpfpInfo = error?.error?.error || error?.message;
this.isLoadingCpfpInfo = false;
}
}
}
processTransaction(tx: Transaction): void {
this.transaction = tx;
this.transaction.flags = getTransactionFlags(this.transaction, null, null, null, this.stateService.network);
this.filters = this.transaction.flags ? toFilters(this.transaction.flags).filter(f => f.txPage) : [];
if (this.transaction.sigops >= 0) {
this.adjustedVsize = Math.max(this.transaction.weight / 4, this.transaction.sigops * 5);
}
this.setupGraph();
this.setFlowEnabled();
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
this.hideFlow = !!hide;
this.setFlowEnabled();
});
this.setGraphSize();
this.ETA$ = combineLatest([
this.stateService.mempoolBlocks$.pipe(startWith(null)),
this.stateService.difficultyAdjustment$.pipe(startWith(null)),
]).pipe(
map(([mempoolBlocks, da]) => {
return this.etaService.calculateETA(
this.stateService.network,
this.transaction,
mempoolBlocks,
null,
da,
null,
null,
null
);
})
);
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe(() => {
if (this.transaction) {
this.stateService.markBlock$.next({
txid: this.transaction.txid,
txFeePerVSize: this.transaction.effectiveFeePerVsize || this.transaction.feePerVsize,
});
}
});
}
async postTx(): Promise<string> {
this.isLoadingBroadcast = true;
this.errorBroadcast = null;
return new Promise((resolve, reject) => {
this.apiService.postTransaction$(this.pushTxForm.get('txRaw').value)
.subscribe((result) => {
this.isLoadingBroadcast = false;
this.successBroadcast = true;
this.transaction.txid = result;
resolve(result);
},
(error) => {
if (typeof error.error === 'string') {
const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"');
this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error);
} else if (error.message) {
this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + error.message;
}
this.isLoadingBroadcast = false;
reject(this.error);
});
});
}
resetState() {
this.transaction = null;
this.error = null;
this.errorPrevouts = null;
this.errorBroadcast = null;
this.successBroadcast = false;
this.isLoading = false;
this.isLoadingPrevouts = false;
this.isLoadingCpfpInfo = false;
this.isLoadingBroadcast = false;
this.adjustedVsize = null;
this.showCpfpDetails = false;
this.hasCpfp = false;
this.fetchCpfp = false;
this.cpfpInfo = null;
this.hasEffectiveFeeRate = false;
this.filters = [];
this.hasPrevouts = false;
this.missingPrevouts = [];
this.stateService.markBlock$.next({});
this.mempoolBlocksSubscription?.unsubscribe();
}
resetForm() {
this.resetState();
this.pushTxForm.reset();
}
@HostListener('window:resize', ['$event'])
setGraphSize(): void {
this.isMobile = window.innerWidth < 850;
if (this.graphContainer?.nativeElement && this.stateService.isBrowser) {
setTimeout(() => {
if (this.graphContainer?.nativeElement?.clientWidth) {
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
} else {
setTimeout(() => { this.setGraphSize(); }, 1);
}
}, 1);
} else {
setTimeout(() => { this.setGraphSize(); }, 1);
}
}
setupGraph() {
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.transaction?.vin?.length || 1, this.transaction?.vout?.length + 1 || 1));
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);
}
toggleGraph() {
const showFlow = !this.flowEnabled;
this.stateService.hideFlow.next(!showFlow);
}
setFlowEnabled() {
this.flowEnabled = !this.hideFlow;
}
expandGraph() {
this.graphExpanded = true;
this.graphHeight = this.maxInOut * 15;
}
collapseGraph() {
this.graphExpanded = false;
this.graphHeight = Math.min(360, this.maxInOut * 80);
}
onOfflineModeChange(e): void {
this.offlineMode = !e.target.checked;
}
ngOnDestroy(): void {
this.mempoolBlocksSubscription?.unsubscribe();
this.flowPrefSubscription?.unsubscribe();
this.stateService.markBlock$.next({});
}
}

View File

@@ -66,7 +66,64 @@
<ng-template [ngIf]="!isLoadingTx && !error">
<!-- CPFP Details -->
<app-cpfp-info *ngIf="showCpfpDetails" [cpfpInfo]="cpfpInfo" [tx]="tx"></app-cpfp-info>
<ng-template [ngIf]="showCpfpDetails">
<br>
<div class="title">
<h2 class="text-left" i18n="transaction.related-transactions|CPFP List">Related Transactions</h2>
</div>
<div class="box cpfp-details">
<table class="table table-fixed table-borderless table-striped">
<thead>
<tr>
<th i18n="transactions-list.vout.scriptpubkey-type">Type</th>
<th class="txids" i18n="dashboard.latest-transactions.txid">TXID</th>
<th *only-vsize class="d-none d-lg-table-cell" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</th>
<th *only-weight class="d-none d-lg-table-cell" i18n="transaction.weight|Transaction Weight">Weight</th>
<th i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
<th class="d-none d-lg-table-cell"></th>
</tr>
</thead>
<tbody>
<ng-template [ngIf]="cpfpInfo?.descendants?.length">
<tr *ngFor="let cpfpTx of cpfpInfo.descendants">
<td><span class="badge badge-primary" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td>
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
</td>
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight | wuBytes: 2"></td>
<td><app-fee-rate [fee]="cpfpTx.fee" [weight]="cpfpTx.weight"></app-fee-rate></td>
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) > roundToOneDecimal(tx)" class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
<ng-template [ngIf]="cpfpInfo?.bestDescendant">
<tr>
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td class="txids">
<app-truncate [text]="cpfpInfo.bestDescendant.txid" [link]="['/tx' | relativeUrl, cpfpInfo.bestDescendant.txid]"></app-truncate>
</td>
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight / 4 | vbytes: 2"></td>
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight | wuBytes: 2"></td>
<td><app-fee-rate [fee]="cpfpInfo.bestDescendant.fee" [weight]="cpfpInfo.bestDescendant.weight"></app-fee-rate></td>
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
<ng-template [ngIf]="cpfpInfo?.ancestors?.length">
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
<td class="txids">
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
</td>
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight | wuBytes: 2"></td>
<td><app-fee-rate [fee]="cpfpTx.fee" [weight]="cpfpTx.weight"></app-fee-rate></td>
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) < roundToOneDecimal(tx)" class="arrow-red" [icon]="['fas', 'angle-double-down']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
</tbody>
</table>
</div>
</ng-template>
<!-- Accelerator -->
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary && (ETA$ | async) as eta;">

View File

@@ -227,6 +227,18 @@
}
}
.cpfp-details {
.txids {
width: 60%;
}
@media (max-width: 500px) {
.txids {
width: 40%;
}
}
}
.tx-list {
.alert-link {
display: block;

View File

@@ -1054,6 +1054,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.stateService.markBlock$.next({});
}
roundToOneDecimal(cpfpTx: any): number {
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
}
setupGraph() {
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1));
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);

View File

@@ -9,8 +9,6 @@ import { TransactionExtrasModule } from '@components/transaction/transaction-ext
import { GraphsModule } from '@app/graphs/graphs.module';
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
import { AccelerateFeeGraphComponent } from '@components/accelerate-checkout/accelerate-fee-graph.component';
import { TransactionRawComponent } from '@components/transaction/transaction-raw.component';
import { CpfpInfoComponent } from '@components/transaction/cpfp-info.component';
const routes: Routes = [
{
@@ -18,10 +16,6 @@ const routes: Routes = [
redirectTo: '/',
pathMatch: 'full',
},
{
path: 'preview',
component: TransactionRawComponent,
},
{
path: ':id',
component: TransactionComponent,
@@ -55,15 +49,12 @@ export class TransactionRoutingModule { }
TransactionDetailsComponent,
AccelerateCheckout,
AccelerateFeeGraphComponent,
TransactionRawComponent,
CpfpInfoComponent,
],
exports: [
TransactionComponent,
TransactionDetailsComponent,
AccelerateCheckout,
AccelerateFeeGraphComponent,
CpfpInfoComponent,
]
})
export class TransactionModule { }

View File

@@ -6,7 +6,7 @@
<app-truncate [text]="tx.txid"></app-truncate>
</a>
<div>
<ng-template [ngIf]="tx.status.confirmed"><app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="tx.status.block_time" [hideTimeSince]="true"></app-timestamp></ng-template>
<ng-template [ngIf]="tx.status.confirmed">&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}</ng-template>
<ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
<i><app-time kind="since" [time]="tx.firstSeen" [fastRender]="true" [showTooltip]="true"></app-time></i>
</ng-template>
@@ -81,7 +81,7 @@
</ng-container>
</div>
</td>
<td class="text-right nowrap amount" [class]="{large: tx.largeInput}">
<td class="text-right nowrap amount" [class]="{large: vin?.prevout?.value > 1000000000 || vin.isInscription}">
<button *ngIf="vin.isInscription" (click)="toggleOrdData(tx.txid, 'vin', vindex)" type="button" class="btn btn-sm badge badge-ord primary" style="margin-right: 10px;">Inscription</button>
<ng-template [ngIf]="vin.prevout && vin.prevout.asset && vin.prevout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
<div *ngIf="assetsMinimal && assetsMinimal[vin.prevout.asset] else assetVinNotFound">
@@ -257,7 +257,7 @@
</ng-template>
</ng-template>
</td>
<td class="text-right nowrap amount" [class]="{large: tx.largeOutput}">
<td class="text-right nowrap amount" [class]="{large: vout?.value > 1000000000}">
<ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
<div *ngIf="assetsMinimal && assetsMinimal[vout.asset] else assetNotFound">
<ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container>

View File

@@ -37,7 +37,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
@Input() addresses: string[] = [];
@Input() rowLimit = 12;
@Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block
@Input() txPreview = false;
@Output() loadMore = new EventEmitter();
@@ -82,7 +81,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
this.refreshOutspends$
.pipe(
switchMap((txIds) => {
if (!this.cached && !this.txPreview) {
if (!this.cached) {
// break list into batches of 50 (maximum supported by esplora)
const batches = [];
for (let i = 0; i < txIds.length; i += 50) {
@@ -120,7 +119,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
),
this.refreshChannels$
.pipe(
filter(() => this.stateService.networkSupportsLightning() && !this.txPreview),
filter(() => this.stateService.networkSupportsLightning()),
switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)),
catchError((error) => {
// handle 404
@@ -188,10 +187,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}
this.transactionsLength = this.transactions.length;
if (!this.txPreview) {
this.cacheService.setTxCache(this.transactions);
}
this.cacheService.setTxCache(this.transactions);
const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
this.transactions.forEach((tx) => {
@@ -262,7 +258,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
const hasAnnex = tx.vin[i].witness?.[tx.vin[i].witness.length - 1].startsWith('50');
if (tx.vin[i].witness.length > (hasAnnex ? 2 : 1) && tx.vin[i].witness[tx.vin[i].witness.length - (hasAnnex ? 3 : 2)].includes('0063036f7264')) {
tx.vin[i].isInscription = true;
tx.largeInput = true;
}
}
}
@@ -273,9 +268,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}
}
}
tx.largeInput = tx.largeInput || tx.vin.some(vin => (vin?.prevout?.value > 1000000000));
tx.largeOutput = tx.vout.some(vout => (vout?.value > 1000000000));
});
if (this.blockTime && this.transactions?.length && this.currency) {
@@ -355,16 +347,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}
loadMoreInputs(tx: Transaction): void {
if (!tx['@vinLoaded'] && !this.txPreview) {
if (!tx['@vinLoaded']) {
this.electrsApiService.getTransaction$(tx.txid)
.subscribe((newTx) => {
tx['@vinLoaded'] = true;
let temp = tx.vin;
tx.vin = newTx.vin;
tx.fee = newTx.fee;
for (const [index, vin] of temp.entries()) {
newTx.vin[index].isInscription = vin.isInscription;
}
this.ref.markForCheck();
});
}

View File

@@ -32,8 +32,6 @@ export interface Transaction {
price?: Price;
sigops?: number;
flags?: bigint;
largeInput?: boolean;
largeOutput?: boolean;
}
export interface TransactionChannels {

View File

@@ -21,7 +21,7 @@
<tbody>
<tr>
<td i18n="lightning.created">Created</td>
<td><app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="channel.created" [hideTimeSince]="true"></app-timestamp></td>
<td>{{ channel.created | date:'yyyy-MM-dd HH:mm' }}</td>
</tr>
<tr>
<td i18n="lightning.capacity">Capacity</td>

View File

@@ -19,7 +19,7 @@
<ng-container *ngFor="let channel of channels;">
<tr>
<td class="timestamp">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="channel.closing_date" [hideTimeSince]="true"></app-timestamp>
&lrm;{{ channel.closing_date | date:'yyyy-MM-dd HH:mm' }}
</td>
<td class="capacity text-right">
<app-amount *ngIf="channel.capacity > 100000000; else smallnode" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>

View File

@@ -14,7 +14,7 @@ class GuardService {
trackerGuard(route: Route, segments: UrlSegment[]): boolean {
const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode;
const path = this.router.getCurrentNavigation()?.extractedUrl.root.children.primary.segments;
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test', 'preview'].includes(path[1].path));
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test'].includes(path[1].path));
}
}

View File

@@ -565,14 +565,6 @@ export class ApiService {
return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/acceleration/request/' + txid, '');
}
getPrevouts$(outpoints: {txid: string; vout: number}[]): Observable<any> {
return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/prevouts', outpoints);
}
getCpfpLocalTx$(tx: any[]): Observable<CpfpInfo[]> {
return this.httpClient.post<CpfpInfo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/cpfp', tx);
}
// Cache methods
async setBlockAuditLoaded(hash: string) {
this.blockAuditLoaded[hash] = true;

View File

@@ -55,7 +55,7 @@ export class EtaService {
return {
hashratePercentage: acceleratingHashrateFraction * 100,
ETA: Date.now() + da.adjustedTimeAvg * mempoolPosition.block,
ETA: Date.now() + da.timeAvg * mempoolPosition.block,
acceleratedETA: this.calculateETAFromShares([
{ block: mempoolPosition.block, hashrateShare: (1 - acceleratingHashrateFraction) },
{ block: 0, hashrateShare: acceleratingHashrateFraction },
@@ -216,7 +216,7 @@ export class EtaService {
}
// at max depth, the transaction is guaranteed to be mined in the next block if it hasn't already
Q += ((max + 1) * (1-tailProb));
const eta = da.adjustedTimeAvg * Q; // T x Q
const eta = da.timeAvg * Q; // T x Q
return {
now,

View File

@@ -143,8 +143,8 @@ export class ServicesApiServices {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
}
accelerateWithGooglePay$(txInput: string, token: string, verificationToken: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
}
getAccelerations$(): Observable<Acceleration[]> {

View File

@@ -186,7 +186,6 @@ export class StateService {
live2Chart$ = new Subject<OptimizedMempoolStats>();
viewAmountMode$: BehaviorSubject<'btc' | 'sats' | 'fiat'>;
timezone$: BehaviorSubject<string>;
connectionState$ = new BehaviorSubject<0 | 1 | 2>(2);
isTabHidden$: Observable<boolean>;
@@ -348,9 +347,6 @@ export class StateService {
const viewAmountModePreference = this.storageService.getValue('view-amount-mode') as 'btc' | 'sats' | 'fiat';
this.viewAmountMode$ = new BehaviorSubject<'btc' | 'sats' | 'fiat'>(viewAmountModePreference || 'btc');
const timezonePreference = this.storageService.getValue('timezone-preference');
this.timezone$ = new BehaviorSubject<string>(timezonePreference || 'local');
this.backend$.subscribe(backend => {
this.backend = backend;
});

View File

@@ -30,7 +30,7 @@
<app-fiat-selector></app-fiat-selector>
</div>
<div class="selector">
<app-timezone-selector></app-timezone-selector>
<app-rate-unit-selector></app-rate-unit-selector>
</div>
<div class="selector d-none" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'">
<app-amount-selector></app-amount-selector>
@@ -76,7 +76,6 @@
<p><a [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.recent-blocks">Recent Blocks</a></p>
<p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
<p><a [routerLink]="['/tx/test' | relativeUrl]" i18n="shared.test-transaction|Test Transaction">Test Transaction</a></p>
<p><a [routerLink]="['/tx/preview' | relativeUrl]" i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</a></p>
<p *ngIf="officialMempoolSpace"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
<p><a [routerLink]="['/docs/api' | relativeUrl]" i18n="footer.api-documentation">API Documentation</a></p>
</div>

View File

@@ -1,6 +1,6 @@
<span *ngIf="seconds === undefined">-</span>
<span *ngIf="seconds !== undefined">
&lrm;{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' : (stateService.timezone$ | async) }}
&lrm;{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }}
<div class="lg-inline" *ngIf="!hideTimeSince">
<i class="symbol">(<app-time kind="since" [time]="seconds" [fastRender]="true" [precision]="precision" [minUnit]="minUnit"></app-time>)</i>
</div>

View File

@@ -1,5 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { StateService } from '@app/services/state.service';
@Component({
selector: 'app-timestamp',
@@ -17,10 +16,6 @@ export class TimestampComponent implements OnChanges {
seconds: number | undefined = undefined;
constructor(
public stateService: StateService,
) { }
ngOnChanges(): void {
if (this.unixTime) {
this.seconds = this.unixTime;

View File

@@ -1,6 +1,6 @@
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null" [style.justify-content]="textAlign" [class.inline]="inline">
<ng-container *ngIf="link">
<a [routerLink]="link" [queryParams]="queryParams" class="truncate-link" [target]="external ? '_blank' : '_self'" [class.disabled]="disabled">
<a [routerLink]="link" [queryParams]="queryParams" class="truncate-link" [target]="external ? '_blank' : '_self'">
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
</a>
</ng-container>

View File

@@ -37,12 +37,6 @@
max-width: 300px;
overflow: hidden;
}
.disabled {
pointer-events: none;
opacity: 0.8;
color: #fff;
}
}
@media (max-width: 567px) {

View File

@@ -15,7 +15,6 @@ export class TruncateComponent {
@Input() maxWidth: number = null;
@Input() inline: boolean = false;
@Input() textAlign: 'start' | 'end' = 'start';
@Input() disabled: boolean = false;
rtl: boolean;
constructor(

View File

@@ -36,7 +36,6 @@ import { FiatSelectorComponent } from '@components/fiat-selector/fiat-selector.c
import { RateUnitSelectorComponent } from '@components/rate-unit-selector/rate-unit-selector.component';
import { ThemeSelectorComponent } from '@components/theme-selector/theme-selector.component';
import { AmountSelectorComponent } from '@components/amount-selector/amount-selector.component';
import { TimezoneSelectorComponent } from '@components/timezone-selector/timezone-selector.component';
import { BrowserOnlyDirective } from '@app/shared/directives/browser-only.directive';
import { ServerOnlyDirective } from '@app/shared/directives/server-only.directive';
import { ColoredPriceDirective } from '@app/shared/directives/colored-price.directive';
@@ -135,7 +134,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
ThemeSelectorComponent,
RateUnitSelectorComponent,
AmountSelectorComponent,
TimezoneSelectorComponent,
ScriptpubkeyTypePipe,
RelativeUrlPipe,
NoSanitizePipe,
@@ -285,7 +283,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
RateUnitSelectorComponent,
ThemeSelectorComponent,
AmountSelectorComponent,
TimezoneSelectorComponent,
ScriptpubkeyTypePipe,
RelativeUrlPipe,
Hex2asciiPipe,

View File

@@ -1,9 +1,8 @@
import { TransactionFlags } from '@app/shared/filters.utils';
import { getVarIntLength, parseMultisigScript, isPoint } from '@app/shared/script.utils';
import { Transaction, Vin } from '@interfaces/electrs.interface';
import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from '@app/shared/script.utils';
import { Transaction } from '@interfaces/electrs.interface';
import { CpfpInfo, RbfInfo, TransactionStripped } from '@interfaces/node-api.interface';
import { StateService } from '@app/services/state.service';
import { Hash } from './sha256';
// Bitcoin Core default policy settings
const MAX_STANDARD_TX_WEIGHT = 400_000;
@@ -589,762 +588,3 @@ export function identifyPrioritizedTransactions(transactions: TransactionStrippe
return { prioritized, deprioritized };
}
// Adapted from mempool backend https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/transaction-utils.ts#L254
// Converts hex bitcoin script to ASM
function convertScriptSigAsm(hex: string): string {
const buf = new Uint8Array(hex.length / 2);
for (let i = 0; i < buf.length; i++) {
buf[i] = parseInt(hex.substr(i * 2, 2), 16);
}
const b = [];
let i = 0;
while (i < buf.length) {
const op = buf[i];
if (op >= 0x01 && op <= 0x4e) {
i++;
let push;
if (op === 0x4c) {
push = buf[i];
b.push('OP_PUSHDATA1');
i += 1;
} else if (op === 0x4d) {
push = buf[i] | (buf[i + 1] << 8);
b.push('OP_PUSHDATA2');
i += 2;
} else if (op === 0x4e) {
push = buf[i] | (buf[i + 1] << 8) | (buf[i + 2] << 16) | (buf[i + 3] << 24);
b.push('OP_PUSHDATA4');
i += 4;
} else {
push = op;
b.push('OP_PUSHBYTES_' + push);
}
const data = buf.slice(i, i + push);
if (data.length !== push) {
break;
}
b.push(uint8ArrayToHexString(data));
i += data.length;
} else {
if (op === 0x00) {
b.push('OP_0');
} else if (op === 0x4f) {
b.push('OP_PUSHNUM_NEG1');
} else if (op === 0xb1) {
b.push('OP_CLTV');
} else if (op === 0xb2) {
b.push('OP_CSV');
} else if (op === 0xba) {
b.push('OP_CHECKSIGADD');
} else {
const opcode = opcodes[op];
if (opcode) {
b.push(opcode);
} else {
b.push('OP_RETURN_' + op);
}
}
i += 1;
}
}
return b.join(' ');
}
// Copied from mempool backend https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/transaction-utils.ts#L327
/**
* This function must only be called when we know the witness we are parsing
* is a taproot witness.
* @param witness An array of hex strings that represents the witness stack of
* the input.
* @returns null if the witness is not a script spend, and the hex string of
* the script item if it is a script spend.
*/
function witnessToP2TRScript(witness: string[]): string | null {
if (witness.length < 2) return null;
// Note: see BIP341 for parsing details of witness stack
// If there are at least two witness elements, and the first byte of the
// last element is 0x50, this last element is called annex a and
// is removed from the witness stack.
const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50';
// If there are at least two witness elements left, script path spending is used.
// Call the second-to-last stack element s, the script.
// (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
if (hasAnnex && witness.length < 3) return null;
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
return witness[positionOfScript];
}
// Copied from mempool backend https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/transaction-utils.ts#L227
// Fills inner_redeemscript_asm and inner_witnessscript_asm fields of fetched prevouts for decoded transactions
export function addInnerScriptsToVin(vin: Vin): void {
if (!vin.prevout) {
return;
}
if (vin.prevout.scriptpubkey_type === 'p2sh') {
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
vin.inner_redeemscript_asm = convertScriptSigAsm(redeemScript);
if (vin.witness && vin.witness.length > 2) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = convertScriptSigAsm(witnessScript);
}
}
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = convertScriptSigAsm(witnessScript);
}
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
const witnessScript = witnessToP2TRScript(vin.witness);
if (witnessScript !== null) {
vin.inner_witnessscript_asm = convertScriptSigAsm(witnessScript);
}
}
}
// Adapted from bitcoinjs-lib at https://github.com/bitcoinjs/bitcoinjs-lib/blob/32e08aa57f6a023e995d8c4f0c9fbdc5f11d1fa0/ts_src/transaction.ts#L78
// Reads buffer of raw transaction data
function fromBuffer(buffer: Uint8Array, network: string): Transaction {
let offset = 0;
function readInt8(): number {
if (offset + 1 > buffer.length) {
throw new Error('Buffer out of bounds');
}
return buffer[offset++];
}
function readInt16() {
if (offset + 2 > buffer.length) {
throw new Error('Buffer out of bounds');
}
const value = buffer[offset] | (buffer[offset + 1] << 8);
offset += 2;
return value;
}
function readInt32(unsigned = false): number {
if (offset + 4 > buffer.length) {
throw new Error('Buffer out of bounds');
}
const value = buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24);
offset += 4;
if (unsigned) {
return value >>> 0;
}
return value;
}
function readInt64(): bigint {
if (offset + 8 > buffer.length) {
throw new Error('Buffer out of bounds');
}
const low = BigInt(buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24));
const high = BigInt(buffer[offset + 4] | (buffer[offset + 5] << 8) | (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24));
offset += 8;
return (high << 32n) | (low & 0xffffffffn);
}
function readVarInt(): bigint {
const first = readInt8();
if (first < 0xfd) {
return BigInt(first);
} else if (first === 0xfd) {
return BigInt(readInt16());
} else if (first === 0xfe) {
return BigInt(readInt32(true));
} else if (first === 0xff) {
return readInt64();
} else {
throw new Error("Invalid VarInt prefix");
}
}
function readSlice(n: number | bigint): Uint8Array {
const length = Number(n);
if (offset + length > buffer.length) {
throw new Error('Cannot read slice out of bounds');
}
const slice = buffer.slice(offset, offset + length);
offset += length;
return slice;
}
function readVarSlice(): Uint8Array {
return readSlice(readVarInt());
}
function readVector(): Uint8Array[] {
const count = readVarInt();
const vector = [];
for (let i = 0; i < count; i++) {
vector.push(readVarSlice());
}
return vector;
}
// Parse raw transaction
const tx = {
status: {
confirmed: null,
block_height: null,
block_hash: null,
block_time: null,
}
} as Transaction;
tx.version = readInt32();
const marker = readInt8();
const flag = readInt8();
let hasWitnesses = false;
if (
marker === 0x00 &&
flag === 0x01
) {
hasWitnesses = true;
} else {
offset -= 2;
}
const vinLen = readVarInt();
tx.vin = [];
for (let i = 0; i < vinLen; ++i) {
const txid = uint8ArrayToHexString(readSlice(32).reverse());
const vout = readInt32(true);
const scriptsig = uint8ArrayToHexString(readVarSlice());
const sequence = readInt32(true);
const is_coinbase = txid === '0'.repeat(64);
const scriptsig_asm = convertScriptSigAsm(scriptsig);
tx.vin.push({ txid, vout, scriptsig, sequence, is_coinbase, scriptsig_asm, prevout: null });
}
const voutLen = readVarInt();
tx.vout = [];
for (let i = 0; i < voutLen; ++i) {
const value = Number(readInt64());
const scriptpubkeyArray = readVarSlice();
const scriptpubkey = uint8ArrayToHexString(scriptpubkeyArray)
const scriptpubkey_asm = convertScriptSigAsm(scriptpubkey);
const toAddress = scriptPubKeyToAddress(scriptpubkey, network);
const scriptpubkey_type = toAddress.type;
const scriptpubkey_address = toAddress?.address;
tx.vout.push({ value, scriptpubkey, scriptpubkey_asm, scriptpubkey_type, scriptpubkey_address });
}
let witnessSize = 0;
if (hasWitnesses) {
const startOffset = offset;
for (let i = 0; i < vinLen; ++i) {
tx.vin[i].witness = readVector().map(uint8ArrayToHexString);
}
witnessSize = offset - startOffset + 2;
}
tx.locktime = readInt32(true);
if (offset !== buffer.length) {
throw new Error('Transaction has unexpected data');
}
tx.size = buffer.length;
tx.weight = (tx.size - witnessSize) * 3 + tx.size;
tx.txid = txid(tx);
return tx;
}
export function decodeRawTransaction(rawtx: string, network: string): Transaction {
if (!rawtx.length || rawtx.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(rawtx)) {
throw new Error('Invalid hex string');
}
const buffer = new Uint8Array(rawtx.length / 2);
for (let i = 0; i < rawtx.length; i += 2) {
buffer[i / 2] = parseInt(rawtx.substring(i, i + 2), 16);
}
return fromBuffer(buffer, network);
}
function serializeTransaction(tx: Transaction): Uint8Array {
const result: number[] = [];
// Add version
result.push(...intToBytes(tx.version, 4));
// Add input count and inputs
result.push(...varIntToBytes(tx.vin.length));
for (const input of tx.vin) {
result.push(...hexStringToUint8Array(input.txid).reverse());
result.push(...intToBytes(input.vout, 4));
const scriptSig = hexStringToUint8Array(input.scriptsig);
result.push(...varIntToBytes(scriptSig.length));
result.push(...scriptSig);
result.push(...intToBytes(input.sequence, 4));
}
// Add output count and outputs
result.push(...varIntToBytes(tx.vout.length));
for (const output of tx.vout) {
result.push(...bigIntToBytes(BigInt(output.value), 8));
const scriptPubKey = hexStringToUint8Array(output.scriptpubkey);
result.push(...varIntToBytes(scriptPubKey.length));
result.push(...scriptPubKey);
}
// Add locktime
result.push(...intToBytes(tx.locktime, 4));
return new Uint8Array(result);
}
function txid(tx: Transaction): string {
const serializedTx = serializeTransaction(tx);
const hash1 = new Hash().update(serializedTx).digest();
const hash2 = new Hash().update(hash1).digest();
return uint8ArrayToHexString(hash2.reverse());
}
// Copied from mempool backend https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/transaction-utils.ts#L177
export function countSigops(transaction: Transaction): number {
let sigops = 0;
for (const input of transaction.vin) {
if (input.scriptsig_asm) {
sigops += countScriptSigops(input.scriptsig_asm, true);
}
if (input.prevout) {
switch (true) {
case input.prevout.scriptpubkey_type === 'p2sh' && input.witness?.length === 2 && input.scriptsig && input.scriptsig.startsWith('160014'):
case input.prevout.scriptpubkey_type === 'v0_p2wpkh':
sigops += 1;
break;
case input.prevout?.scriptpubkey_type === 'p2sh' && input.witness?.length && input.scriptsig && input.scriptsig.startsWith('220020'):
case input.prevout.scriptpubkey_type === 'v0_p2wsh':
if (input.witness?.length) {
sigops += countScriptSigops(convertScriptSigAsm(input.witness[input.witness.length - 1]), false, true);
}
break;
case input.prevout.scriptpubkey_type === 'p2sh':
if (input.inner_redeemscript_asm) {
sigops += countScriptSigops(input.inner_redeemscript_asm);
}
break;
}
}
}
for (const output of transaction.vout) {
if (output.scriptpubkey_asm) {
sigops += countScriptSigops(output.scriptpubkey_asm, true);
}
}
return sigops;
}
function scriptPubKeyToAddress(scriptPubKey: string, network: string): { address: string, type: string } {
// P2PKH
if (/^76a914[0-9a-f]{40}88ac$/.test(scriptPubKey)) {
return { address: p2pkh(scriptPubKey.substring(6, 6 + 40), network), type: 'p2pkh' };
}
// P2PK
if (/^21[0-9a-f]{66}ac$/.test(scriptPubKey) || /^41[0-9a-f]{130}ac$/.test(scriptPubKey)) {
return { address: null, type: 'p2pk' };
}
// P2SH
if (/^a914[0-9a-f]{40}87$/.test(scriptPubKey)) {
return { address: p2sh(scriptPubKey.substring(4, 4 + 40), network), type: 'p2sh' };
}
// P2WPKH
if (/^0014[0-9a-f]{40}$/.test(scriptPubKey)) {
return { address: p2wpkh(scriptPubKey.substring(4, 4 + 40), network), type: 'v0_p2wpkh' };
}
// P2WSH
if (/^0020[0-9a-f]{64}$/.test(scriptPubKey)) {
return { address: p2wsh(scriptPubKey.substring(4, 4 + 64), network), type: 'v0_p2wsh' };
}
// P2TR
if (/^5120[0-9a-f]{64}$/.test(scriptPubKey)) {
return { address: p2tr(scriptPubKey.substring(4, 4 + 64), network), type: 'v1_p2tr' };
}
// multisig
if (/^[0-9a-f]+ae$/.test(scriptPubKey)) {
return { address: null, type: 'multisig' };
}
// anchor
if (scriptPubKey === '51024e73') {
return { address: p2a(network), type: 'anchor' };
}
// op_return
if (/^6a/.test(scriptPubKey)) {
return { address: null, type: 'op_return' };
}
return { address: null, type: 'unknown' };
}
function p2pkh(pubKeyHash: string, network: string): string {
const pubkeyHashArray = hexStringToUint8Array(pubKeyHash);
const version = ['testnet', 'testnet4', 'signet'].includes(network) ? 0x6f : 0x00;
const versionedPayload = Uint8Array.from([version, ...pubkeyHashArray]);
const hash1 = new Hash().update(versionedPayload).digest();
const hash2 = new Hash().update(hash1).digest();
const checksum = hash2.slice(0, 4);
const finalPayload = Uint8Array.from([...versionedPayload, ...checksum]);
const bitcoinAddress = base58Encode(finalPayload);
return bitcoinAddress;
}
function p2sh(scriptHash: string, network: string): string {
const scriptHashArray = hexStringToUint8Array(scriptHash);
const version = ['testnet', 'testnet4', 'signet'].includes(network) ? 0xc4 : 0x05;
const versionedPayload = Uint8Array.from([version, ...scriptHashArray]);
const hash1 = new Hash().update(versionedPayload).digest();
const hash2 = new Hash().update(hash1).digest();
const checksum = hash2.slice(0, 4);
const finalPayload = Uint8Array.from([...versionedPayload, ...checksum]);
const bitcoinAddress = base58Encode(finalPayload);
return bitcoinAddress;
}
function p2wpkh(pubKeyHash: string, network: string): string {
const pubkeyHashArray = hexStringToUint8Array(pubKeyHash);
const hrp = ['testnet', 'testnet4', 'signet'].includes(network) ? 'tb' : 'bc';
const version = 0;
const words = [version].concat(toWords(pubkeyHashArray));
const bech32Address = bech32Encode(hrp, words);
return bech32Address;
}
function p2wsh(scriptHash: string, network: string): string {
const scriptHashArray = hexStringToUint8Array(scriptHash);
const hrp = ['testnet', 'testnet4', 'signet'].includes(network) ? 'tb' : 'bc';
const version = 0;
const words = [version].concat(toWords(scriptHashArray));
const bech32Address = bech32Encode(hrp, words);
return bech32Address;
}
function p2tr(pubKeyHash: string, network: string): string {
const pubkeyHashArray = hexStringToUint8Array(pubKeyHash);
const hrp = ['testnet', 'testnet4', 'signet'].includes(network) ? 'tb' : 'bc';
const version = 1;
const words = [version].concat(toWords(pubkeyHashArray));
const bech32Address = bech32Encode(hrp, words, 0x2bc830a3);
return bech32Address;
}
function p2a(network: string): string {
const pubkeyHashArray = hexStringToUint8Array('4e73');
const hrp = ['testnet', 'testnet4', 'signet'].includes(network) ? 'tb' : 'bc';
const version = 1;
const words = [version].concat(toWords(pubkeyHashArray));
const bech32Address = bech32Encode(hrp, words, 0x2bc830a3);
return bech32Address;
}
// base58 encoding
function base58Encode(data: Uint8Array): string {
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
let hexString = Array.from(data)
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');
let num = BigInt("0x" + hexString);
let encoded = "";
while (num > 0) {
const remainder = Number(num % 58n);
num = num / 58n;
encoded = BASE58_ALPHABET[remainder] + encoded;
}
for (let byte of data) {
if (byte === 0) {
encoded = "1" + encoded;
} else {
break;
}
}
return encoded;
}
// bech32 encoding
// Adapted from https://github.com/bitcoinjs/bech32/blob/5ceb0e3d4625561a459c85643ca6947739b2d83c/src/index.ts
function bech32Encode(prefix: string, words: number[], constant: number = 1) {
const BECH32_ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
const checksum = createChecksum(prefix, words, constant);
const combined = words.concat(checksum);
let result = prefix + '1';
for (let i = 0; i < combined.length; ++i) {
result += BECH32_ALPHABET.charAt(combined[i]);
}
return result;
}
function polymodStep(pre) {
const GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
const b = pre >> 25;
return (
((pre & 0x1ffffff) << 5) ^
((b & 1 ? GENERATORS[0] : 0) ^
(b & 2 ? GENERATORS[1] : 0) ^
(b & 4 ? GENERATORS[2] : 0) ^
(b & 8 ? GENERATORS[3] : 0) ^
(b & 16 ? GENERATORS[4] : 0))
);
}
function prefixChk(prefix) {
let chk = 1;
for (let i = 0; i < prefix.length; ++i) {
const c = prefix.charCodeAt(i);
chk = polymodStep(chk) ^ (c >> 5);
}
chk = polymodStep(chk);
for (let i = 0; i < prefix.length; ++i) {
const c = prefix.charCodeAt(i);
chk = polymodStep(chk) ^ (c & 0x1f);
}
return chk;
}
function createChecksum(prefix: string, words: number[], constant: number) {
const POLYMOD_CONST = constant;
let chk = prefixChk(prefix);
for (let i = 0; i < words.length; ++i) {
const x = words[i];
chk = polymodStep(chk) ^ x;
}
for (let i = 0; i < 6; ++i) {
chk = polymodStep(chk);
}
chk ^= POLYMOD_CONST;
const checksum = [];
for (let i = 0; i < 6; ++i) {
checksum.push((chk >> (5 * (5 - i))) & 31);
}
return checksum;
}
function convertBits(data, fromBits, toBits, pad) {
let acc = 0;
let bits = 0;
const ret = [];
const maxV = (1 << toBits) - 1;
for (let i = 0; i < data.length; ++i) {
const value = data[i];
if (value < 0 || value >> fromBits) throw new Error('Invalid value');
acc = (acc << fromBits) | value;
bits += fromBits;
while (bits >= toBits) {
bits -= toBits;
ret.push((acc >> bits) & maxV);
}
}
if (pad) {
if (bits > 0) {
ret.push((acc << (toBits - bits)) & maxV);
}
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxV)) {
throw new Error('Invalid data');
}
return ret;
}
function toWords(bytes) {
return convertBits(bytes, 8, 5, true);
}
// Helper functions
function uint8ArrayToHexString(uint8Array: Uint8Array): string {
return Array.from(uint8Array).map(byte => byte.toString(16).padStart(2, '0')).join('');
}
function hexStringToUint8Array(hex: string): Uint8Array {
const buf = new Uint8Array(hex.length / 2);
for (let i = 0; i < buf.length; i++) {
buf[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return buf;
}
function intToBytes(value: number, byteLength: number): number[] {
const bytes = [];
for (let i = 0; i < byteLength; i++) {
bytes.push((value >> (8 * i)) & 0xff);
}
return bytes;
}
function bigIntToBytes(value: bigint, byteLength: number): number[] {
const bytes = [];
for (let i = 0; i < byteLength; i++) {
bytes.push(Number((value >> BigInt(8 * i)) & 0xffn));
}
return bytes;
}
function varIntToBytes(value: number | bigint): number[] {
const bytes = [];
if (typeof value === 'number') {
if (value < 0xfd) {
bytes.push(value);
} else if (value <= 0xffff) {
bytes.push(0xfd, value & 0xff, (value >> 8) & 0xff);
} else if (value <= 0xffffffff) {
bytes.push(0xfe, ...intToBytes(value, 4));
}
} else {
if (value < 0xfdn) {
bytes.push(Number(value));
} else if (value <= 0xffffn) {
bytes.push(0xfd, Number(value & 0xffn), Number((value >> 8n) & 0xffn));
} else if (value <= 0xffffffffn) {
bytes.push(0xfe, ...intToBytes(Number(value), 4));
} else {
bytes.push(0xff, ...bigIntToBytes(value, 8));
}
}
return bytes;
}
// Inversed the opcodes object from https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/utils/bitcoin-script.ts#L1
const opcodes = {
0: 'OP_0',
76: 'OP_PUSHDATA1',
77: 'OP_PUSHDATA2',
78: 'OP_PUSHDATA4',
79: 'OP_PUSHNUM_NEG1',
80: 'OP_RESERVED',
81: 'OP_PUSHNUM_1',
82: 'OP_PUSHNUM_2',
83: 'OP_PUSHNUM_3',
84: 'OP_PUSHNUM_4',
85: 'OP_PUSHNUM_5',
86: 'OP_PUSHNUM_6',
87: 'OP_PUSHNUM_7',
88: 'OP_PUSHNUM_8',
89: 'OP_PUSHNUM_9',
90: 'OP_PUSHNUM_10',
91: 'OP_PUSHNUM_11',
92: 'OP_PUSHNUM_12',
93: 'OP_PUSHNUM_13',
94: 'OP_PUSHNUM_14',
95: 'OP_PUSHNUM_15',
96: 'OP_PUSHNUM_16',
97: 'OP_NOP',
98: 'OP_VER',
99: 'OP_IF',
100: 'OP_NOTIF',
101: 'OP_VERIF',
102: 'OP_VERNOTIF',
103: 'OP_ELSE',
104: 'OP_ENDIF',
105: 'OP_VERIFY',
106: 'OP_RETURN',
107: 'OP_TOALTSTACK',
108: 'OP_FROMALTSTACK',
109: 'OP_2DROP',
110: 'OP_2DUP',
111: 'OP_3DUP',
112: 'OP_2OVER',
113: 'OP_2ROT',
114: 'OP_2SWAP',
115: 'OP_IFDUP',
116: 'OP_DEPTH',
117: 'OP_DROP',
118: 'OP_DUP',
119: 'OP_NIP',
120: 'OP_OVER',
121: 'OP_PICK',
122: 'OP_ROLL',
123: 'OP_ROT',
124: 'OP_SWAP',
125: 'OP_TUCK',
126: 'OP_CAT',
127: 'OP_SUBSTR',
128: 'OP_LEFT',
129: 'OP_RIGHT',
130: 'OP_SIZE',
131: 'OP_INVERT',
132: 'OP_AND',
133: 'OP_OR',
134: 'OP_XOR',
135: 'OP_EQUAL',
136: 'OP_EQUALVERIFY',
137: 'OP_RESERVED1',
138: 'OP_RESERVED2',
139: 'OP_1ADD',
140: 'OP_1SUB',
141: 'OP_2MUL',
142: 'OP_2DIV',
143: 'OP_NEGATE',
144: 'OP_ABS',
145: 'OP_NOT',
146: 'OP_0NOTEQUAL',
147: 'OP_ADD',
148: 'OP_SUB',
149: 'OP_MUL',
150: 'OP_DIV',
151: 'OP_MOD',
152: 'OP_LSHIFT',
153: 'OP_RSHIFT',
154: 'OP_BOOLAND',
155: 'OP_BOOLOR',
156: 'OP_NUMEQUAL',
157: 'OP_NUMEQUALVERIFY',
158: 'OP_NUMNOTEQUAL',
159: 'OP_LESSTHAN',
160: 'OP_GREATERTHAN',
161: 'OP_LESSTHANOREQUAL',
162: 'OP_GREATERTHANOREQUAL',
163: 'OP_MIN',
164: 'OP_MAX',
165: 'OP_WITHIN',
166: 'OP_RIPEMD160',
167: 'OP_SHA1',
168: 'OP_SHA256',
169: 'OP_HASH160',
170: 'OP_HASH256',
171: 'OP_CODESEPARATOR',
172: 'OP_CHECKSIG',
173: 'OP_CHECKSIGVERIFY',
174: 'OP_CHECKMULTISIG',
175: 'OP_CHECKMULTISIGVERIFY',
176: 'OP_NOP1',
177: 'OP_CHECKLOCKTIMEVERIFY',
178: 'OP_CHECKSEQUENCEVERIFY',
179: 'OP_NOP4',
180: 'OP_NOP5',
181: 'OP_NOP6',
182: 'OP_NOP7',
183: 'OP_NOP8',
184: 'OP_NOP9',
185: 'OP_NOP10',
186: 'OP_CHECKSIGADD',
253: 'OP_PUBKEYHASH',
254: 'OP_PUBKEY',
255: 'OP_INVALIDOPCODE',
};

View File

@@ -0,0 +1,45 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>River | Invest in Bitcoin with confidence</title>
<script src="/resources/config.js"></script>
<script src="/resources/customize.js"></script>
<base href="/">
<meta name="description" content="Easily buy Bitcoin in minutes. Zero fees on recurring buys. Invest in Bitcoin with confidence with world-class security." />
<meta property="og:image" content="https://mempool.space/resources/river/river-preview.jpg" />
<meta property="og:image:type" content="image/jpeg" />
<meta property="og:image:width" content="2000" />
<meta property="og:image:height" content="1000" />
<meta property="og:description" content="Easily buy Bitcoin in minutes. Zero fees on recurring buys. Invest in Bitcoin with confidence with world-class security." />
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@mempool">
<meta name="twitter:creator" content="@mempool">
<meta name="twitter:title" content="River | Invest in Bitcoin with confidence">
<meta name="twitter:description" content="Easily buy Bitcoin in minutes. Zero fees on recurring buys. Invest in Bitcoin with confidence with world-class security." />
<meta name="twitter:image" content="https://mempool.space/resources/river/river-preview.jpg" />
<meta name="twitter:domain" content="river.mempool.space">
<link rel="apple-touch-icon" sizes="180x180" href="/resources/river/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/resources/river/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/resources/river/favicons/favicon-16x16.png">
<link rel="manifest" href="/resources/river/favicons/site.webmanifest">
<link rel="shortcut icon" href="/resources/river/favicons/favicon.ico">
<link id="canonical" rel="canonical" href="https://river.mempool.space">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-config" content="/resources/favicons/browserconfig.xml">
<meta name="theme-color" content="#1d1f31">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<app-root></app-root>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -0,0 +1,4 @@
<svg width="160" height="40" viewBox="0 0 160 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M75.8865 31.2087H70.2185L66.2029 23.3666H59.531V31.2087H54.5116V8.52042H66.8515C69.4627 8.52042 71.4874 9.22873 72.9256 10.6425C74.3609 12.0591 75.08 13.8144 75.08 15.9082C75.08 17.3135 74.7388 18.5975 74.0592 19.7658C73.3796 20.9341 72.4011 21.8286 71.1293 22.4551L75.8893 31.2059L75.8865 31.2087ZM59.531 18.3407H67.1109C68.0612 18.3407 68.7888 18.1291 69.2964 17.7086C69.8039 17.2881 70.0577 16.7096 70.0577 15.9759C70.0577 15.2422 69.8039 14.6525 69.2964 14.2094C68.7888 13.7664 68.0612 13.5463 67.1109 13.5463H59.531V18.3435V18.3407ZM84.2954 31.2087H79.276V8.52042H84.2954V31.2087ZM99.709 31.0083L106.192 8.52042H111.699L105.135 31.2087H94.0862L87.7949 8.52042H93.3022L99.7119 31.0083H99.709ZM134.718 26.1857V31.2087H114.638V8.52042H134.557V13.5434H119.66V17.3671H134.397V22.3591H119.66V26.1828H134.721L134.718 26.1857ZM159.914 31.2087H154.246L150.23 23.3666H143.559V31.2087H138.539V8.52042H150.879C153.49 8.52042 155.515 9.22873 156.953 10.6425C158.388 12.0591 159.108 13.8144 159.108 15.9082C159.108 17.3135 158.766 18.5975 158.087 19.7658C157.407 20.9341 156.429 21.8286 155.157 22.4551L159.917 31.2059L159.914 31.2087ZM143.559 18.3407H151.138C152.089 18.3407 152.816 18.1291 153.324 17.7086C153.831 17.2881 154.085 16.7096 154.085 15.9759C154.085 15.2422 153.831 14.6525 153.324 14.2094C152.816 13.7664 152.089 13.5463 151.138 13.5463H143.559V18.3435V18.3407Z" fill="#F9F9F9"/>
<path d="M6.50834 35.4049L19.1838 7.60612C19.2346 7.49324 19.4038 7.56097 19.3643 7.67949L10.2673 34.0024C11.6265 33.5734 13.0195 33.2207 14.4407 32.9442L20.481 8.00683C20.5092 7.88549 20.6896 7.92218 20.6727 8.04352L17.1591 32.5124C18.3181 32.3713 19.4968 32.281 20.684 32.2415L21.8289 8.15357C21.8345 8.02941 22.0178 8.02941 22.0235 8.15357L23.1683 32.2415C24.3583 32.2782 25.5342 32.3685 26.6932 32.5124L23.1796 8.04352C23.1627 7.92218 23.3404 7.88549 23.3714 8.00683L29.4116 32.9442C30.8328 33.2207 32.2259 33.5763 33.5851 34.0024L24.4881 7.67949C24.4486 7.56379 24.6178 7.49324 24.6685 7.60612L37.344 35.4049C39.6338 36.3982 41.8107 37.6031 43.8495 39L21.9248 1L0 39C2.03597 37.6031 4.21294 36.3982 6.50552 35.4049" fill="#C5A063"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -131,7 +131,7 @@ export NVM_DIR="${HOME}/.nvm"
source "${NVM_DIR}/nvm.sh"
# what to look for
frontends=(mainnet liquid onbtc bitb meta)
frontends=(mainnet liquid onbtc bitb meta river)
backends=(mainnet testnet testnet4 signet liquid liquidtestnet onbtc bitb)
frontend_repos=()
backend_repos=()

View File

@@ -153,6 +153,6 @@
},
"WALLETS": {
"ENABLED": true,
"WALLETS": ["BITB", "3350"]
"WALLETS": ["BITB", "3350", "RIVER"]
}
}

View File

@@ -0,0 +1,19 @@
{
"OFFICIAL_MEMPOOL_SPACE": true,
"TESTNET_ENABLED": true,
"TESTNET4_ENABLED": true,
"LIQUID_ENABLED": true,
"LIQUID_TESTNET_ENABLED": true,
"BISQ_ENABLED": true,
"BISQ_SEPARATE_BACKEND": true,
"SIGNET_ENABLED": true,
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets",
"ITEMS_PER_PAGE": 25,
"LIGHTNING": true,
"ACCELERATOR": true,
"PUBLIC_ACCELERATIONS": true,
"AUDIT": true,
"CUSTOMIZATION": "custom-river-config.json"
}

View File

@@ -16,7 +16,7 @@ screen -dmS x startx
sleep 3
# start unfurlers for each frontend
for site in mainnet liquid onbtc bitb meta;do
for site in mainnet liquid onbtc bitb meta river;do
cd "$HOME/${site}/unfurler" && \
echo "starting mempool unfurler: ${site}" && \
screen -dmS "unfurler-${site}" sh -c 'while true;do npm run unfurler;sleep 2;done'

View File

@@ -0,0 +1,17 @@
{
"SERVER": {
"HOST": "https://river.tk7.mempool.space",
"HTTP_PORT": 8007
},
"MEMPOOL": {
"HTTP_HOST": "http://127.0.0.1",
"HTTP_PORT": 7,
"NETWORK": "river"
},
"PUPPETEER": {
"CLUSTER_SIZE": 8,
"EXEC_PATH": "/usr/local/bin/chrome",
"MAX_PAGE_AGE": 86400,
"RENDER_TIMEOUT": 3000
}
}

View File

@@ -333,6 +333,28 @@ export const networks = {
routes: routes.lightning.routes,
}
}
},
river: {
networkName: 'River',
title: 'River | Invest in Bitcoin with confidence',
description: 'Easily buy Bitcoin in minutes. Zero fees on recurring buys. Invest in Bitcoin with confidence with world-class security.',
fallbackImg: '/resources/river/river-preview.jpg',
routes: { // only dynamic routes supported
block: routes.block,
address: routes.address,
wallet: routes.wallet,
tx: routes.tx,
mining: {
title: "Mining",
routes: {
pool: routes.mining.routes.pool,
}
},
lightning: {
title: "Lightning",
routes: routes.lightning.routes,
}
}
}
};