Compare commits
2 Commits
orangesurf
...
natsoni/mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1927ab1bd8 | ||
|
|
f756fb4d79 |
734
backend/package-lock.json
generated
734
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,7 @@
|
||||
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.7.2",
|
||||
@@ -47,16 +47,16 @@
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.11.0",
|
||||
"mysql2": "~3.10.0",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"redis": "^4.7.0",
|
||||
"redis": "^4.6.6",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/express": "^4.17.17",
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import logger from '../../logger';
|
||||
import { MempoolTransactionExtended } from '../../mempool.interfaces';
|
||||
import { GraphTx, getSameBlockRelatives, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from '../mini-miner';
|
||||
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||
|
||||
const BLOCK_WEIGHT_UNITS = 4_000_000;
|
||||
const BLOCK_SIGOPS = 80_000;
|
||||
const MAX_RELATIVE_GRAPH_SIZE = 200;
|
||||
const BID_BOOST_WINDOW = 40_000;
|
||||
const BID_BOOST_MIN_OFFSET = 10_000;
|
||||
const BID_BOOST_MAX_OFFSET = 400_000;
|
||||
|
||||
export type Acceleration = {
|
||||
type Acceleration = {
|
||||
txid: string;
|
||||
max_bid: number;
|
||||
};
|
||||
@@ -27,6 +28,31 @@ export interface AccelerationInfo {
|
||||
cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
|
||||
}
|
||||
|
||||
interface GraphTx {
|
||||
txid: string;
|
||||
vsize: number;
|
||||
weight: number;
|
||||
fees: {
|
||||
base: number; // in sats
|
||||
};
|
||||
depends: string[];
|
||||
spentby: string[];
|
||||
}
|
||||
|
||||
interface MempoolTx extends GraphTx {
|
||||
ancestorcount: number;
|
||||
ancestorsize: number;
|
||||
fees: { // in sats
|
||||
base: number;
|
||||
ancestor: number;
|
||||
};
|
||||
|
||||
ancestors: Map<string, MempoolTx>,
|
||||
ancestorRate: number;
|
||||
individualRate: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
class AccelerationCosts {
|
||||
/**
|
||||
* Takes a list of accelerations and verbose block data
|
||||
@@ -35,7 +61,7 @@ class AccelerationCosts {
|
||||
* @param accelerationsx
|
||||
* @param verboseBlock
|
||||
*/
|
||||
public calculateBoostRate(accelerations: Acceleration[], blockTxs: MempoolTransactionExtended[]): number {
|
||||
public calculateBoostRate(accelerations: Acceleration[], blockTxs: IEsploraApi.Transaction[]): number {
|
||||
// Run GBT ourselves to calculate accurate effective fee rates
|
||||
// the list of transactions comes from a mined block, so we already know everything fits within consensus limits
|
||||
const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity);
|
||||
@@ -144,28 +170,108 @@ class AccelerationCosts {
|
||||
/**
|
||||
* Takes an accelerated mined txid and a target rate
|
||||
* Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors
|
||||
*
|
||||
* @param txid
|
||||
* @param medianFeeRate
|
||||
*
|
||||
* @param txid
|
||||
* @param medianFeeRate
|
||||
*/
|
||||
public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo {
|
||||
// Get same-block transaction ancestors
|
||||
const allRelatives = getSameBlockRelatives(tx, transactions);
|
||||
const relativesMap = initializeRelatives(allRelatives);
|
||||
const rootTx = relativesMap.get(tx.txid) as GraphTx;
|
||||
const allRelatives = this.getSameBlockRelatives(tx, transactions);
|
||||
const relativesMap = this.initializeRelatives(allRelatives);
|
||||
const rootTx = relativesMap.get(tx.txid) as MempoolTx;
|
||||
|
||||
// Calculate cost to boost
|
||||
return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a raw transaction, and builds a graph of same-block relatives,
|
||||
* and returns as a MempoolTx
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
private getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> {
|
||||
const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
|
||||
const spendMap = new Map<string, string>(); // map of outpoints to spending txids
|
||||
for (const tx of transactions) {
|
||||
blockTxs.set(tx.txid, tx);
|
||||
for (const vin of tx.vin) {
|
||||
spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
|
||||
}
|
||||
}
|
||||
|
||||
const relatives: Map<string, GraphTx> = new Map();
|
||||
const stack: string[] = [tx.txid];
|
||||
|
||||
// build set of same-block ancestors
|
||||
while (stack.length > 0) {
|
||||
const nextTxid = stack.pop();
|
||||
const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
|
||||
if (!nextTx || relatives.has(nextTx.txid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mempoolTx = this.convertToGraphTx(nextTx);
|
||||
|
||||
mempoolTx.fees.base = nextTx.fee || 0;
|
||||
mempoolTx.depends = nextTx.vin.map(vin => vin.txid).filter(inTxid => inTxid && blockTxs.has(inTxid)) as string[];
|
||||
mempoolTx.spentby = nextTx.vout.map((vout, index) => spendMap.get(`${nextTx.txid}:${index}`)).filter(outTxid => outTxid && blockTxs.has(outTxid)) as string[];
|
||||
|
||||
for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
|
||||
if (txid) {
|
||||
stack.push(txid);
|
||||
}
|
||||
}
|
||||
|
||||
relatives.set(mempoolTx.txid, mempoolTx);
|
||||
}
|
||||
|
||||
return relatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a raw transaction and converts it to MempoolTx format
|
||||
* fee and ancestor data is initialized with dummy/null values
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
private convertToGraphTx(tx: MempoolTransactionExtended): GraphTx {
|
||||
return {
|
||||
txid: tx.txid,
|
||||
vsize: Math.ceil(tx.weight / 4),
|
||||
weight: tx.weight,
|
||||
fees: {
|
||||
base: 0, // dummy
|
||||
},
|
||||
depends: [], // dummy
|
||||
spentby: [], //dummy
|
||||
};
|
||||
}
|
||||
|
||||
private convertGraphToMempoolTx(tx: GraphTx): MempoolTx {
|
||||
return {
|
||||
...tx,
|
||||
fees: {
|
||||
base: tx.fees.base,
|
||||
ancestor: tx.fees.base,
|
||||
},
|
||||
ancestorcount: 1,
|
||||
ancestorsize: Math.ceil(tx.weight / 4),
|
||||
ancestors: new Map<string, MempoolTx>(),
|
||||
ancestorRate: 0,
|
||||
individualRate: 0,
|
||||
score: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a root transaction, a list of in-mempool ancestors, and a target fee rate,
|
||||
* Calculate the minimum set of transactions to fee-bump, their total vsize + fees
|
||||
*
|
||||
*
|
||||
* @param tx
|
||||
* @param ancestors
|
||||
*/
|
||||
private calculateAccelerationAncestors(tx: GraphTx, relatives: Map<string, GraphTx>, targetFeeRate: number): AccelerationInfo {
|
||||
private calculateAccelerationAncestors(tx: MempoolTx, relatives: Map<string, MempoolTx>, targetFeeRate: number): AccelerationInfo {
|
||||
// add root tx to the ancestor map
|
||||
relatives.set(tx.txid, tx);
|
||||
|
||||
@@ -177,12 +283,12 @@ class AccelerationCosts {
|
||||
});
|
||||
|
||||
// Initialize individual & ancestor fee rates
|
||||
relatives.forEach(entry => setAncestorScores(entry));
|
||||
relatives.forEach(entry => this.setAncestorScores(entry));
|
||||
|
||||
// Sort by descending ancestor score
|
||||
let sortedRelatives = Array.from(relatives.values()).sort(mempoolComparator);
|
||||
let sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
|
||||
|
||||
let includedInCluster: Map<string, GraphTx> | null = null;
|
||||
let includedInCluster: Map<string, MempoolTx> | null = null;
|
||||
|
||||
// While highest score >= targetFeeRate
|
||||
let maxIterations = MAX_RELATIVE_GRAPH_SIZE;
|
||||
@@ -191,17 +297,17 @@ class AccelerationCosts {
|
||||
// Grab the highest scoring entry
|
||||
const best = sortedRelatives.shift();
|
||||
if (best) {
|
||||
const cluster = new Map<string, GraphTx>(best.ancestors?.entries() || []);
|
||||
const cluster = new Map<string, MempoolTx>(best.ancestors?.entries() || []);
|
||||
if (best.ancestors.has(tx.txid)) {
|
||||
includedInCluster = cluster;
|
||||
}
|
||||
cluster.set(best.txid, best);
|
||||
// Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
|
||||
// and update scores, ancestor totals and dependencies for the survivors
|
||||
removeAncestors(cluster, relatives);
|
||||
this.removeAncestors(cluster, relatives);
|
||||
|
||||
// re-sort
|
||||
sortedRelatives = Array.from(relatives.values()).sort(mempoolComparator);
|
||||
sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +345,394 @@ class AccelerationCosts {
|
||||
nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
|
||||
* for each transaction.
|
||||
*
|
||||
* @param tx
|
||||
* @param all
|
||||
*/
|
||||
private setAncestors(tx: MempoolTx, all: Map<string, MempoolTx>, visited: Map<string, Map<string, MempoolTx>>, depth: number = 0): Map<string, MempoolTx> {
|
||||
// sanity check for infinite recursion / too many ancestors (should never happen)
|
||||
if (depth >= 100) {
|
||||
logger.warn('acceleration dependency calculation failed: setAncestors reached depth of 100, unable to proceed', `Accelerator`);
|
||||
throw new Error('invalid_tx_dependencies');
|
||||
}
|
||||
|
||||
// initialize the ancestor map for this tx
|
||||
tx.ancestors = new Map<string, MempoolTx>();
|
||||
tx.depends.forEach(parentId => {
|
||||
const parent = all.get(parentId);
|
||||
if (parent) {
|
||||
// add the parent
|
||||
tx.ancestors?.set(parentId, parent);
|
||||
// check for a cached copy of this parent's ancestors
|
||||
let ancestors = visited.get(parent.txid);
|
||||
if (!ancestors) {
|
||||
// recursively fetch the parent's ancestors
|
||||
ancestors = this.setAncestors(parent, all, visited, depth + 1);
|
||||
}
|
||||
// and add to this tx's map
|
||||
ancestors.forEach((ancestor, ancestorId) => {
|
||||
tx.ancestors?.set(ancestorId, ancestor);
|
||||
});
|
||||
}
|
||||
});
|
||||
visited.set(tx.txid, tx.ancestors);
|
||||
|
||||
return tx.ancestors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
|
||||
* by running setAncestors on each leaf, and caching intermediate results.
|
||||
* then initializes ancestor data for each transaction
|
||||
*
|
||||
* @param all
|
||||
*/
|
||||
private initializeRelatives(all: Map<string, GraphTx>): Map<string, MempoolTx> {
|
||||
const mempoolTxs = new Map<string, MempoolTx>();
|
||||
all.forEach(entry => {
|
||||
mempoolTxs.set(entry.txid, this.convertGraphToMempoolTx(entry));
|
||||
});
|
||||
const visited: Map<string, Map<string, MempoolTx>> = new Map();
|
||||
const leaves: MempoolTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
|
||||
for (const leaf of leaves) {
|
||||
this.setAncestors(leaf, mempoolTxs, visited);
|
||||
}
|
||||
mempoolTxs.forEach(entry => {
|
||||
entry.ancestors?.forEach(ancestor => {
|
||||
entry.ancestorcount++;
|
||||
entry.ancestorsize += ancestor.vsize;
|
||||
entry.fees.ancestor += ancestor.fees.base;
|
||||
});
|
||||
this.setAncestorScores(entry);
|
||||
});
|
||||
return mempoolTxs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a cluster of transactions from an in-mempool dependency graph
|
||||
* and update the survivors' scores and ancestors
|
||||
*
|
||||
* @param cluster
|
||||
* @param ancestors
|
||||
*/
|
||||
private removeAncestors(cluster: Map<string, MempoolTx>, all: Map<string, MempoolTx>): void {
|
||||
// remove
|
||||
cluster.forEach(tx => {
|
||||
all.delete(tx.txid);
|
||||
});
|
||||
|
||||
// update survivors
|
||||
all.forEach(tx => {
|
||||
cluster.forEach(remove => {
|
||||
if (tx.ancestors?.has(remove.txid)) {
|
||||
// remove as dependency
|
||||
tx.ancestors.delete(remove.txid);
|
||||
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
|
||||
// update ancestor sizes and fees
|
||||
tx.ancestorsize -= remove.vsize;
|
||||
tx.fees.ancestor -= remove.fees.base;
|
||||
}
|
||||
});
|
||||
// recalculate fee rates
|
||||
this.setAncestorScores(tx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a mempool transaction, and set the fee rates and ancestor score
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
private setAncestorScores(tx: MempoolTx): void {
|
||||
tx.individualRate = tx.fees.base / tx.vsize;
|
||||
tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize;
|
||||
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
|
||||
}
|
||||
|
||||
// Sort by descending score
|
||||
private mempoolComparator(a, b): number {
|
||||
return b.score - a.score;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccelerationCosts;
|
||||
export default new AccelerationCosts;
|
||||
|
||||
interface TemplateTransaction {
|
||||
txid: string;
|
||||
order: number;
|
||||
weight: number;
|
||||
adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
|
||||
sigops: number;
|
||||
fee: number;
|
||||
feeDelta: number;
|
||||
ancestors: string[];
|
||||
cluster: string[];
|
||||
effectiveFeePerVsize: number;
|
||||
}
|
||||
|
||||
interface MinerTransaction extends TemplateTransaction {
|
||||
inputs: string[];
|
||||
feePerVsize: number;
|
||||
relativesSet: boolean;
|
||||
ancestorMap: Map<string, MinerTransaction>;
|
||||
children: Set<MinerTransaction>;
|
||||
ancestorFee: number;
|
||||
ancestorVsize: number;
|
||||
ancestorSigops: number;
|
||||
score: number;
|
||||
used: boolean;
|
||||
modified: boolean;
|
||||
dependencyRate: number;
|
||||
}
|
||||
|
||||
/*
|
||||
* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
|
||||
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
|
||||
*/
|
||||
export function makeBlockTemplate(candidates: IEsploraApi.Transaction[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
|
||||
const auditPool: Map<string, MinerTransaction> = new Map();
|
||||
const mempoolArray: MinerTransaction[] = [];
|
||||
|
||||
candidates.forEach(tx => {
|
||||
// initializing everything up front helps V8 optimize property access later
|
||||
const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
|
||||
const feePerVsize = (tx.fee / adjustedVsize);
|
||||
auditPool.set(tx.txid, {
|
||||
txid: tx.txid,
|
||||
order: txidToOrdering(tx.txid),
|
||||
fee: tx.fee,
|
||||
feeDelta: 0,
|
||||
weight: tx.weight,
|
||||
adjustedVsize,
|
||||
feePerVsize: feePerVsize,
|
||||
effectiveFeePerVsize: feePerVsize,
|
||||
dependencyRate: feePerVsize,
|
||||
sigops: tx.sigops || 0,
|
||||
inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
|
||||
relativesSet: false,
|
||||
ancestors: [],
|
||||
cluster: [],
|
||||
ancestorMap: new Map<string, MinerTransaction>(),
|
||||
children: new Set<MinerTransaction>(),
|
||||
ancestorFee: 0,
|
||||
ancestorVsize: 0,
|
||||
ancestorSigops: 0,
|
||||
score: 0,
|
||||
used: false,
|
||||
modified: false,
|
||||
});
|
||||
mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
|
||||
});
|
||||
|
||||
// set accelerated effective fee
|
||||
for (const acceleration of accelerations) {
|
||||
const tx = auditPool.get(acceleration.txid);
|
||||
if (tx) {
|
||||
tx.feeDelta = acceleration.max_bid;
|
||||
tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
|
||||
tx.effectiveFeePerVsize = tx.feePerVsize;
|
||||
tx.dependencyRate = tx.feePerVsize;
|
||||
}
|
||||
}
|
||||
|
||||
// Build relatives graph & calculate ancestor scores
|
||||
for (const tx of mempoolArray) {
|
||||
if (!tx.relativesSet) {
|
||||
setRelatives(tx, auditPool);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by descending ancestor score
|
||||
mempoolArray.sort(priorityComparator);
|
||||
|
||||
// Build blocks by greedily choosing the highest feerate package
|
||||
// (i.e. the package rooted in the transaction with the best ancestor score)
|
||||
const blocks: number[][] = [];
|
||||
let blockWeight = 0;
|
||||
let blockSigops = 0;
|
||||
const transactions: MinerTransaction[] = [];
|
||||
let modified: MinerTransaction[] = [];
|
||||
const overflow: MinerTransaction[] = [];
|
||||
let failures = 0;
|
||||
while (mempoolArray.length || modified.length) {
|
||||
// skip invalid transactions
|
||||
while (mempoolArray[0].used || mempoolArray[0].modified) {
|
||||
mempoolArray.shift();
|
||||
}
|
||||
|
||||
// Select best next package
|
||||
let nextTx;
|
||||
const nextPoolTx = mempoolArray[0];
|
||||
const nextModifiedTx = modified[0];
|
||||
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
|
||||
nextTx = nextPoolTx;
|
||||
mempoolArray.shift();
|
||||
} else {
|
||||
modified.shift();
|
||||
if (nextModifiedTx) {
|
||||
nextTx = nextModifiedTx;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextTx && !nextTx?.used) {
|
||||
// Check if the package fits into this block
|
||||
if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
|
||||
const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
||||
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
||||
const clusterTxids = sortedTxSet.map(tx => tx.txid);
|
||||
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
|
||||
const used: MinerTransaction[] = [];
|
||||
while (sortedTxSet.length) {
|
||||
const ancestor = sortedTxSet.pop();
|
||||
if (!ancestor) {
|
||||
continue;
|
||||
}
|
||||
ancestor.used = true;
|
||||
ancestor.usedBy = nextTx.txid;
|
||||
// update this tx with effective fee rate & relatives data
|
||||
if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
|
||||
ancestor.effectiveFeePerVsize = effectiveFeeRate;
|
||||
}
|
||||
ancestor.cluster = clusterTxids;
|
||||
transactions.push(ancestor);
|
||||
blockWeight += ancestor.weight;
|
||||
blockSigops += ancestor.sigops;
|
||||
used.push(ancestor);
|
||||
}
|
||||
|
||||
// remove these as valid package ancestors for any descendants remaining in the mempool
|
||||
if (used.length) {
|
||||
used.forEach(tx => {
|
||||
modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
|
||||
});
|
||||
}
|
||||
|
||||
failures = 0;
|
||||
} else {
|
||||
// hold this package in an overflow list while we check for smaller options
|
||||
overflow.push(nextTx);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
// this block is full
|
||||
const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
|
||||
const queueEmpty = !mempoolArray.length && !modified.length;
|
||||
|
||||
if (exceededPackageTries || queueEmpty) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const tx of transactions) {
|
||||
tx.ancestors = Object.values(tx.ancestorMap);
|
||||
}
|
||||
|
||||
return transactions;
|
||||
}
|
||||
|
||||
// traverse in-mempool ancestors
|
||||
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
|
||||
function setRelatives(
|
||||
tx: MinerTransaction,
|
||||
mempool: Map<string, MinerTransaction>,
|
||||
): void {
|
||||
for (const parent of tx.inputs) {
|
||||
const parentTx = mempool.get(parent);
|
||||
if (parentTx && !tx.ancestorMap?.has(parent)) {
|
||||
tx.ancestorMap.set(parent, parentTx);
|
||||
parentTx.children.add(tx);
|
||||
// visit each node only once
|
||||
if (!parentTx.relativesSet) {
|
||||
setRelatives(parentTx, mempool);
|
||||
}
|
||||
parentTx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorMap.set(ancestor.txid, ancestor);
|
||||
});
|
||||
}
|
||||
};
|
||||
tx.ancestorFee = (tx.fee + tx.feeDelta);
|
||||
tx.ancestorVsize = tx.adjustedVsize || 0;
|
||||
tx.ancestorSigops = tx.sigops || 0;
|
||||
tx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
|
||||
tx.ancestorVsize += ancestor.adjustedVsize;
|
||||
tx.ancestorSigops += ancestor.sigops;
|
||||
});
|
||||
tx.score = tx.ancestorFee / tx.ancestorVsize;
|
||||
tx.relativesSet = true;
|
||||
}
|
||||
|
||||
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
|
||||
// avoids recursion to limit call stack depth
|
||||
function updateDescendants(
|
||||
rootTx: MinerTransaction,
|
||||
mempool: Map<string, MinerTransaction>,
|
||||
modified: MinerTransaction[],
|
||||
clusterRate: number,
|
||||
): MinerTransaction[] {
|
||||
const descendantSet: Set<MinerTransaction> = new Set();
|
||||
// stack of nodes left to visit
|
||||
const descendants: MinerTransaction[] = [];
|
||||
let descendantTx: MinerTransaction | undefined;
|
||||
rootTx.children.forEach(childTx => {
|
||||
if (!descendantSet.has(childTx)) {
|
||||
descendants.push(childTx);
|
||||
descendantSet.add(childTx);
|
||||
}
|
||||
});
|
||||
while (descendants.length) {
|
||||
descendantTx = descendants.pop();
|
||||
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
|
||||
// remove tx as ancestor
|
||||
descendantTx.ancestorMap.delete(rootTx.txid);
|
||||
descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
|
||||
descendantTx.ancestorVsize -= rootTx.adjustedVsize;
|
||||
descendantTx.ancestorSigops -= rootTx.sigops;
|
||||
descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
|
||||
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
|
||||
|
||||
if (!descendantTx.modified) {
|
||||
descendantTx.modified = true;
|
||||
modified.push(descendantTx);
|
||||
}
|
||||
|
||||
// add this node's children to the stack
|
||||
descendantTx.children.forEach(childTx => {
|
||||
// visit each node only once
|
||||
if (!descendantSet.has(childTx)) {
|
||||
descendants.push(childTx);
|
||||
descendantSet.add(childTx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// return new, resorted modified list
|
||||
return modified.sort(priorityComparator);
|
||||
}
|
||||
|
||||
// Used to sort an array of MinerTransactions by descending ancestor score
|
||||
function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
|
||||
if (b.score === a.score) {
|
||||
// tie-break by txid for stability
|
||||
return a.order - b.order;
|
||||
} else {
|
||||
return b.score - a.score;
|
||||
}
|
||||
}
|
||||
|
||||
// returns the most significant 4 bytes of the txid as an integer
|
||||
function txidToOrdering(txid: string): number {
|
||||
return parseInt(
|
||||
txid.substring(62, 64) +
|
||||
txid.substring(60, 62) +
|
||||
txid.substring(58, 60) +
|
||||
txid.substring(56, 58),
|
||||
16
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import bitcoinClient from './bitcoin-client';
|
||||
import difficultyAdjustment from '../difficulty-adjustment';
|
||||
import transactionRepository from '../../repositories/TransactionRepository';
|
||||
import rbfCache from '../rbf-cache';
|
||||
import { calculateMempoolTxCpfp } from '../cpfp';
|
||||
import { calculateCpfp } from '../cpfp';
|
||||
|
||||
class BitcoinRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
@@ -42,7 +42,6 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
|
||||
@@ -160,17 +159,15 @@ class BitcoinRoutes {
|
||||
descendants: tx.descendants || null,
|
||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
||||
sigops: tx.sigops,
|
||||
fee: tx.fee,
|
||||
adjustedVsize: tx.adjustedVsize,
|
||||
acceleration: tx.acceleration,
|
||||
acceleratedBy: tx.acceleratedBy || undefined,
|
||||
acceleratedAt: tx.acceleratedAt || undefined,
|
||||
feeDelta: tx.feeDelta || undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const cpfpInfo = calculateMempoolTxCpfp(tx, mempool.getMempool());
|
||||
const cpfpInfo = calculateCpfp(tx, mempool.getMempool());
|
||||
|
||||
res.json(cpfpInfo);
|
||||
return;
|
||||
@@ -364,20 +361,6 @@ class BitcoinRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getBlockTxAuditSummary(req: Request, res: Response) {
|
||||
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 {
|
||||
return res.status(404).send(`transaction audit not available`);
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getBlocks(req: Request, res: Response) {
|
||||
try {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
|
||||
|
||||
@@ -2,7 +2,7 @@ import config from '../config';
|
||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||
import logger from '../logger';
|
||||
import memPool from './mempool';
|
||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit, TransactionAudit } from '../mempool.interfaces';
|
||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
|
||||
import { Common } from './common';
|
||||
import diskCache from './disk-cache';
|
||||
import transactionUtils from './transaction-utils';
|
||||
@@ -30,9 +30,6 @@ import redisCache from './redis-cache';
|
||||
import rbfCache from './rbf-cache';
|
||||
import { calcBitsDifference } from './difficulty-adjustment';
|
||||
import AccelerationRepository from '../repositories/AccelerationRepository';
|
||||
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
|
||||
import mempool from './mempool';
|
||||
import CpfpRepository from '../repositories/CpfpRepository';
|
||||
|
||||
class Blocks {
|
||||
private blocks: BlockExtended[] = [];
|
||||
@@ -570,11 +567,8 @@ class Blocks {
|
||||
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||
const currentBlockHeight = blockchainInfo.blocks;
|
||||
|
||||
const targetSummaryVersion: number = 1;
|
||||
const targetTemplateVersion: number = 1;
|
||||
|
||||
const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesBelowVersion(targetSummaryVersion);
|
||||
const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesBelowVersion(targetTemplateVersion);
|
||||
const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesWithVersion(0);
|
||||
const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesWithVersion(0);
|
||||
|
||||
// nothing to do
|
||||
if (!unclassifiedBlocksList?.length && !unclassifiedTemplatesList?.length) {
|
||||
@@ -607,24 +601,16 @@ class Blocks {
|
||||
|
||||
for (let height = currentBlockHeight; height >= 0; height--) {
|
||||
try {
|
||||
let txs: MempoolTransactionExtended[] | null = null;
|
||||
let txs: TransactionExtended[] | null = null;
|
||||
if (unclassifiedBlocks[height]) {
|
||||
const blockHash = unclassifiedBlocks[height];
|
||||
// fetch transactions
|
||||
txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendMempoolTransaction(tx)) || [];
|
||||
txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendTransaction(tx)) || [];
|
||||
// add CPFP
|
||||
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
|
||||
const cpfpSummary = Common.calculateCpfp(height, txs, true);
|
||||
// classify
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
||||
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
|
||||
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
|
||||
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
|
||||
if (!cpfpRepository.compareClusters(cpfpClusters, cpfpSummary.clusters)) {
|
||||
// CPFP clusters changed - update the compact_cpfp tables
|
||||
await CpfpRepository.$deleteClustersAt(height);
|
||||
await this.$saveCpfp(blockHash, height, cpfpSummary);
|
||||
}
|
||||
}
|
||||
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 1);
|
||||
await Common.sleep$(250);
|
||||
}
|
||||
if (unclassifiedTemplates[height]) {
|
||||
@@ -650,7 +636,7 @@ class Blocks {
|
||||
}
|
||||
templateTxs.push(tx || templateTx);
|
||||
}
|
||||
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
|
||||
const cpfpSummary = Common.calculateCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as TransactionExtended[], true);
|
||||
// classify
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
||||
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
|
||||
@@ -904,7 +890,7 @@ class Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, Object.values(mempool.getAccelerations()).map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
||||
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
||||
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
||||
@@ -1163,7 +1149,7 @@ class Blocks {
|
||||
transactions: cpfpSummary.transactions.map(tx => {
|
||||
let flags: number = 0;
|
||||
try {
|
||||
flags = Common.getTransactionFlags(tx);
|
||||
flags = tx.flags || Common.getTransactionFlags(tx);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@@ -1373,14 +1359,6 @@ class Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
return BlocksAuditsRepository.$getBlockTxAudit(hash, txid);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public getLastDifficultyAdjustmentTime(): number {
|
||||
return this.lastDifficultyAdjustmentTime;
|
||||
}
|
||||
@@ -1413,7 +1391,7 @@ class Blocks {
|
||||
}
|
||||
|
||||
if (transactions?.length != null) {
|
||||
const summary = calculateFastBlockCpfp(height, transactions as TransactionExtended[]);
|
||||
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
|
||||
|
||||
await this.$saveCpfp(hash, height, summary);
|
||||
|
||||
|
||||
@@ -419,15 +419,12 @@ export class Common {
|
||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
|
||||
// Update variable flags (CPFP, RBF)
|
||||
flags &= ~TransactionFlags.cpfp_child;
|
||||
if (tx.ancestors?.length) {
|
||||
flags |= TransactionFlags.cpfp_child;
|
||||
}
|
||||
flags &= ~TransactionFlags.cpfp_parent;
|
||||
if (tx.descendants?.length) {
|
||||
flags |= TransactionFlags.cpfp_parent;
|
||||
}
|
||||
flags &= ~TransactionFlags.replacement;
|
||||
if (tx.replacement) {
|
||||
flags |= TransactionFlags.replacement;
|
||||
}
|
||||
@@ -809,6 +806,96 @@ export class Common {
|
||||
}
|
||||
}
|
||||
|
||||
static calculateCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
|
||||
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
|
||||
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
|
||||
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
|
||||
let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
|
||||
const txMap: { [txid: string]: TransactionExtended } = {};
|
||||
// initialize the txMap
|
||||
for (const tx of transactions) {
|
||||
txMap[tx.txid] = tx;
|
||||
}
|
||||
// reverse pass to identify CPFP clusters
|
||||
for (let i = transactions.length - 1; i >= 0; i--) {
|
||||
const tx = transactions[i];
|
||||
if (!ancestors[tx.txid]) {
|
||||
let totalFee = 0;
|
||||
let totalVSize = 0;
|
||||
clusterTxs.forEach(tx => {
|
||||
totalFee += tx?.fee || 0;
|
||||
totalVSize += (tx.weight / 4);
|
||||
});
|
||||
const effectiveFeePerVsize = totalFee / totalVSize;
|
||||
let cluster: CpfpCluster;
|
||||
if (clusterTxs.length > 1) {
|
||||
cluster = {
|
||||
root: clusterTxs[0].txid,
|
||||
height,
|
||||
txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
|
||||
effectiveFeePerVsize,
|
||||
};
|
||||
clusters.push(cluster);
|
||||
}
|
||||
clusterTxs.forEach(tx => {
|
||||
txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
|
||||
if (cluster) {
|
||||
clusterMap[tx.txid] = cluster;
|
||||
}
|
||||
});
|
||||
// reset working vars
|
||||
clusterTxs = [];
|
||||
ancestors = {};
|
||||
}
|
||||
clusterTxs.push(tx);
|
||||
tx.vin.forEach(vin => {
|
||||
ancestors[vin.txid] = true;
|
||||
});
|
||||
}
|
||||
// forward pass to enforce ancestor rate caps
|
||||
for (const tx of transactions) {
|
||||
let minAncestorRate = tx.effectiveFeePerVsize;
|
||||
for (const vin of tx.vin) {
|
||||
if (txMap[vin.txid]?.effectiveFeePerVsize) {
|
||||
minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize);
|
||||
}
|
||||
}
|
||||
// check rounded values to skip cases with almost identical fees
|
||||
const roundedMinAncestorRate = Math.ceil(minAncestorRate);
|
||||
const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize);
|
||||
if (roundedMinAncestorRate < roundedEffectiveFeeRate) {
|
||||
tx.effectiveFeePerVsize = minAncestorRate;
|
||||
if (!clusterMap[tx.txid]) {
|
||||
// add a single-tx cluster to record the dependent rate
|
||||
const cluster = {
|
||||
root: tx.txid,
|
||||
height,
|
||||
txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }],
|
||||
effectiveFeePerVsize: minAncestorRate,
|
||||
};
|
||||
clusterMap[tx.txid] = cluster;
|
||||
clusters.push(cluster);
|
||||
} else {
|
||||
// update the existing cluster with the dependent rate
|
||||
clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (saveRelatives) {
|
||||
for (const cluster of clusters) {
|
||||
cluster.txs.forEach((member, index) => {
|
||||
txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse();
|
||||
txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse();
|
||||
txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize;
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
transactions,
|
||||
clusters,
|
||||
};
|
||||
}
|
||||
|
||||
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats {
|
||||
const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate);
|
||||
|
||||
|
||||
@@ -1,172 +1,29 @@
|
||||
import { Ancestor, CpfpCluster, CpfpInfo, CpfpSummary, MempoolTransactionExtended, TransactionExtended } from '../mempool.interfaces';
|
||||
import { GraphTx, convertToGraphTx, expandRelativesGraph, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from './mini-miner';
|
||||
import { CpfpInfo, MempoolTransactionExtended } from '../mempool.interfaces';
|
||||
import memPool from './mempool';
|
||||
import { Acceleration } from './acceleration/acceleration';
|
||||
|
||||
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
|
||||
const MAX_CLUSTER_ITERATIONS = 100;
|
||||
const MAX_GRAPH_SIZE = 50; // the maximum number of in-mempool relatives to consider
|
||||
|
||||
export function calculateFastBlockCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
|
||||
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
|
||||
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
|
||||
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
|
||||
let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
|
||||
const txMap: { [txid: string]: TransactionExtended } = {};
|
||||
// initialize the txMap
|
||||
for (const tx of transactions) {
|
||||
txMap[tx.txid] = tx;
|
||||
}
|
||||
// reverse pass to identify CPFP clusters
|
||||
for (let i = transactions.length - 1; i >= 0; i--) {
|
||||
const tx = transactions[i];
|
||||
if (!ancestors[tx.txid]) {
|
||||
let totalFee = 0;
|
||||
let totalVSize = 0;
|
||||
clusterTxs.forEach(tx => {
|
||||
totalFee += tx?.fee || 0;
|
||||
totalVSize += (tx.weight / 4);
|
||||
});
|
||||
const effectiveFeePerVsize = totalFee / totalVSize;
|
||||
let cluster: CpfpCluster;
|
||||
if (clusterTxs.length > 1) {
|
||||
cluster = {
|
||||
root: clusterTxs[0].txid,
|
||||
height,
|
||||
txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
|
||||
effectiveFeePerVsize,
|
||||
};
|
||||
clusters.push(cluster);
|
||||
}
|
||||
clusterTxs.forEach(tx => {
|
||||
txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
|
||||
if (cluster) {
|
||||
clusterMap[tx.txid] = cluster;
|
||||
}
|
||||
});
|
||||
// reset working vars
|
||||
clusterTxs = [];
|
||||
ancestors = {};
|
||||
}
|
||||
clusterTxs.push(tx);
|
||||
tx.vin.forEach(vin => {
|
||||
ancestors[vin.txid] = true;
|
||||
});
|
||||
}
|
||||
// forward pass to enforce ancestor rate caps
|
||||
for (const tx of transactions) {
|
||||
let minAncestorRate = tx.effectiveFeePerVsize;
|
||||
for (const vin of tx.vin) {
|
||||
if (txMap[vin.txid]?.effectiveFeePerVsize) {
|
||||
minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize);
|
||||
}
|
||||
}
|
||||
// check rounded values to skip cases with almost identical fees
|
||||
const roundedMinAncestorRate = Math.ceil(minAncestorRate);
|
||||
const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize);
|
||||
if (roundedMinAncestorRate < roundedEffectiveFeeRate) {
|
||||
tx.effectiveFeePerVsize = minAncestorRate;
|
||||
if (!clusterMap[tx.txid]) {
|
||||
// add a single-tx cluster to record the dependent rate
|
||||
const cluster = {
|
||||
root: tx.txid,
|
||||
height,
|
||||
txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }],
|
||||
effectiveFeePerVsize: minAncestorRate,
|
||||
};
|
||||
clusterMap[tx.txid] = cluster;
|
||||
clusters.push(cluster);
|
||||
} else {
|
||||
// update the existing cluster with the dependent rate
|
||||
clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (saveRelatives) {
|
||||
for (const cluster of clusters) {
|
||||
cluster.txs.forEach((member, index) => {
|
||||
txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse();
|
||||
txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse();
|
||||
txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize;
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
transactions,
|
||||
clusters,
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateGoodBlockCpfp(height: number, transactions: MempoolTransactionExtended[], accelerations: Acceleration[]): CpfpSummary {
|
||||
const txMap: { [txid: string]: MempoolTransactionExtended } = {};
|
||||
for (const tx of transactions) {
|
||||
txMap[tx.txid] = tx;
|
||||
}
|
||||
const template = makeBlockTemplate(transactions, accelerations, 1, Infinity, Infinity);
|
||||
const clusters = new Map<string, string[]>();
|
||||
for (const tx of template) {
|
||||
const cluster = tx.cluster || [];
|
||||
const root = cluster.length ? cluster[cluster.length - 1] : null;
|
||||
if (cluster.length > 1 && root && !clusters.has(root)) {
|
||||
clusters.set(root, cluster);
|
||||
}
|
||||
txMap[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
|
||||
}
|
||||
|
||||
const clusterArray: CpfpCluster[] = [];
|
||||
|
||||
for (const cluster of clusters.values()) {
|
||||
for (const txid of cluster) {
|
||||
const mempoolTx = txMap[txid];
|
||||
if (mempoolTx) {
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
let matched = false;
|
||||
cluster.forEach(relativeTxid => {
|
||||
if (relativeTxid === txid) {
|
||||
matched = true;
|
||||
} else {
|
||||
const relative = {
|
||||
txid: relativeTxid,
|
||||
fee: txMap[relativeTxid].fee,
|
||||
weight: (txMap[relativeTxid].adjustedVsize * 4) || txMap[relativeTxid].weight,
|
||||
};
|
||||
if (matched) {
|
||||
descendants.push(relative);
|
||||
} else {
|
||||
ancestors.push(relative);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
Object.assign(mempoolTx, { ancestors, descendants, bestDescendant: null, cpfpChecked: true });
|
||||
}
|
||||
}
|
||||
const root = cluster[cluster.length - 1];
|
||||
clusterArray.push({
|
||||
root: root,
|
||||
height,
|
||||
txs: cluster.reverse().map(txid => ({
|
||||
txid,
|
||||
fee: txMap[txid].fee,
|
||||
weight: (txMap[txid].adjustedVsize * 4) || txMap[txid].weight,
|
||||
})),
|
||||
effectiveFeePerVsize: txMap[root].effectiveFeePerVsize,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
transactions: transactions.map(tx => txMap[tx.txid]),
|
||||
clusters: clusterArray,
|
||||
interface GraphTx extends MempoolTransactionExtended {
|
||||
depends: string[];
|
||||
spentby: string[];
|
||||
ancestorMap: Map<string, GraphTx>;
|
||||
fees: {
|
||||
base: number;
|
||||
ancestor: number;
|
||||
};
|
||||
ancestorcount: number;
|
||||
ancestorsize: number;
|
||||
ancestorRate: number;
|
||||
individualRate: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
|
||||
export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
|
||||
if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) {
|
||||
tx.cpfpDirty = false;
|
||||
return {
|
||||
@@ -175,31 +32,30 @@ export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool:
|
||||
descendants: tx.descendants || [],
|
||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize,
|
||||
sigops: tx.sigops,
|
||||
fee: tx.fee,
|
||||
adjustedVsize: tx.adjustedVsize,
|
||||
acceleration: tx.acceleration
|
||||
};
|
||||
}
|
||||
|
||||
const ancestorMap = new Map<string, GraphTx>();
|
||||
const graphTx = convertToGraphTx(tx, memPool.getSpendMap());
|
||||
const graphTx = mempoolToGraphTx(tx);
|
||||
ancestorMap.set(tx.txid, graphTx);
|
||||
|
||||
const allRelatives = expandRelativesGraph(mempool, ancestorMap, memPool.getSpendMap());
|
||||
const allRelatives = expandRelativesGraph(mempool, ancestorMap);
|
||||
const relativesMap = initializeRelatives(allRelatives);
|
||||
const cluster = calculateCpfpCluster(tx.txid, relativesMap);
|
||||
|
||||
let totalVsize = 0;
|
||||
let totalFee = 0;
|
||||
for (const tx of cluster.values()) {
|
||||
totalVsize += tx.vsize;
|
||||
totalFee += tx.fees.base;
|
||||
totalVsize += tx.adjustedVsize;
|
||||
totalFee += tx.fee;
|
||||
}
|
||||
const effectiveFeePerVsize = totalFee / totalVsize;
|
||||
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].ancestors = Array.from(tx.ancestorMap.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee }));
|
||||
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestorMap.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee }));
|
||||
mempool[tx.txid].bestDescendant = null;
|
||||
mempool[tx.txid].cpfpChecked = true;
|
||||
mempool[tx.txid].cpfpDirty = true;
|
||||
@@ -214,12 +70,88 @@ export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool:
|
||||
descendants: tx.descendants || [],
|
||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize,
|
||||
sigops: tx.sigops,
|
||||
fee: tx.fee,
|
||||
adjustedVsize: tx.adjustedVsize,
|
||||
acceleration: tx.acceleration
|
||||
};
|
||||
}
|
||||
|
||||
function mempoolToGraphTx(tx: MempoolTransactionExtended): GraphTx {
|
||||
return {
|
||||
...tx,
|
||||
depends: tx.vin.map(v => v.txid),
|
||||
spentby: tx.vout.map((v, i) => memPool.getFromSpendMap(tx.txid, i)).map(tx => tx?.txid).filter(txid => txid != null) as string[],
|
||||
ancestorMap: new Map(),
|
||||
fees: {
|
||||
base: tx.fee,
|
||||
ancestor: tx.fee,
|
||||
},
|
||||
ancestorcount: 1,
|
||||
ancestorsize: tx.adjustedVsize,
|
||||
ancestorRate: 0,
|
||||
individualRate: 0,
|
||||
score: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives
|
||||
*/
|
||||
function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>): Map<string, GraphTx> {
|
||||
const relatives: Map<string, GraphTx> = new Map();
|
||||
const stack: GraphTx[] = Array.from(ancestors.values());
|
||||
while (stack.length > 0) {
|
||||
if (relatives.size > MAX_GRAPH_SIZE) {
|
||||
return relatives;
|
||||
}
|
||||
|
||||
const nextTx = stack.pop();
|
||||
if (!nextTx) {
|
||||
continue;
|
||||
}
|
||||
relatives.set(nextTx.txid, nextTx);
|
||||
|
||||
for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) {
|
||||
if (relatives.has(relativeTxid)) {
|
||||
// already processed this tx
|
||||
continue;
|
||||
}
|
||||
let mempoolTx = ancestors.get(relativeTxid);
|
||||
if (!mempoolTx && mempool[relativeTxid]) {
|
||||
mempoolTx = mempoolToGraphTx(mempool[relativeTxid]);
|
||||
}
|
||||
if (mempoolTx) {
|
||||
stack.push(mempoolTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
|
||||
* by running setAncestors on each leaf, and caching intermediate results.
|
||||
* then initializes ancestor data for each transaction
|
||||
*
|
||||
* @param all
|
||||
*/
|
||||
function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> {
|
||||
const visited: Map<string, Map<string, GraphTx>> = new Map();
|
||||
const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
|
||||
for (const leaf of leaves) {
|
||||
setAncestors(leaf, mempoolTxs, visited);
|
||||
}
|
||||
mempoolTxs.forEach(entry => {
|
||||
entry.ancestorMap?.forEach(ancestor => {
|
||||
entry.ancestorcount++;
|
||||
entry.ancestorsize += ancestor.adjustedVsize;
|
||||
entry.fees.ancestor += ancestor.fees.base;
|
||||
});
|
||||
setAncestorScores(entry);
|
||||
});
|
||||
return mempoolTxs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a root transaction and a list of in-mempool ancestors,
|
||||
* Calculate the CPFP cluster
|
||||
@@ -240,10 +172,10 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
|
||||
let sortedRelatives = Array.from(graph.values()).sort(mempoolComparator);
|
||||
|
||||
// Iterate until we reach a cluster that includes our target tx
|
||||
let maxIterations = MAX_CLUSTER_ITERATIONS;
|
||||
let maxIterations = MAX_GRAPH_SIZE;
|
||||
let best = sortedRelatives.shift();
|
||||
let bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []);
|
||||
while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestors.has(tx.txid)) && maxIterations > 0) {
|
||||
let bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []);
|
||||
while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestorMap.has(tx.txid)) && maxIterations > 0) {
|
||||
maxIterations--;
|
||||
if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) {
|
||||
break;
|
||||
@@ -258,7 +190,7 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
|
||||
// Grab the next highest scoring entry
|
||||
best = sortedRelatives.shift();
|
||||
if (best) {
|
||||
bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []);
|
||||
bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []);
|
||||
bestCluster.set(best?.txid, best);
|
||||
}
|
||||
}
|
||||
@@ -267,4 +199,88 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
|
||||
bestCluster.set(tx.txid, tx);
|
||||
|
||||
return bestCluster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a cluster of transactions from an in-mempool dependency graph
|
||||
* and update the survivors' scores and ancestors
|
||||
*
|
||||
* @param cluster
|
||||
* @param ancestors
|
||||
*/
|
||||
function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void {
|
||||
// remove
|
||||
cluster.forEach(tx => {
|
||||
all.delete(tx.txid);
|
||||
});
|
||||
|
||||
// update survivors
|
||||
all.forEach(tx => {
|
||||
cluster.forEach(remove => {
|
||||
if (tx.ancestorMap?.has(remove.txid)) {
|
||||
// remove as dependency
|
||||
tx.ancestorMap.delete(remove.txid);
|
||||
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
|
||||
// update ancestor sizes and fees
|
||||
tx.ancestorsize -= remove.adjustedVsize;
|
||||
tx.fees.ancestor -= remove.fees.base;
|
||||
}
|
||||
});
|
||||
// recalculate fee rates
|
||||
setAncestorScores(tx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
|
||||
* for each transaction.
|
||||
*
|
||||
* @param tx
|
||||
* @param all
|
||||
*/
|
||||
function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> {
|
||||
// sanity check for infinite recursion / too many ancestors (should never happen)
|
||||
if (depth > MAX_GRAPH_SIZE) {
|
||||
return tx.ancestorMap;
|
||||
}
|
||||
|
||||
// initialize the ancestor map for this tx
|
||||
tx.ancestorMap = new Map<string, GraphTx>();
|
||||
tx.depends.forEach(parentId => {
|
||||
const parent = all.get(parentId);
|
||||
if (parent) {
|
||||
// add the parent
|
||||
tx.ancestorMap?.set(parentId, parent);
|
||||
// check for a cached copy of this parent's ancestors
|
||||
let ancestors = visited.get(parent.txid);
|
||||
if (!ancestors) {
|
||||
// recursively fetch the parent's ancestors
|
||||
ancestors = setAncestors(parent, all, visited, depth + 1);
|
||||
}
|
||||
// and add to this tx's map
|
||||
ancestors.forEach((ancestor, ancestorId) => {
|
||||
tx.ancestorMap?.set(ancestorId, ancestor);
|
||||
});
|
||||
}
|
||||
});
|
||||
visited.set(tx.txid, tx.ancestorMap);
|
||||
|
||||
return tx.ancestorMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a mempool transaction, and set the fee rates and ancestor score
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
function setAncestorScores(tx: GraphTx): GraphTx {
|
||||
tx.individualRate = (tx.fees.base * 100_000_000) / tx.adjustedVsize;
|
||||
tx.ancestorRate = (tx.fees.ancestor * 100_000_000) / tx.ancestorsize;
|
||||
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Sort by descending score
|
||||
function mempoolComparator(a: GraphTx, b: GraphTx): number {
|
||||
return b.score - a.score;
|
||||
}
|
||||
@@ -453,7 +453,6 @@ class MempoolBlocks {
|
||||
mempoolTx.acceleration = true;
|
||||
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
||||
mempoolTx.acceleratedAt = acceleration?.added;
|
||||
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||
for (const ancestor of mempoolTx.ancestors || []) {
|
||||
if (!mempool[ancestor.txid].acceleration) {
|
||||
mempool[ancestor.txid].cpfpDirty = true;
|
||||
@@ -461,7 +460,6 @@ class MempoolBlocks {
|
||||
mempool[ancestor.txid].acceleration = true;
|
||||
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
|
||||
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
|
||||
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
|
||||
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -396,6 +396,10 @@ class Mempool {
|
||||
}
|
||||
|
||||
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const changed: string[] = [];
|
||||
|
||||
|
||||
@@ -1,515 +0,0 @@
|
||||
import { Acceleration } from './acceleration/acceleration';
|
||||
import { MempoolTransactionExtended } from '../mempool.interfaces';
|
||||
import logger from '../logger';
|
||||
|
||||
const BLOCK_WEIGHT_UNITS = 4_000_000;
|
||||
const BLOCK_SIGOPS = 80_000;
|
||||
const MAX_RELATIVE_GRAPH_SIZE = 100;
|
||||
|
||||
export interface GraphTx {
|
||||
txid: string;
|
||||
vsize: number;
|
||||
weight: number;
|
||||
depends: string[];
|
||||
spentby: string[];
|
||||
|
||||
ancestorcount: number;
|
||||
ancestorsize: number;
|
||||
fees: { // in sats
|
||||
base: number;
|
||||
ancestor: number;
|
||||
};
|
||||
|
||||
ancestors: Map<string, GraphTx>,
|
||||
ancestorRate: number;
|
||||
individualRate: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
interface TemplateTransaction {
|
||||
txid: string;
|
||||
order: number;
|
||||
weight: number;
|
||||
adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
|
||||
sigops: number;
|
||||
fee: number;
|
||||
feeDelta: number;
|
||||
ancestors: string[];
|
||||
cluster: string[];
|
||||
effectiveFeePerVsize: number;
|
||||
}
|
||||
|
||||
interface MinerTransaction extends TemplateTransaction {
|
||||
inputs: string[];
|
||||
feePerVsize: number;
|
||||
relativesSet: boolean;
|
||||
ancestorMap: Map<string, MinerTransaction>;
|
||||
children: Set<MinerTransaction>;
|
||||
ancestorFee: number;
|
||||
ancestorVsize: number;
|
||||
ancestorSigops: number;
|
||||
score: number;
|
||||
used: boolean;
|
||||
modified: boolean;
|
||||
dependencyRate: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a raw transaction, and builds a graph of same-block relatives,
|
||||
* and returns as a GraphTx
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
export function getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> {
|
||||
const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
|
||||
const spendMap = new Map<string, string>(); // map of outpoints to spending txids
|
||||
for (const tx of transactions) {
|
||||
blockTxs.set(tx.txid, tx);
|
||||
for (const vin of tx.vin) {
|
||||
spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
|
||||
}
|
||||
}
|
||||
|
||||
const relatives: Map<string, GraphTx> = new Map();
|
||||
const stack: string[] = [tx.txid];
|
||||
|
||||
// build set of same-block ancestors
|
||||
while (stack.length > 0) {
|
||||
const nextTxid = stack.pop();
|
||||
const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
|
||||
if (!nextTx || relatives.has(nextTx.txid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mempoolTx = convertToGraphTx(nextTx, spendMap);
|
||||
|
||||
for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
|
||||
if (txid) {
|
||||
stack.push(txid);
|
||||
}
|
||||
}
|
||||
|
||||
relatives.set(mempoolTx.txid, mempoolTx);
|
||||
}
|
||||
|
||||
return relatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a raw transaction and converts it to GraphTx format
|
||||
* fee and ancestor data is initialized with dummy/null values
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
export function convertToGraphTx(tx: MempoolTransactionExtended, spendMap?: Map<string, MempoolTransactionExtended | string>): GraphTx {
|
||||
return {
|
||||
txid: tx.txid,
|
||||
vsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)),
|
||||
weight: tx.weight,
|
||||
fees: {
|
||||
base: tx.fee || 0,
|
||||
ancestor: tx.fee || 0,
|
||||
},
|
||||
depends: (tx.vin.map(vin => vin.txid).filter(depend => depend) as string[]),
|
||||
spentby: spendMap ? (tx.vout.map((vout, index) => { const spend = spendMap.get(`${tx.txid}:${index}`); return (spend?.['txid'] || spend); }).filter(spent => spent) as string[]) : [],
|
||||
|
||||
ancestorcount: 1,
|
||||
ancestorsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)),
|
||||
ancestors: new Map<string, GraphTx>(),
|
||||
ancestorRate: 0,
|
||||
individualRate: 0,
|
||||
score: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives
|
||||
*/
|
||||
export function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>, spendMap: Map<string, MempoolTransactionExtended>): Map<string, GraphTx> {
|
||||
const relatives: Map<string, GraphTx> = new Map();
|
||||
const stack: GraphTx[] = Array.from(ancestors.values());
|
||||
while (stack.length > 0) {
|
||||
if (relatives.size > MAX_RELATIVE_GRAPH_SIZE) {
|
||||
return relatives;
|
||||
}
|
||||
|
||||
const nextTx = stack.pop();
|
||||
if (!nextTx) {
|
||||
continue;
|
||||
}
|
||||
relatives.set(nextTx.txid, nextTx);
|
||||
|
||||
for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) {
|
||||
if (relatives.has(relativeTxid)) {
|
||||
// already processed this tx
|
||||
continue;
|
||||
}
|
||||
let ancestorTx = ancestors.get(relativeTxid);
|
||||
if (!ancestorTx && relativeTxid in mempool) {
|
||||
const mempoolTx = mempool[relativeTxid];
|
||||
ancestorTx = convertToGraphTx(mempoolTx, spendMap);
|
||||
}
|
||||
if (ancestorTx) {
|
||||
stack.push(ancestorTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
|
||||
* for each transaction.
|
||||
*
|
||||
* @param tx
|
||||
* @param all
|
||||
*/
|
||||
function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> {
|
||||
// sanity check for infinite recursion / too many ancestors (should never happen)
|
||||
if (depth > MAX_RELATIVE_GRAPH_SIZE) {
|
||||
logger.warn('cpfp dependency calculation failed: setAncestors reached depth of 100, unable to proceed');
|
||||
return tx.ancestors;
|
||||
}
|
||||
|
||||
// initialize the ancestor map for this tx
|
||||
tx.ancestors = new Map<string, GraphTx>();
|
||||
tx.depends.forEach(parentId => {
|
||||
const parent = all.get(parentId);
|
||||
if (parent) {
|
||||
// add the parent
|
||||
tx.ancestors?.set(parentId, parent);
|
||||
// check for a cached copy of this parent's ancestors
|
||||
let ancestors = visited.get(parent.txid);
|
||||
if (!ancestors) {
|
||||
// recursively fetch the parent's ancestors
|
||||
ancestors = setAncestors(parent, all, visited, depth + 1);
|
||||
}
|
||||
// and add to this tx's map
|
||||
ancestors.forEach((ancestor, ancestorId) => {
|
||||
tx.ancestors?.set(ancestorId, ancestor);
|
||||
});
|
||||
}
|
||||
});
|
||||
visited.set(tx.txid, tx.ancestors);
|
||||
|
||||
return tx.ancestors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
|
||||
* by running setAncestors on each leaf, and caching intermediate results.
|
||||
* then initializes ancestor data for each transaction
|
||||
*
|
||||
* @param all
|
||||
*/
|
||||
export function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> {
|
||||
const visited: Map<string, Map<string, GraphTx>> = new Map();
|
||||
const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
|
||||
for (const leaf of leaves) {
|
||||
setAncestors(leaf, mempoolTxs, visited);
|
||||
}
|
||||
mempoolTxs.forEach(entry => {
|
||||
entry.ancestors?.forEach(ancestor => {
|
||||
entry.ancestorcount++;
|
||||
entry.ancestorsize += ancestor.vsize;
|
||||
entry.fees.ancestor += ancestor.fees.base;
|
||||
});
|
||||
setAncestorScores(entry);
|
||||
});
|
||||
return mempoolTxs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a cluster of transactions from an in-mempool dependency graph
|
||||
* and update the survivors' scores and ancestors
|
||||
*
|
||||
* @param cluster
|
||||
* @param ancestors
|
||||
*/
|
||||
export function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void {
|
||||
// remove
|
||||
cluster.forEach(tx => {
|
||||
all.delete(tx.txid);
|
||||
});
|
||||
|
||||
// update survivors
|
||||
all.forEach(tx => {
|
||||
cluster.forEach(remove => {
|
||||
if (tx.ancestors?.has(remove.txid)) {
|
||||
// remove as dependency
|
||||
tx.ancestors.delete(remove.txid);
|
||||
tx.depends = tx.depends.filter(parent => parent !== remove.txid);
|
||||
// update ancestor sizes and fees
|
||||
tx.ancestorsize -= remove.vsize;
|
||||
tx.fees.ancestor -= remove.fees.base;
|
||||
}
|
||||
});
|
||||
// recalculate fee rates
|
||||
setAncestorScores(tx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a mempool transaction, and set the fee rates and ancestor score
|
||||
*
|
||||
* @param tx
|
||||
*/
|
||||
export function setAncestorScores(tx: GraphTx): void {
|
||||
tx.individualRate = tx.fees.base / tx.vsize;
|
||||
tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize;
|
||||
tx.score = Math.min(tx.individualRate, tx.ancestorRate);
|
||||
}
|
||||
|
||||
// Sort by descending score
|
||||
export function mempoolComparator(a: GraphTx, b: GraphTx): number {
|
||||
return b.score - a.score;
|
||||
}
|
||||
|
||||
/*
|
||||
* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
|
||||
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
|
||||
*/
|
||||
export function makeBlockTemplate(candidates: MempoolTransactionExtended[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
|
||||
const auditPool: Map<string, MinerTransaction> = new Map();
|
||||
const mempoolArray: MinerTransaction[] = [];
|
||||
|
||||
candidates.forEach(tx => {
|
||||
// initializing everything up front helps V8 optimize property access later
|
||||
const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
|
||||
const feePerVsize = (tx.fee / adjustedVsize);
|
||||
auditPool.set(tx.txid, {
|
||||
txid: tx.txid,
|
||||
order: txidToOrdering(tx.txid),
|
||||
fee: tx.fee,
|
||||
feeDelta: 0,
|
||||
weight: tx.weight,
|
||||
adjustedVsize,
|
||||
feePerVsize: feePerVsize,
|
||||
effectiveFeePerVsize: feePerVsize,
|
||||
dependencyRate: feePerVsize,
|
||||
sigops: tx.sigops || 0,
|
||||
inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
|
||||
relativesSet: false,
|
||||
ancestors: [],
|
||||
cluster: [],
|
||||
ancestorMap: new Map<string, MinerTransaction>(),
|
||||
children: new Set<MinerTransaction>(),
|
||||
ancestorFee: 0,
|
||||
ancestorVsize: 0,
|
||||
ancestorSigops: 0,
|
||||
score: 0,
|
||||
used: false,
|
||||
modified: false,
|
||||
});
|
||||
mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
|
||||
});
|
||||
|
||||
// set accelerated effective fee
|
||||
for (const acceleration of accelerations) {
|
||||
const tx = auditPool.get(acceleration.txid);
|
||||
if (tx) {
|
||||
tx.feeDelta = acceleration.max_bid;
|
||||
tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
|
||||
tx.effectiveFeePerVsize = tx.feePerVsize;
|
||||
tx.dependencyRate = tx.feePerVsize;
|
||||
}
|
||||
}
|
||||
|
||||
// Build relatives graph & calculate ancestor scores
|
||||
for (const tx of mempoolArray) {
|
||||
if (!tx.relativesSet) {
|
||||
setRelatives(tx, auditPool);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by descending ancestor score
|
||||
mempoolArray.sort(priorityComparator);
|
||||
|
||||
// Build blocks by greedily choosing the highest feerate package
|
||||
// (i.e. the package rooted in the transaction with the best ancestor score)
|
||||
const blocks: number[][] = [];
|
||||
let blockWeight = 0;
|
||||
let blockSigops = 0;
|
||||
const transactions: MinerTransaction[] = [];
|
||||
let modified: MinerTransaction[] = [];
|
||||
const overflow: MinerTransaction[] = [];
|
||||
let failures = 0;
|
||||
while (mempoolArray.length || modified.length) {
|
||||
// skip invalid transactions
|
||||
while (mempoolArray[0].used || mempoolArray[0].modified) {
|
||||
mempoolArray.shift();
|
||||
}
|
||||
|
||||
// Select best next package
|
||||
let nextTx;
|
||||
const nextPoolTx = mempoolArray[0];
|
||||
const nextModifiedTx = modified[0];
|
||||
if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
|
||||
nextTx = nextPoolTx;
|
||||
mempoolArray.shift();
|
||||
} else {
|
||||
modified.shift();
|
||||
if (nextModifiedTx) {
|
||||
nextTx = nextModifiedTx;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextTx && !nextTx?.used) {
|
||||
// Check if the package fits into this block
|
||||
if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
|
||||
const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
||||
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
||||
const clusterTxids = sortedTxSet.map(tx => tx.txid);
|
||||
const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
|
||||
const used: MinerTransaction[] = [];
|
||||
while (sortedTxSet.length) {
|
||||
const ancestor = sortedTxSet.pop();
|
||||
if (!ancestor) {
|
||||
continue;
|
||||
}
|
||||
ancestor.used = true;
|
||||
ancestor.usedBy = nextTx.txid;
|
||||
// update this tx with effective fee rate & relatives data
|
||||
if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
|
||||
ancestor.effectiveFeePerVsize = effectiveFeeRate;
|
||||
}
|
||||
ancestor.cluster = clusterTxids;
|
||||
transactions.push(ancestor);
|
||||
blockWeight += ancestor.weight;
|
||||
blockSigops += ancestor.sigops;
|
||||
used.push(ancestor);
|
||||
}
|
||||
|
||||
// remove these as valid package ancestors for any descendants remaining in the mempool
|
||||
if (used.length) {
|
||||
used.forEach(tx => {
|
||||
modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
|
||||
});
|
||||
}
|
||||
|
||||
failures = 0;
|
||||
} else {
|
||||
// hold this package in an overflow list while we check for smaller options
|
||||
overflow.push(nextTx);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
// this block is full
|
||||
const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
|
||||
const queueEmpty = !mempoolArray.length && !modified.length;
|
||||
|
||||
if (exceededPackageTries || queueEmpty) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const tx of transactions) {
|
||||
tx.ancestors = Object.values(tx.ancestorMap);
|
||||
}
|
||||
|
||||
return transactions;
|
||||
}
|
||||
|
||||
// traverse in-mempool ancestors
|
||||
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
|
||||
function setRelatives(
|
||||
tx: MinerTransaction,
|
||||
mempool: Map<string, MinerTransaction>,
|
||||
): void {
|
||||
for (const parent of tx.inputs) {
|
||||
const parentTx = mempool.get(parent);
|
||||
if (parentTx && !tx.ancestorMap?.has(parent)) {
|
||||
tx.ancestorMap.set(parent, parentTx);
|
||||
parentTx.children.add(tx);
|
||||
// visit each node only once
|
||||
if (!parentTx.relativesSet) {
|
||||
setRelatives(parentTx, mempool);
|
||||
}
|
||||
parentTx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorMap.set(ancestor.txid, ancestor);
|
||||
});
|
||||
}
|
||||
};
|
||||
tx.ancestorFee = (tx.fee + tx.feeDelta);
|
||||
tx.ancestorVsize = tx.adjustedVsize || 0;
|
||||
tx.ancestorSigops = tx.sigops || 0;
|
||||
tx.ancestorMap.forEach((ancestor) => {
|
||||
tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
|
||||
tx.ancestorVsize += ancestor.adjustedVsize;
|
||||
tx.ancestorSigops += ancestor.sigops;
|
||||
});
|
||||
tx.score = tx.ancestorFee / tx.ancestorVsize;
|
||||
tx.relativesSet = true;
|
||||
}
|
||||
|
||||
// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
|
||||
// avoids recursion to limit call stack depth
|
||||
function updateDescendants(
|
||||
rootTx: MinerTransaction,
|
||||
mempool: Map<string, MinerTransaction>,
|
||||
modified: MinerTransaction[],
|
||||
clusterRate: number,
|
||||
): MinerTransaction[] {
|
||||
const descendantSet: Set<MinerTransaction> = new Set();
|
||||
// stack of nodes left to visit
|
||||
const descendants: MinerTransaction[] = [];
|
||||
let descendantTx: MinerTransaction | undefined;
|
||||
rootTx.children.forEach(childTx => {
|
||||
if (!descendantSet.has(childTx)) {
|
||||
descendants.push(childTx);
|
||||
descendantSet.add(childTx);
|
||||
}
|
||||
});
|
||||
while (descendants.length) {
|
||||
descendantTx = descendants.pop();
|
||||
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
|
||||
// remove tx as ancestor
|
||||
descendantTx.ancestorMap.delete(rootTx.txid);
|
||||
descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
|
||||
descendantTx.ancestorVsize -= rootTx.adjustedVsize;
|
||||
descendantTx.ancestorSigops -= rootTx.sigops;
|
||||
descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
|
||||
descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
|
||||
|
||||
if (!descendantTx.modified) {
|
||||
descendantTx.modified = true;
|
||||
modified.push(descendantTx);
|
||||
}
|
||||
|
||||
// add this node's children to the stack
|
||||
descendantTx.children.forEach(childTx => {
|
||||
// visit each node only once
|
||||
if (!descendantSet.has(childTx)) {
|
||||
descendants.push(childTx);
|
||||
descendantSet.add(childTx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// return new, resorted modified list
|
||||
return modified.sort(priorityComparator);
|
||||
}
|
||||
|
||||
// Used to sort an array of MinerTransactions by descending ancestor score
|
||||
function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
|
||||
if (b.score === a.score) {
|
||||
// tie-break by txid for stability
|
||||
return a.order - b.order;
|
||||
} else {
|
||||
return b.score - a.score;
|
||||
}
|
||||
}
|
||||
|
||||
// returns the most significant 4 bytes of the txid as an integer
|
||||
function txidToOrdering(txid: string): number {
|
||||
return parseInt(
|
||||
txid.substring(62, 64) +
|
||||
txid.substring(60, 62) +
|
||||
txid.substring(58, 60) +
|
||||
txid.substring(56, 58),
|
||||
16
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import bitcoinClient from '../bitcoin/bitcoin-client';
|
||||
import mining from "./mining";
|
||||
import PricesRepository from '../../repositories/PricesRepository';
|
||||
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
||||
import accelerationApi from '../services/acceleration';
|
||||
|
||||
class MiningRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
@@ -42,8 +41,6 @@ class MiningRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/total', this.$getAccelerationTotals)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'accelerations', this.$getActiveAccelerations)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'acceleration/request/:txid', this.$requestAcceleration)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -448,33 +445,6 @@ class MiningRoutes {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getActiveAccelerations(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(accelerationApi.accelerations || []);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $requestAcceleration(req: Request, res: Response): Promise<void> {
|
||||
res.setHeader('Pragma', 'no-cache');
|
||||
res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
|
||||
res.setHeader('expires', -1);
|
||||
try {
|
||||
accelerationApi.accelerationRequested(req.params.txid);
|
||||
res.status(200).send('ok');
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MiningRoutes();
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import config from '../../config';
|
||||
import logger from '../../logger';
|
||||
import { BlockExtended } from '../../mempool.interfaces';
|
||||
import { BlockExtended, PoolTag } from '../../mempool.interfaces';
|
||||
import axios from 'axios';
|
||||
|
||||
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
|
||||
|
||||
export interface Acceleration {
|
||||
txid: string,
|
||||
added: number,
|
||||
@@ -37,91 +35,18 @@ export interface AccelerationHistory {
|
||||
};
|
||||
|
||||
class AccelerationApi {
|
||||
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
|
||||
private _accelerations: Acceleration[] | null = null;
|
||||
private lastPoll = 0;
|
||||
private forcePoll = false;
|
||||
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
|
||||
|
||||
public get accelerations(): Acceleration[] | null {
|
||||
return this._accelerations;
|
||||
}
|
||||
|
||||
public countMyAccelerationsWithStatus(filter: MyAccelerationStatus): number {
|
||||
return Object.values(this.myAccelerations).reduce((count, {status}) => { return count + (status === filter ? 1 : 0); }, 0);
|
||||
}
|
||||
|
||||
public accelerationRequested(txid: string): void {
|
||||
if (this.onDemandPollingEnabled) {
|
||||
this.myAccelerations[txid] = { status: 'requested', added: Date.now() };
|
||||
}
|
||||
}
|
||||
|
||||
public accelerationConfirmed(): void {
|
||||
this.forcePoll = true;
|
||||
}
|
||||
|
||||
private async $fetchAccelerations(): Promise<Acceleration[] | null> {
|
||||
try {
|
||||
const response = await axios.get(this.apiPath, { responseType: 'json', timeout: 10000 });
|
||||
return response?.data || [];
|
||||
} catch (e) {
|
||||
logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async $updateAccelerations(): Promise<Acceleration[] | null> {
|
||||
if (!this.onDemandPollingEnabled) {
|
||||
const accelerations = await this.$fetchAccelerations();
|
||||
if (accelerations) {
|
||||
this._accelerations = accelerations;
|
||||
return this._accelerations;
|
||||
public async $fetchAccelerations(): Promise<Acceleration[] | null> {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
try {
|
||||
const response = await axios.get(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`, { responseType: 'json', timeout: 10000 });
|
||||
return response.data as Acceleration[];
|
||||
} catch (e) {
|
||||
logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e));
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return this.$updateAccelerationsOnDemand();
|
||||
return [];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
|
||||
const shouldUpdate = this.forcePoll
|
||||
|| this.countMyAccelerationsWithStatus('requested') > 0
|
||||
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
|
||||
|
||||
// update accelerations if necessary
|
||||
if (shouldUpdate) {
|
||||
const accelerations = await this.$fetchAccelerations();
|
||||
this.lastPoll = Date.now();
|
||||
this.forcePoll = false;
|
||||
if (accelerations) {
|
||||
const latestAccelerations: Record<string, Acceleration> = {};
|
||||
// set relevant accelerations to 'accelerating'
|
||||
for (const acc of accelerations) {
|
||||
if (this.myAccelerations[acc.txid]) {
|
||||
latestAccelerations[acc.txid] = acc;
|
||||
this.myAccelerations[acc.txid] = { status: 'accelerating', added: Date.now(), acceleration: acc };
|
||||
}
|
||||
}
|
||||
// txs that are no longer accelerating are either confirmed or canceled, so mark for expiry
|
||||
for (const [txid, { status, acceleration }] of Object.entries(this.myAccelerations)) {
|
||||
if (status === 'accelerating' && !latestAccelerations[txid]) {
|
||||
this.myAccelerations[txid] = { status: 'done', added: Date.now(), acceleration };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clear expired accelerations (confirmed / failed / not accepted) after 10 minutes
|
||||
for (const [txid, { status, added }] of Object.entries(this.myAccelerations)) {
|
||||
if (['requested', 'done'].includes(status) && added < (Date.now() - (1000 * 60 * 10))) {
|
||||
delete this.myAccelerations[txid];
|
||||
}
|
||||
}
|
||||
|
||||
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
|
||||
return this._accelerations;
|
||||
}
|
||||
|
||||
public async $fetchAccelerationHistory(page?: number, status?: string): Promise<AccelerationHistory[] | null> {
|
||||
|
||||
@@ -103,7 +103,7 @@ class TransactionUtils {
|
||||
}
|
||||
const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4);
|
||||
const transactionExtended: TransactionExtended = Object.assign({
|
||||
vsize: transaction.weight / 4,
|
||||
vsize: Math.round(transaction.weight / 4),
|
||||
feePerVsize: feePerVbytes,
|
||||
effectiveFeePerVsize: feePerVbytes,
|
||||
}, transaction);
|
||||
@@ -123,7 +123,7 @@ class TransactionUtils {
|
||||
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
||||
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
||||
order: this.txidToOrdering(transaction.txid),
|
||||
vsize,
|
||||
vsize: Math.round(transaction.weight / 4),
|
||||
adjustedVsize,
|
||||
sigops,
|
||||
feePerVsize: feePerVbytes,
|
||||
|
||||
@@ -33,7 +33,7 @@ interface AddressTransactions {
|
||||
removed: MempoolTransactionExtended[],
|
||||
}
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import { calculateMempoolTxCpfp } from './cpfp';
|
||||
import { calculateCpfp } from './cpfp';
|
||||
|
||||
// valid 'want' subscriptions
|
||||
const wantable = [
|
||||
@@ -538,9 +538,9 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, true);
|
||||
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
} else {
|
||||
await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, true);
|
||||
await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
}
|
||||
|
||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
@@ -823,12 +823,11 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
},
|
||||
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
||||
};
|
||||
if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) {
|
||||
calculateMempoolTxCpfp(mempoolTx, newMempool);
|
||||
calculateCpfp(mempoolTx, newMempool);
|
||||
}
|
||||
if (mempoolTx.cpfpDirty) {
|
||||
positionData['cpfp'] = {
|
||||
@@ -865,10 +864,9 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
};
|
||||
if (!mempoolTx.cpfpChecked) {
|
||||
calculateMempoolTxCpfp(mempoolTx, newMempool);
|
||||
calculateCpfp(mempoolTx, newMempool);
|
||||
}
|
||||
if (mempoolTx.cpfpDirty) {
|
||||
txInfo.cpfp = {
|
||||
@@ -951,14 +949,18 @@ class WebsocketHandler {
|
||||
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||
let projectedBlocks;
|
||||
const auditMempool = _memPool;
|
||||
const isAccelerated = accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
|
||||
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
|
||||
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
const added = memPool.limitGBT ? (candidates?.added || []) : [];
|
||||
const removed = memPool.limitGBT ? (candidates?.removed || []) : [];
|
||||
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id);
|
||||
if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) {
|
||||
if (config.MEMPOOL.RUST_GBT) {
|
||||
const added = memPool.limitGBT ? (candidates?.added || []) : [];
|
||||
const removed = memPool.limitGBT ? (candidates?.removed || []) : [];
|
||||
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id);
|
||||
} else {
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id);
|
||||
}
|
||||
} else {
|
||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id);
|
||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
}
|
||||
|
||||
if (Common.indexingEnabled()) {
|
||||
@@ -1038,7 +1040,7 @@ class WebsocketHandler {
|
||||
const removed = memPool.limitGBT ? (candidates?.removed || []) : transactions;
|
||||
await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, _memPool, added, removed, candidates, true);
|
||||
} else {
|
||||
await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, true);
|
||||
await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||
}
|
||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||
@@ -1140,7 +1142,6 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
},
|
||||
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
||||
});
|
||||
@@ -1163,7 +1164,6 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ class Server {
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
||||
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
||||
const newAccelerations = await accelerationApi.$updateAccelerations();
|
||||
const newAccelerations = await accelerationApi.$fetchAccelerations();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||
if (numHandledBlocks === 0) {
|
||||
@@ -333,9 +333,7 @@ class Server {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
accelerationRoutes.initRoutes(this.app);
|
||||
}
|
||||
if (!config.MEMPOOL.OFFICIAL) {
|
||||
aboutRoutes.initRoutes(this.app);
|
||||
}
|
||||
aboutRoutes.initRoutes(this.app);
|
||||
}
|
||||
|
||||
healthCheck(): void {
|
||||
|
||||
@@ -42,19 +42,6 @@ export interface BlockAudit {
|
||||
matchRate: number,
|
||||
expectedFees?: number,
|
||||
expectedWeight?: number,
|
||||
template?: any[];
|
||||
}
|
||||
|
||||
export interface TransactionAudit {
|
||||
seen?: boolean;
|
||||
expected?: boolean;
|
||||
added?: boolean;
|
||||
prioritized?: boolean;
|
||||
delayed?: number;
|
||||
accelerated?: boolean;
|
||||
conflict?: boolean;
|
||||
coinbase?: boolean;
|
||||
firstSeen?: number;
|
||||
}
|
||||
|
||||
export interface AuditScore {
|
||||
@@ -126,7 +113,6 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
||||
acceleration?: boolean;
|
||||
acceleratedBy?: number[];
|
||||
acceleratedAt?: number;
|
||||
feeDelta?: number;
|
||||
replacement?: boolean;
|
||||
uid?: number;
|
||||
flags?: number;
|
||||
@@ -224,7 +210,6 @@ export interface CpfpInfo {
|
||||
sigops?: number;
|
||||
adjustedVsize?: number,
|
||||
acceleration?: boolean,
|
||||
fee?: number;
|
||||
}
|
||||
|
||||
export interface TransactionStripped {
|
||||
@@ -450,7 +435,7 @@ export interface OptimizedStatistic {
|
||||
|
||||
export interface TxTrackingInfo {
|
||||
replacedBy?: string,
|
||||
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number },
|
||||
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number },
|
||||
cpfp?: {
|
||||
ancestors?: Ancestor[],
|
||||
bestDescendant?: Ancestor | null,
|
||||
@@ -463,7 +448,6 @@ export interface TxTrackingInfo {
|
||||
accelerated?: boolean,
|
||||
acceleratedBy?: number[],
|
||||
acceleratedAt?: number,
|
||||
feeDelta?: number,
|
||||
confirmed?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccelerationInfo } from '../api/acceleration/acceleration';
|
||||
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration/acceleration';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
@@ -11,7 +11,6 @@ import accelerationCosts from '../api/acceleration/acceleration';
|
||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||
import transactionUtils from '../api/transaction-utils';
|
||||
import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces';
|
||||
import { makeBlockTemplate } from '../api/mini-miner';
|
||||
|
||||
export interface PublicAcceleration {
|
||||
txid: string,
|
||||
@@ -213,15 +212,6 @@ class AccelerationRepository {
|
||||
this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, successfulAccelerations);
|
||||
}
|
||||
}
|
||||
let anyConfirmed = false;
|
||||
for (const acc of accelerations) {
|
||||
if (blockTxs[acc.txid]) {
|
||||
anyConfirmed = true;
|
||||
}
|
||||
}
|
||||
if (anyConfirmed) {
|
||||
accelerationApi.accelerationConfirmed();
|
||||
}
|
||||
const lastSyncedHeight = await this.$getLastSyncedHeight();
|
||||
// if we've missed any blocks, let the indexer catch up from the last synced height on the next run
|
||||
if (block.height === lastSyncedHeight + 1) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import blocks from '../api/blocks';
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces';
|
||||
import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
||||
|
||||
class BlocksAuditRepositories {
|
||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||
@@ -98,41 +98,6 @@ class BlocksAuditRepositories {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
|
||||
try {
|
||||
const blockAudit = await this.$getBlockAudit(hash);
|
||||
|
||||
if (blockAudit) {
|
||||
const isAdded = blockAudit.addedTxs.includes(txid);
|
||||
const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
|
||||
const isAccelerated = blockAudit.acceleratedTxs.includes(txid);
|
||||
const isConflict = blockAudit.fullrbfTxs.includes(txid);
|
||||
let isExpected = false;
|
||||
let firstSeen = undefined;
|
||||
blockAudit.template?.forEach(tx => {
|
||||
if (tx.txid === txid) {
|
||||
isExpected = true;
|
||||
firstSeen = tx.time;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
expected: isExpected,
|
||||
added: isAdded,
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
firstSeen,
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot fetch block transaction audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(
|
||||
|
||||
@@ -114,43 +114,6 @@ class BlocksSummariesRepository {
|
||||
return [];
|
||||
}
|
||||
|
||||
public async $getSummariesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`
|
||||
SELECT
|
||||
height,
|
||||
id,
|
||||
version
|
||||
FROM blocks_summaries
|
||||
WHERE version < ?
|
||||
ORDER BY height DESC;`, [version]);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err(`Cannot get block summaries below version. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public async $getTemplatesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`
|
||||
SELECT
|
||||
blocks_summaries.height as height,
|
||||
blocks_templates.id as id,
|
||||
blocks_templates.version as version
|
||||
FROM blocks_templates
|
||||
JOIN blocks_summaries ON blocks_templates.id = blocks_summaries.id
|
||||
WHERE blocks_templates.version < ?
|
||||
ORDER BY height DESC;`, [version]);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err(`Cannot get block summaries below version. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fee percentiles if the block has already been indexed, [] otherwise
|
||||
*
|
||||
|
||||
@@ -91,26 +91,6 @@ class CpfpRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
public async $getClustersAt(height: number): Promise<CpfpCluster[]> {
|
||||
const [clusterRows]: any = await DB.query(
|
||||
`
|
||||
SELECT *
|
||||
FROM compact_cpfp_clusters
|
||||
WHERE height = ?
|
||||
`,
|
||||
[height]
|
||||
);
|
||||
return clusterRows.map(cluster => {
|
||||
if (cluster?.txs) {
|
||||
cluster.effectiveFeePerVsize = cluster.fee_rate;
|
||||
cluster.txs = this.unpack(cluster.txs);
|
||||
return cluster;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).filter(cluster => cluster !== null);
|
||||
}
|
||||
|
||||
public async $deleteClustersFrom(height: number): Promise<void> {
|
||||
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
|
||||
try {
|
||||
@@ -142,37 +122,6 @@ class CpfpRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async $deleteClustersAt(height: number): Promise<void> {
|
||||
logger.info(`Delete cpfp clusters at height ${height} from the database`);
|
||||
try {
|
||||
const [rows] = await DB.query(
|
||||
`
|
||||
SELECT txs, height, root from compact_cpfp_clusters
|
||||
WHERE height = ?
|
||||
`,
|
||||
[height]
|
||||
) as RowDataPacket[][];
|
||||
if (rows?.length) {
|
||||
for (const clusterToDelete of rows) {
|
||||
const txs = this.unpack(clusterToDelete?.txs);
|
||||
for (const tx of txs) {
|
||||
await transactionRepository.$removeTransaction(tx.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
await DB.query(
|
||||
`
|
||||
DELETE from compact_cpfp_clusters
|
||||
WHERE height = ?
|
||||
`,
|
||||
[height]
|
||||
);
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot delete cpfp clusters from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// insert a dummy row to mark that we've indexed as far as this block
|
||||
public async $insertProgressMarker(height: number): Promise<void> {
|
||||
try {
|
||||
@@ -241,32 +190,6 @@ class CpfpRepository {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// returns `true` if two sets of CPFP clusters are deeply identical
|
||||
public compareClusters(clustersA: CpfpCluster[], clustersB: CpfpCluster[]): boolean {
|
||||
if (clustersA.length !== clustersB.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
clustersA = clustersA.sort((a,b) => a.root.localeCompare(b.root));
|
||||
clustersB = clustersB.sort((a,b) => a.root.localeCompare(b.root));
|
||||
|
||||
for (let i = 0; i < clustersA.length; i++) {
|
||||
if (clustersA[i].root !== clustersB[i].root) {
|
||||
return false;
|
||||
}
|
||||
if (clustersA[i].txs.length !== clustersB[i].txs.length) {
|
||||
return false;
|
||||
}
|
||||
for (let j = 0; j < clustersA[i].txs.length; j++) {
|
||||
if (clustersA[i].txs[j].txid !== clustersB[i].txs[j].txid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new CpfpRepository();
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of May 21, 2024.
|
||||
|
||||
Signed: hans-crypto
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 12, 2024.
|
||||
|
||||
Signed: jlopp
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 9, 2024.
|
||||
|
||||
Signed: svrgnty
|
||||
@@ -144,12 +144,12 @@ __REPLICATION_STATISTICS_START_TIME__=${REPLICATION_STATISTICS_START_TIME:=14819
|
||||
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
||||
|
||||
# MEMPOOL_SERVICES
|
||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
|
||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
|
||||
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||
|
||||
# REDIS
|
||||
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
|
||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
|
||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
|
||||
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
|
||||
|
||||
# FIAT_PRICE
|
||||
|
||||
@@ -40,8 +40,6 @@ __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__ACCELERATOR__=${ACCELERATOR:=false}
|
||||
__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true}
|
||||
__SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
|
||||
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
|
||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
||||
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
||||
@@ -71,8 +69,6 @@ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __ACCELERATOR__
|
||||
export __ACCELERATOR_BUTTON__
|
||||
export __SERVICES_API__
|
||||
export __PUBLIC_ACCELERATIONS__
|
||||
export __HISTORICAL_PRICE__
|
||||
export __ADDITIONAL_CURRENCIES__
|
||||
|
||||
@@ -25,7 +25,5 @@
|
||||
"HISTORICAL_PRICE": true,
|
||||
"ADDITIONAL_CURRENCIES": false,
|
||||
"ACCELERATOR": false,
|
||||
"ACCELERATOR_BUTTON": true,
|
||||
"PUBLIC_ACCELERATIONS": false,
|
||||
"SERVICES_API": "https://mempool.space/api/v1/services"
|
||||
"PUBLIC_ACCELERATIONS": false
|
||||
}
|
||||
|
||||
53
frontend/package-lock.json
generated
53
frontend/package-lock.json
generated
@@ -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.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "~6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "~6.6.0",
|
||||
"@fortawesome/fontawesome-common-types": "~6.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "~6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "~6.5.1",
|
||||
"@mempool/mempool.js": "2.3.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
|
||||
"@types/qrcode": "~1.5.0",
|
||||
@@ -3669,30 +3669,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"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==",
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
|
||||
"integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
|
||||
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
|
||||
"integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
"@fortawesome/fontawesome-common-types": "6.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"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==",
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
|
||||
"integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
"@fortawesome/fontawesome-common-types": "6.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -20794,24 +20797,24 @@
|
||||
}
|
||||
},
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"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=="
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
|
||||
"integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A=="
|
||||
},
|
||||
"@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
|
||||
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
|
||||
"integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
"@fortawesome/fontawesome-common-types": "6.5.1"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-solid-svg-icons": {
|
||||
"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==",
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
|
||||
"integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
"@fortawesome/fontawesome-common-types": "6.5.1"
|
||||
}
|
||||
},
|
||||
"@goto-bus-stop/common-shake": {
|
||||
|
||||
@@ -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.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "~6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "~6.6.0",
|
||||
"@fortawesome/fontawesome-common-types": "~6.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "~6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "~6.5.1",
|
||||
"@mempool/mempool.js": "2.3.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
|
||||
"@types/qrcode": "~1.5.0",
|
||||
|
||||
@@ -9,7 +9,6 @@ import { StatusViewComponent } from './components/status-view/status-view.compon
|
||||
import { AddressGroupComponent } from './components/address-group/address-group.component';
|
||||
import { TrackerComponent } from './components/tracker/tracker.component';
|
||||
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
|
||||
import { TrackerGuard } from './route-guards';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
@@ -141,17 +140,16 @@ let routes: Routes = [
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
canMatch: [TrackerGuard],
|
||||
runGuardsAndResolvers: 'always',
|
||||
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'tracker',
|
||||
data: { networkSpecific: true },
|
||||
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
children: [],
|
||||
@@ -215,6 +213,10 @@ let routes: Routes = [
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
},
|
||||
];
|
||||
|
||||
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
@@ -299,16 +301,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (!window['isMempoolSpaceBuild']) {
|
||||
routes.push({
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
initialNavigation: 'enabledBlocking',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@if (accelerateError) {
|
||||
<div class="row mb-1 text-center">
|
||||
<div class="col-sm">
|
||||
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
|
||||
<h1 style="font-size: larger;">Sorry, something went wrong!</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center mt-1">
|
||||
@@ -389,30 +389,16 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
||||
@if (canPayWithCashapp) {
|
||||
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
|
||||
<p class="text-nowrap">—<span i18n="or">OR</span>—</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
||||
@if (canPayWithCashapp) {
|
||||
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
||||
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <app-fiat [value]="cost"></app-fiat> with</p>
|
||||
@if (canPayWithCashapp) {
|
||||
<img class="paymentMethod mx-2" style="width: 200px" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
|
||||
}
|
||||
@if (canPayWithApplePay) {
|
||||
@if (canPayWithCashapp) { <span class="mt-1 mb-1"></span> }
|
||||
<div class="paymentMethod mx-2" style="width: 200px; height: 55px">
|
||||
<img src="/resources/apple-pay.png" height=37 (click)="moveToStep('applepay')">
|
||||
</div>
|
||||
}
|
||||
@if (canPayWithGooglePay) {
|
||||
@if (canPayWithCashapp || canPayWithApplePay) { <span class="mt-1 mb-1"></span> }
|
||||
<div class="paymentMethod mx-2" style="width: 200px; height: 55px">
|
||||
<img src="/resources/google-pay.png" height=37 (click)="moveToStep('googlepay')">
|
||||
</div>
|
||||
}
|
||||
<img class="paymentMethod mx-2" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -435,9 +421,9 @@
|
||||
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
|
||||
} @else if (step === 'cashapp') {
|
||||
<!-- Show checkout page -->
|
||||
<div class="row mb-md-1 text-center" id="confirm-title">
|
||||
<div class="row mb-md-1 text-center">
|
||||
<div class="col-sm" id="confirm-payment-title">
|
||||
<h1 style="font-size: larger;"><ng-content select="[slot='checkout-title']"></ng-content><span class="default-slot" i18n="accelerator.confirm-your-payment">Confirm your payment</span></h1>
|
||||
</div>
|
||||
@@ -451,7 +437,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
|
||||
@if (!loadingCashapp) {
|
||||
<div class="row text-center mt-1">
|
||||
<div class="col-sm">
|
||||
<div class="form-group w-100">
|
||||
@@ -470,14 +456,8 @@
|
||||
<div class="row text-center mt-1">
|
||||
<div class="col-sm">
|
||||
<div class="form-group w-100">
|
||||
@if (step === 'applepay') {
|
||||
<div id="apple-pay-button" class="apple-pay-button apple-pay-button-black" style="height: 50px" [style]="loadingApplePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
} @else if (step === 'cashapp') {
|
||||
<div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
} @else if (step === 'googlepay') {
|
||||
<div id="google-pay-button" class="d-inline-block" style="height: 50px" [style]="loadingGooglePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
}
|
||||
@if (loadingCashapp || loadingApplePay || loadingGooglePay) {
|
||||
<div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||
@if (loadingCashapp) {
|
||||
<div display="d-flex flex-row justify-content-center">
|
||||
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
|
||||
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||
@@ -569,18 +549,25 @@
|
||||
<button type="button" *ngIf="advancedEnabled" class="btn btn-sm btn-outline-info btn-small-height ml-2" (click)="moveToStep('quote')" i18n="accelerator.customize">customize</button>
|
||||
</ng-template>
|
||||
|
||||
<ng-template id="accelerate-to" #accelerateTo let-x i18n="accelerator.accelerate-to-x">Accelerate to ~{{ x | number : '1.0-0' }} sat/vB</ng-template>
|
||||
<ng-template #accelerateTo let-x i18n="accelerator.accelerate-to-x">Accelerate to ~{{ x | number : '1.0-0' }} sat/vB</ng-template>
|
||||
|
||||
<ng-template #accelerateButton>
|
||||
<div class="position-relative">
|
||||
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" [class.disabled]="!canPay || quoteError || cantPayReason || calculating || (!advancedEnabled && selectedOption !== 'accel')" style="width: 200px" (click)="moveToStep('checkout')">
|
||||
@if (!couldPay && !quoteError && !(estimate?.availablePaymentMethods.bitcoin || estimate?.availablePaymentMethods.balance)) {
|
||||
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center disabled" style="width: 200px">
|
||||
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
|
||||
<span i18n="transaction.accelerate|Accelerate button label">Accelerate</span>
|
||||
<span>Coming soon</span>
|
||||
</button>
|
||||
@if (quoteError || cantPayReason) {
|
||||
<div class="btn-error-wrapper"><span class="btn-error"><app-mempool-error [error]="quoteError || cantPayReason" [textOnly]="true" alertClass=""></app-mempool-error></span></div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="position-relative">
|
||||
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" [class.disabled]="!canPay || quoteError || cantPayReason || calculating || (!advancedEnabled && selectedOption !== 'accel')" style="width: 200px" (click)="moveToStep('checkout')">
|
||||
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
|
||||
<span i18n="transaction.accelerate|Accelerate button label">Accelerate</span>
|
||||
</button>
|
||||
@if (quoteError || cantPayReason) {
|
||||
<div class="btn-error-wrapper"><span class="btn-error"><app-mempool-error [error]="quoteError || cantPayReason" [textOnly]="true" alertClass=""></app-mempool-error></span></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #accountPayButton>
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
.paymentMethod {
|
||||
padding: 10px;
|
||||
background-color: var(--secondary);
|
||||
border-radius: 10px;
|
||||
border-radius: 15px;
|
||||
border: 2px solid var(--bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -201,19 +202,4 @@
|
||||
|
||||
.btn-error-wrapper {
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.apple-pay-button {
|
||||
display: inline-block;
|
||||
-webkit-appearance: -apple-pay-button;
|
||||
-apple-pay-button-type: plain; /* Use any supported button type. */
|
||||
}
|
||||
.apple-pay-button-black {
|
||||
-apple-pay-button-style: black;
|
||||
}
|
||||
.apple-pay-button-white {
|
||||
-apple-pay-button-style: white;
|
||||
}
|
||||
.apple-pay-button-white-with-line {
|
||||
-apple-pay-button-style: white-outline;
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
/* eslint-disable no-console */
|
||||
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
|
||||
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { md5, insecureRandomUUID } from '../../shared/common.utils';
|
||||
import { nextRoundNumber } from '../../shared/common.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ETA, EtaService } from '../../services/eta.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { MiningStats } from '../../services/mining.service';
|
||||
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
|
||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp';
|
||||
|
||||
export type AccelerationEstimate = {
|
||||
hasAccess: boolean;
|
||||
@@ -25,7 +22,7 @@ export type AccelerationEstimate = {
|
||||
mempoolBaseFee: number;
|
||||
vsizeFee: number;
|
||||
pools: number[];
|
||||
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
|
||||
availablePaymentMethods: {[method: string]: {min: number, max: number}};
|
||||
unavailable?: boolean;
|
||||
options: { // recommended bid options
|
||||
fee: number; // recommended userBid in sats
|
||||
@@ -48,7 +45,7 @@ export const MIN_BID_RATIO = 1;
|
||||
export const DEFAULT_BID_RATIO = 2;
|
||||
export const MAX_BID_RATIO = 4;
|
||||
|
||||
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success';
|
||||
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'processing' | 'paid' | 'success';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerate-checkout',
|
||||
@@ -62,8 +59,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
@Input() eta: ETA;
|
||||
@Input() scrollEvent: boolean;
|
||||
@Input() cashappEnabled: boolean = true;
|
||||
@Input() applePayEnabled: boolean = false;
|
||||
@Input() googlePayEnabled: boolean = true;
|
||||
@Input() advancedEnabled: boolean = false;
|
||||
@Input() forceMobile: boolean = false;
|
||||
@Input() showDetails: boolean = false;
|
||||
@@ -85,12 +80,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
|
||||
private _step: CheckoutStep = 'summary';
|
||||
simpleMode: boolean = true;
|
||||
paymentMethod: 'cashapp' | 'btcpay';
|
||||
timeoutTimer: any;
|
||||
|
||||
authSubscription$: Subscription;
|
||||
auth: IAuth | null = null;
|
||||
|
||||
// accelerator stuff
|
||||
square: { appId: string, locationId: string};
|
||||
accelerationUUID: string;
|
||||
accelerationSubscription: Subscription;
|
||||
difficultySubscription: Subscription;
|
||||
@@ -111,39 +108,30 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
|
||||
// square
|
||||
loadingCashapp = false;
|
||||
loadingApplePay = false;
|
||||
loadingGooglePay = false;
|
||||
cashappError = false;
|
||||
cashappSubmit: any;
|
||||
payments: any;
|
||||
cashAppPay: any;
|
||||
applePay: any;
|
||||
googlePay: any;
|
||||
cashAppSubscription: Subscription;
|
||||
conversionsSubscription: Subscription;
|
||||
conversions: Record<string, number>;
|
||||
|
||||
conversions: any;
|
||||
|
||||
// btcpay
|
||||
loadingBtcpayInvoice = false;
|
||||
invoice = undefined;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private etaService: EtaService,
|
||||
private audioService: AudioService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private authService: AuthServiceMempool,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private authService: AuthServiceMempool
|
||||
) {
|
||||
this.accelerationUUID = insecureRandomUUID();
|
||||
|
||||
// Check if Apple Pay available
|
||||
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
|
||||
if (window['ApplePaySession']) {
|
||||
this.applePayEnabled = true;
|
||||
}
|
||||
this.accelerationUUID = window.crypto.randomUUID();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
ngOnInit() {
|
||||
this.authSubscription$ = this.authService.getAuth$().subscribe((auth) => {
|
||||
if (this.auth?.user?.userId !== auth?.user?.userId) {
|
||||
this.auth = auth;
|
||||
@@ -168,6 +156,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.moveToStep('summary');
|
||||
}
|
||||
|
||||
this.servicesApiService.setupSquare$().subscribe(ids => {
|
||||
this.square = {
|
||||
appId: ids.squareAppId,
|
||||
locationId: ids.squareLocationId
|
||||
};
|
||||
});
|
||||
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
@@ -175,7 +170,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
ngOnDestroy() {
|
||||
if (this.estimateSubscription) {
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
@@ -195,7 +190,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
moveToStep(step: CheckoutStep): void {
|
||||
moveToStep(step: CheckoutStep) {
|
||||
this._step = step;
|
||||
if (this.timeoutTimer) {
|
||||
clearTimeout(this.timeoutTimer);
|
||||
@@ -203,9 +198,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) {
|
||||
this.fetchEstimate();
|
||||
}
|
||||
if (this._step === 'checkout') {
|
||||
this.enterpriseService.goal(8);
|
||||
}
|
||||
if (this._step === 'checkout' && this.canPayWithBitcoin) {
|
||||
this.btcpayInvoiceFailed = false;
|
||||
this.loadingBtcpayInvoice = true;
|
||||
@@ -215,24 +207,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.loadingCashapp = true;
|
||||
this.insertSquare();
|
||||
this.setupSquare();
|
||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'applepay' && this.applePayEnabled) {
|
||||
this.loadingApplePay = true;
|
||||
this.insertSquare();
|
||||
this.setupSquare();
|
||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'googlepay' && this.googlePayEnabled) {
|
||||
this.loadingGooglePay = true;
|
||||
this.insertSquare();
|
||||
this.setupSquare();
|
||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'paid') {
|
||||
this.timePaid = Date.now();
|
||||
this.timeoutTimer = setTimeout(() => {
|
||||
if (this.step === 'paid') {
|
||||
this.accelerateError = 'internal_server_error';
|
||||
}
|
||||
}, 120000);
|
||||
}, 120000)
|
||||
}
|
||||
this.hasDetails.emit(this._step === 'quote');
|
||||
}
|
||||
@@ -243,14 +224,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to element id with or without setTimeout
|
||||
*/
|
||||
* Scroll to element id with or without setTimeout
|
||||
*/
|
||||
scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void {
|
||||
setTimeout(() => {
|
||||
this.scrollToElement(id, position);
|
||||
}, timeout);
|
||||
}
|
||||
scrollToElement(id: string, position: ScrollLogicalPosition): void {
|
||||
scrollToElement(id: string, position: ScrollLogicalPosition) {
|
||||
const acceleratePreviewAnchor = document.getElementById(id);
|
||||
if (acceleratePreviewAnchor) {
|
||||
this.cd.markForCheck();
|
||||
@@ -265,7 +246,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Accelerator
|
||||
*/
|
||||
fetchEstimate(): void {
|
||||
fetchEstimate() {
|
||||
if (this.estimateSubscription) {
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
@@ -311,14 +292,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
|
||||
this.validateChoice();
|
||||
|
||||
if (!this.couldPay) {
|
||||
this.quoteError = `cannot_accelerate_tx`;
|
||||
if (this.step === 'summary') {
|
||||
this.unavailable.emit(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) {
|
||||
this.loadingBtcpayInvoice = true;
|
||||
this.requestBTCPayInvoice();
|
||||
@@ -329,7 +302,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
}),
|
||||
|
||||
catchError(() => {
|
||||
catchError((response) => {
|
||||
this.estimate = undefined;
|
||||
this.quoteError = `cannot_accelerate_tx`;
|
||||
this.estimateSubscription.unsubscribe();
|
||||
@@ -384,11 +357,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
this.showSuccess = true;
|
||||
this.estimateSubscription.unsubscribe();
|
||||
this.moveToStep('paid');
|
||||
this.moveToStep('paid')
|
||||
},
|
||||
error: (response) => {
|
||||
this.accelerateError = response.error;
|
||||
@@ -400,7 +372,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
* Square
|
||||
*/
|
||||
insertSquare(): void {
|
||||
if (window['Square']) {
|
||||
//@ts-ignore
|
||||
if (window.Square) {
|
||||
return;
|
||||
}
|
||||
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
|
||||
@@ -412,17 +385,19 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
statsUrl = 'https://web.squarecdn.com/v1/square.js';
|
||||
}
|
||||
|
||||
(function(): void {
|
||||
(function() {
|
||||
const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
// @ts-ignore
|
||||
g.type='text/javascript'; g.src=statsUrl; s.parentNode.insertBefore(g, s);
|
||||
})();
|
||||
}
|
||||
setupSquare(): void {
|
||||
const init = (): void => {
|
||||
setupSquare() {
|
||||
const init = () => {
|
||||
this.initSquare();
|
||||
};
|
||||
|
||||
if (!window['Square']) {
|
||||
//@ts-ignore
|
||||
if (!window.Square) {
|
||||
console.debug('Square.js failed to load properly. Retrying in 1 second.');
|
||||
setTimeout(init, 1000);
|
||||
} else {
|
||||
@@ -431,215 +406,23 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
async initSquare(): Promise<void> {
|
||||
try {
|
||||
this.servicesApiService.setupSquare$().subscribe({
|
||||
next: async (ids) => {
|
||||
this.payments = window['Square'].payments(ids.squareAppId, ids.squareLocationId);
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (this._step === 'cashapp' || urlParams.get('cash_request_id')) {
|
||||
await this.requestCashAppPayment();
|
||||
} else if (this._step === 'applepay') {
|
||||
await this.requestApplePayPayment();
|
||||
} else if (this._step === 'googlepay') {
|
||||
await this.requestGooglePayPayment();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
console.debug('Error loading Square Payments');
|
||||
this.accelerateError = 'cannot_setup_square';
|
||||
}
|
||||
});
|
||||
//@ts-ignore
|
||||
this.payments = window.Square.payments(this.square.appId, this.square.locationId)
|
||||
await this.requestCashAppPayment();
|
||||
} catch (e) {
|
||||
console.debug('Error loading Square Payments', e);
|
||||
this.accelerateError = 'cannot_setup_square';
|
||||
this.cashappError = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* APPLE PAY
|
||||
*/
|
||||
async requestApplePayPayment(): Promise<void> {
|
||||
async requestCashAppPayment() {
|
||||
if (this.cashAppSubscription) {
|
||||
this.cashAppSubscription.unsubscribe();
|
||||
}
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
if (this.applePay) {
|
||||
this.applePay.destroy();
|
||||
}
|
||||
|
||||
const costUSD = this.cost / 100_000_000 * conversions.USD;
|
||||
const paymentRequest = this.payments.paymentRequest({
|
||||
countryCode: 'US',
|
||||
currencyCode: 'USD',
|
||||
total: {
|
||||
amount: costUSD.toFixed(2),
|
||||
label: 'Total',
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
this.applePay = await this.payments.applePay(paymentRequest);
|
||||
const applePayButton = document.getElementById('apple-pay-button');
|
||||
if (!applePayButton) {
|
||||
console.error(`Unable to find apple pay button id='apple-pay-button'`);
|
||||
// Try again
|
||||
setTimeout(this.requestApplePayPayment.bind(this), 500);
|
||||
return;
|
||||
}
|
||||
this.loadingApplePay = false;
|
||||
applePayButton.addEventListener('click', async event => {
|
||||
event.preventDefault();
|
||||
const tokenResult = await this.applePay.tokenize();
|
||||
if (tokenResult?.status === 'OK') {
|
||||
const card = tokenResult.details?.card;
|
||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||
console.error(`Cannot retreive payment card details`);
|
||||
this.accelerateError = 'apple_pay_no_card_details';
|
||||
return;
|
||||
}
|
||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||
this.servicesApiService.accelerateWithApplePay$(
|
||||
this.tx.txid,
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.applePay) {
|
||||
this.applePay.destroy();
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.moveToStep('paid');
|
||||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
// Reset everything by reloading the page :D, can be improved
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||
if (tokenResult.errors) {
|
||||
errorMessage += ` and errors: ${JSON.stringify(
|
||||
tokenResult.errors,
|
||||
)}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GOOGLE PAY
|
||||
*/
|
||||
async requestGooglePayPayment(): Promise<void> {
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
if (this.googlePay) {
|
||||
this.googlePay.destroy();
|
||||
}
|
||||
|
||||
const costUSD = this.cost / 100_000_000 * conversions.USD;
|
||||
const paymentRequest = this.payments.paymentRequest({
|
||||
countryCode: 'US',
|
||||
currencyCode: 'USD',
|
||||
total: {
|
||||
amount: costUSD.toFixed(2),
|
||||
label: 'Total'
|
||||
}
|
||||
});
|
||||
this.googlePay = await this.payments.googlePay(paymentRequest , {
|
||||
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
});
|
||||
|
||||
await this.googlePay.attach(`#google-pay-button`, {
|
||||
buttonType: 'pay',
|
||||
buttonSizeMode: 'fill',
|
||||
});
|
||||
this.loadingGooglePay = false;
|
||||
|
||||
document.getElementById('google-pay-button').addEventListener('click', async event => {
|
||||
event.preventDefault();
|
||||
const tokenResult = await this.googlePay.tokenize();
|
||||
if (tokenResult?.status === 'OK') {
|
||||
const card = tokenResult.details?.card;
|
||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||
console.error(`Cannot retreive payment card details`);
|
||||
this.accelerateError = 'apple_pay_no_card_details';
|
||||
return;
|
||||
}
|
||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||
this.servicesApiService.accelerateWithGooglePay$(
|
||||
this.tx.txid,
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.googlePay) {
|
||||
this.googlePay.destroy();
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.moveToStep('paid');
|
||||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
// Reset everything by reloading the page :D, can be improved
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||
if (tokenResult.errors) {
|
||||
errorMessage += ` and errors: ${JSON.stringify(
|
||||
tokenResult.errors,
|
||||
)}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* CASHAPP
|
||||
*/
|
||||
async requestCashAppPayment(): Promise<void> {
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
@@ -653,37 +436,41 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
countryCode: 'US',
|
||||
currencyCode: 'USD',
|
||||
total: {
|
||||
amount: costUSD.toFixed(2),
|
||||
amount: costUSD.toString(),
|
||||
label: 'Total',
|
||||
pending: true,
|
||||
productUrl: `${redirectHostname}/tracker/${this.tx.txid}`,
|
||||
}
|
||||
},
|
||||
button: { shape: 'semiround', size: 'small', theme: 'light'}
|
||||
});
|
||||
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
|
||||
redirectURL: `${redirectHostname}/tracker/${this.tx.txid}`,
|
||||
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`
|
||||
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
button: { shape: 'semiround', size: 'small', theme: 'light'}
|
||||
});
|
||||
|
||||
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'dark' });
|
||||
if (this.step === 'cashapp') {
|
||||
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' })
|
||||
}
|
||||
this.loadingCashapp = false;
|
||||
|
||||
this.cashAppPay.addEventListener('ontokenization', event => {
|
||||
const that = this;
|
||||
this.cashAppPay.addEventListener('ontokenization', function (event) {
|
||||
const { tokenResult, error } = event.detail;
|
||||
if (error) {
|
||||
this.accelerateError = error;
|
||||
} else if (tokenResult.status === 'OK') {
|
||||
this.servicesApiService.accelerateWithCashApp$(
|
||||
this.tx.txid,
|
||||
that.servicesApiService.accelerateWithCashApp$(
|
||||
that.tx.txid,
|
||||
tokenResult.token,
|
||||
tokenResult.details.cashAppPay.cashtag,
|
||||
tokenResult.details.cashAppPay.referenceId,
|
||||
this.accelerationUUID
|
||||
that.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.cashAppPay) {
|
||||
this.cashAppPay.destroy();
|
||||
that.audioService.playSound('ascend-chime-cartoon');
|
||||
if (that.cashAppPay) {
|
||||
that.cashAppPay.destroy();
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.moveToStep('paid');
|
||||
@@ -694,7 +481,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.accelerateError = response.error;
|
||||
that.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
// Reset everything by reloading the page :D, can be improved
|
||||
@@ -713,7 +500,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
/**
|
||||
* BTCPay
|
||||
*/
|
||||
async requestBTCPayInvoice(): Promise<void> {
|
||||
async requestBTCPayInvoice() {
|
||||
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
|
||||
switchMap(response => {
|
||||
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
|
||||
@@ -730,10 +517,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
bitcoinPaymentCompleted(): void {
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
this.estimateSubscription.unsubscribe();
|
||||
this.moveToStep('paid');
|
||||
this.moveToStep('paid')
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
@@ -743,61 +529,47 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
/**
|
||||
* UI events
|
||||
*/
|
||||
selectedOptionChanged(event): void {
|
||||
selectedOptionChanged(event) {
|
||||
this.selectedOption = event.target.id;
|
||||
}
|
||||
|
||||
get step(): CheckoutStep {
|
||||
get step() {
|
||||
return this._step;
|
||||
}
|
||||
|
||||
get paymentMethods(): PaymentMethod[] {
|
||||
return Object.keys(this.estimate?.availablePaymentMethods || {}) as PaymentMethod[];
|
||||
get paymentMethods() {
|
||||
return Object.keys(this.estimate?.availablePaymentMethods || {});
|
||||
}
|
||||
|
||||
get couldPayWithBitcoin(): boolean {
|
||||
get couldPayWithBitcoin() {
|
||||
return !!this.estimate?.availablePaymentMethods?.bitcoin;
|
||||
}
|
||||
|
||||
get couldPayWithCashapp(): boolean {
|
||||
if (!this.cashappEnabled) {
|
||||
get couldPayWithCashapp() {
|
||||
if (!this.cashappEnabled || this.stateService.referrer !== 'https://cash.app/') {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.cashapp;
|
||||
}
|
||||
|
||||
get couldPayWithApplePay(): boolean {
|
||||
if (!this.applePayEnabled) {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.applePay;
|
||||
}
|
||||
|
||||
get couldPayWithGooglePay(): boolean {
|
||||
if (!this.googlePayEnabled) {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.googlePay;
|
||||
}
|
||||
|
||||
get couldPayWithBalance(): boolean {
|
||||
get couldPayWithBalance() {
|
||||
if (!this.hasAccessToBalanceMode) {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.balance;
|
||||
}
|
||||
|
||||
get couldPay(): boolean {
|
||||
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay || this.couldPayWithGooglePay;
|
||||
get couldPay() {
|
||||
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp;
|
||||
}
|
||||
|
||||
get canPayWithBitcoin(): boolean {
|
||||
get canPayWithBitcoin() {
|
||||
const paymentMethod = this.estimate?.availablePaymentMethods?.bitcoin;
|
||||
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max;
|
||||
}
|
||||
|
||||
get canPayWithCashapp(): boolean {
|
||||
if (!this.cashappEnabled || !this.conversions) {
|
||||
get canPayWithCashapp() {
|
||||
if (!this.cashappEnabled || !this.conversions || this.stateService.referrer !== 'https://cash.app/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -808,43 +580,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get canPayWithApplePay(): boolean {
|
||||
if (!this.applePayEnabled || !this.conversions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const paymentMethod = this.estimate?.availablePaymentMethods?.applePay;
|
||||
if (paymentMethod) {
|
||||
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
|
||||
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get canPayWithGooglePay(): boolean {
|
||||
if (!this.googlePayEnabled || !this.conversions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const paymentMethod = this.estimate?.availablePaymentMethods?.googlePay;
|
||||
if (paymentMethod) {
|
||||
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
|
||||
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get canPayWithBalance(): boolean {
|
||||
get canPayWithBalance() {
|
||||
if (!this.hasAccessToBalanceMode) {
|
||||
return false;
|
||||
}
|
||||
@@ -852,11 +592,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max && this.cost <= this.estimate?.userBalance;
|
||||
}
|
||||
|
||||
get canPay(): boolean {
|
||||
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay || this.canPayWithGooglePay;
|
||||
get canPay() {
|
||||
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp;
|
||||
}
|
||||
|
||||
get hasAccessToBalanceMode(): boolean {
|
||||
get hasAccessToBalanceMode() {
|
||||
return this.isLoggedIn() && this.estimate?.hasAccess;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="fee-graph" *ngIf="tx && estimate" #feeGraph>
|
||||
<div class="fee-graph" *ngIf="tx && estimate">
|
||||
<div class="column">
|
||||
<ng-container *ngFor="let bar of bars">
|
||||
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
|
||||
import { Router } from '@angular/router';
|
||||
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
|
||||
import { tap, switchMap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
|
||||
|
||||
interface GraphBar {
|
||||
rate: number;
|
||||
style?: Record<string,string>;
|
||||
style: any;
|
||||
class: 'tx' | 'target' | 'max';
|
||||
label: string;
|
||||
active?: boolean;
|
||||
rateIndex?: number;
|
||||
fee?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -18,7 +22,7 @@ interface GraphBar {
|
||||
templateUrl: './accelerate-fee-graph.component.html',
|
||||
styleUrls: ['./accelerate-fee-graph.component.scss'],
|
||||
})
|
||||
export class AccelerateFeeGraphComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
|
||||
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
@Input() estimate: AccelerationEstimate;
|
||||
@Input() showEstimate = false;
|
||||
@@ -26,37 +30,13 @@ export class AccelerateFeeGraphComponent implements OnInit, AfterViewInit, OnCha
|
||||
@Input() maxRateIndex: number = 0;
|
||||
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
|
||||
|
||||
@ViewChild('feeGraph')
|
||||
container: ElementRef<HTMLDivElement>;
|
||||
height: number;
|
||||
observer: ResizeObserver;
|
||||
stopResizeLoop = false;
|
||||
|
||||
bars: GraphBar[] = [];
|
||||
tooltipPosition = { x: 0, y: 0 };
|
||||
|
||||
constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (ResizeObserver) {
|
||||
this.observer = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
this.height = entry.contentRect.height;
|
||||
this.initGraph();
|
||||
}
|
||||
});
|
||||
this.observer.observe(this.container.nativeElement);
|
||||
} else {
|
||||
this.startResizeFallbackLoop();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.initGraph();
|
||||
}
|
||||
@@ -65,61 +45,44 @@ export class AccelerateFeeGraphComponent implements OnInit, AfterViewInit, OnCha
|
||||
if (!this.tx || !this.estimate) {
|
||||
return;
|
||||
}
|
||||
const hasNextBlockRate = (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee);
|
||||
const numBars = hasNextBlockRate ? 4 : 3;
|
||||
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
|
||||
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
|
||||
let baseHeight = Math.max(this.height - (numBars * 30), this.height * (baseRate / maxRate));
|
||||
const bars: GraphBar[] = [];
|
||||
let lastHeight = 0;
|
||||
if (hasNextBlockRate) {
|
||||
lastHeight = Math.max(lastHeight + 30, (this.height * ((this.estimate.targetFeeRate - baseRate) / maxRate)));
|
||||
const baseHeight = baseRate / maxRate;
|
||||
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => {
|
||||
return {
|
||||
rate: option.rate,
|
||||
style: this.getStyle(option.rate, maxRate, baseHeight),
|
||||
class: 'max',
|
||||
label: this.showEstimate ? $localize`maximum` : $localize`:@@25fbf6e80a945703c906a5a7d8c92e8729c7ab21:accelerated`,
|
||||
active: option.index === this.maxRateIndex,
|
||||
rateIndex: option.index,
|
||||
fee: option.fee,
|
||||
}
|
||||
});
|
||||
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
|
||||
bars.push({
|
||||
rate: this.estimate.targetFeeRate,
|
||||
height: lastHeight,
|
||||
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||
class: 'target',
|
||||
label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(),
|
||||
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||
});
|
||||
}
|
||||
this.maxRateOptions.forEach((option, index) => {
|
||||
lastHeight = Math.max(lastHeight + 30, (this.height * ((option.rate - baseRate) / maxRate)));
|
||||
bars.push({
|
||||
rate: option.rate,
|
||||
height: lastHeight,
|
||||
class: 'max',
|
||||
label: this.showEstimate ? $localize`maximum` : $localize`accelerated`,
|
||||
active: option.index === this.maxRateIndex,
|
||||
rateIndex: option.index,
|
||||
fee: option.fee,
|
||||
})
|
||||
})
|
||||
|
||||
bars.reverse();
|
||||
|
||||
baseHeight = this.height - lastHeight;
|
||||
|
||||
for (const bar of bars) {
|
||||
bar.style = this.getStyle(bar.height, baseHeight);
|
||||
}
|
||||
|
||||
bars.push({
|
||||
rate: baseRate,
|
||||
style: this.getStyle(baseHeight, 0),
|
||||
height: baseHeight,
|
||||
style: this.getStyle(baseRate, maxRate, 0),
|
||||
class: 'tx',
|
||||
label: '',
|
||||
fee: this.estimate.txSummary.effectiveFee,
|
||||
});
|
||||
|
||||
this.bars = bars;
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
|
||||
getStyle(height: number, base: number): Record<string,string> {
|
||||
getStyle(rate, maxRate, base) {
|
||||
const top = (rate / maxRate);
|
||||
return {
|
||||
height: `${height}px`,
|
||||
bottom: base ? `${base}px` : '0',
|
||||
height: `${(top - base) * 100}%`,
|
||||
bottom: base ? `${base * 100}%` : '0',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,20 +96,4 @@ export class AccelerateFeeGraphComponent implements OnInit, AfterViewInit, OnCha
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
|
||||
}
|
||||
|
||||
startResizeFallbackLoop(): void {
|
||||
if (this.stopResizeLoop) {
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
this.height = this.container?.nativeElement?.clientHeight || 0;
|
||||
this.initGraph();
|
||||
this.startResizeFallbackLoop();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stopResizeLoop = true;
|
||||
this.observer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<div
|
||||
#tooltip
|
||||
*ngIf="accelerationInfo && tooltipPosition !== null"
|
||||
class="acceleration-tooltip"
|
||||
[style.left]="tooltipPosition.x + 'px'"
|
||||
[style.top]="tooltipPosition.y + 'px'"
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label" i18n="transaction.status|Transaction Status">Status</td>
|
||||
<td class="value">
|
||||
@if (accelerationInfo.status === 'seen') {
|
||||
<span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span>
|
||||
} @else if (accelerationInfo.status === 'accelerated') {
|
||||
<span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
|
||||
} @else if (accelerationInfo.status === 'mined') {
|
||||
<span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="accelerationInfo.fee">
|
||||
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
|
||||
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
|
||||
@if (accelerationInfo.status === 'accelerated') {
|
||||
<td style="color: #905cf4;" class="value">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
} @else {
|
||||
<td style="color: #905cf4;" class="value">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
||||
}
|
||||
</tr>
|
||||
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
|
||||
@if (accelerationInfo.status === 'seen') {
|
||||
<td class="label" i18n="transaction.fee-rate">Fee rate</td>
|
||||
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||
} @else if (accelerationInfo.status === 'accelerated' || accelerationInfo.status === 'mined') {
|
||||
<td class="label" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
|
||||
@if (accelerationInfo.status === 'accelerated') {
|
||||
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.feeDelta || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||
} @else {
|
||||
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.bidBoost || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||
}
|
||||
}
|
||||
</tr>
|
||||
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
|
||||
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
|
||||
<td class="value" *ngIf="accelerationInfo.pools">
|
||||
<ng-container *ngFor="let pool of accelerationInfo.pools">
|
||||
<img *ngIf="accelerationInfo.poolsData[pool]"
|
||||
class="pool-logo"
|
||||
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
|
||||
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'"
|
||||
[alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||
</ng-container>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,35 +0,0 @@
|
||||
.acceleration-tooltip {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
|
||||
color: var(--tooltip-grey);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 10px 15px;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
|
||||
.badge.badge-accelerated {
|
||||
background-color: var(--tertiary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-timeline-tooltip',
|
||||
templateUrl: './acceleration-timeline-tooltip.component.html',
|
||||
styleUrls: ['./acceleration-timeline-tooltip.component.scss'],
|
||||
})
|
||||
export class AccelerationTimelineTooltipComponent implements OnChanges {
|
||||
@Input() accelerationInfo: any;
|
||||
@Input() cursorPosition: { x: number, y: number };
|
||||
|
||||
tooltipPosition: any = null;
|
||||
|
||||
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
|
||||
let x = Math.max(10, changes.cursorPosition.currentValue.x - 50);
|
||||
let y = changes.cursorPosition.currentValue.y + 20;
|
||||
if (this.tooltipElement) {
|
||||
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
|
||||
if ((x + elementBounds.width) > (window.innerWidth - 10)) {
|
||||
x = Math.max(0, window.innerWidth - elementBounds.width - 10);
|
||||
}
|
||||
if (y + elementBounds.height > (window.innerHeight - 20)) {
|
||||
y = y - elementBounds.height - 20;
|
||||
}
|
||||
}
|
||||
this.tooltipPosition = { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
hasPoolsData(): boolean {
|
||||
return Object.keys(this.accelerationInfo.poolsData).length > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,6 @@
|
||||
<div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed">
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="acceleration-timeline box">
|
||||
<div class="timeline-wrapper">
|
||||
@if (!tx.status.confirmed) {
|
||||
<div class="timeline">
|
||||
<div class="intervals">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval-spacer"></div>
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
@if (eta) {
|
||||
~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> -->
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval-spacer"></div>
|
||||
<div class="node">
|
||||
<div class="acc-to-confirmed right go-faster"></div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
</div>
|
||||
<div class="node" [id]="'confirmed'">
|
||||
<div class="acc-to-confirmed left go-faster"></div>
|
||||
<div class="shape-border waiting">
|
||||
<div class="shape"></div>
|
||||
</div>
|
||||
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="timeline">
|
||||
<div class="intervals">
|
||||
<div class="node-spacer"></div>
|
||||
@@ -44,13 +12,7 @@
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="interval-time">
|
||||
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
|
||||
</div>
|
||||
} @else if (standardETA && !tx.status.confirmed) {
|
||||
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
|
||||
}
|
||||
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
@@ -58,85 +20,134 @@
|
||||
<div class="nodes">
|
||||
<div class="node" [id]="'first-seen'">
|
||||
<div class="seen-to-acc right"></div>
|
||||
<div class="shape-border hovering" (pointerover)="onHover($event, 'seen');" (pointerout)="onBlur($event);">
|
||||
<div class="shape-border">
|
||||
<div class="shape"></div>
|
||||
</div>
|
||||
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
|
||||
<div class="time">
|
||||
@if (useAbsoluteTime) {
|
||||
<span>{{ transactionTime * 1000 | date }}</span>
|
||||
} @else {
|
||||
<app-time kind="since" [time]="transactionTime"></app-time>
|
||||
}
|
||||
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="seen-to-acc"></div>
|
||||
</div>
|
||||
<div class="node" [class.accelerated]="!tx.status.confirmed" [id]="'accelerated'">
|
||||
<div class="node" [id]="'accelerated'">
|
||||
<div class="seen-to-acc left"></div>
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="acc-to-confirmed right"></div>
|
||||
} @else {
|
||||
<div class="seen-to-acc right"></div>
|
||||
}
|
||||
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
|
||||
<div class="acc-to-confirmed right"></div>
|
||||
<div class="shape-border">
|
||||
<div class="shape"></div>
|
||||
@if (!tx.status.confirmed) {
|
||||
<div class="connector down loading"></div>
|
||||
}
|
||||
</div>
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
|
||||
}
|
||||
<div class="time" [class.no-margin]="!tx.status.confirmed" [class.offset-left]="!tx.status.confirmed">
|
||||
@if (!tx.status.confirmed) {
|
||||
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
|
||||
}
|
||||
@if (useAbsoluteTime) {
|
||||
<span>{{ acceleratedAt * 1000 | date }}</span>
|
||||
} @else {
|
||||
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed"></app-time>
|
||||
}
|
||||
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
|
||||
<div class="time">
|
||||
<app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="acc-to-confirmed"></div>
|
||||
} @else {
|
||||
<div class="seen-to-acc"></div>
|
||||
}
|
||||
</div>
|
||||
<div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'">
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="acc-to-confirmed left"></div>
|
||||
} @else {
|
||||
<div class="seen-to-acc left"></div>
|
||||
}
|
||||
<div class="shape-border"
|
||||
[ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}"
|
||||
(pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)"
|
||||
(pointerout)="onBlur($event);">
|
||||
<div class="node selected" [id]="'confirmed'">
|
||||
<div class="acc-to-confirmed left" ></div>
|
||||
<div class="shape-border">
|
||||
<div class="shape"></div>
|
||||
</div>
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="status"><span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span></div>
|
||||
<div class="time">
|
||||
@if (useAbsoluteTime) {
|
||||
<span>{{ tx.status.block_time * 1000 | date }}</span>
|
||||
} @else {
|
||||
<app-time kind="since" [time]="tx.status.block_time"></app-time>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (acceleratedETA) { <!-- Not yet accelerated; to be shown only in acceleration checkout -->
|
||||
} @else if (standardETA) { <!-- Accelerated -->
|
||||
<div class="acceleration-timeline box">
|
||||
<div class="timeline-wrapper">
|
||||
<div class="timeline">
|
||||
<div class="intervals">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval-spacer"></div>
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
@if (eta) {
|
||||
~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> -->
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval-spacer"></div>
|
||||
<div class="node">
|
||||
<div class="acc-to-confirmed loading right"></div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="acc-to-confirmed loading"></div>
|
||||
</div>
|
||||
<div class="node" [id]="'confirmed'">
|
||||
<div class="acc-to-confirmed loading left"></div>
|
||||
<div class="shape-border waiting">
|
||||
<div class="shape animate"></div>
|
||||
</div>
|
||||
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline">
|
||||
<div class="intervals">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
<app-time [time]="acceleratedAt - transactionTime"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<div class="node" [id]="'first-seen'">
|
||||
<div class="seen-to-acc right"></div>
|
||||
<div class="shape-border">
|
||||
<div class="shape"></div>
|
||||
</div>
|
||||
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
|
||||
<div class="time">
|
||||
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="seen-to-acc"></div>
|
||||
</div>
|
||||
<div class="node accelerated" [id]="'accelerated'">
|
||||
<div class="seen-to-acc left"></div>
|
||||
<div class="seen-to-acc right"></div>
|
||||
<div class="shape-border">
|
||||
<div class="shape"></div>
|
||||
<div class="connector down loading"></div>
|
||||
</div>
|
||||
<div class="time" style="margin-top: 3px;">
|
||||
<span i18n="transaction.audit.accelerated">Accelerated</span> <app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="seen-to-acc"></div>
|
||||
</div>
|
||||
<div class="node" [id]="'confirmed'">
|
||||
<div class="seen-to-acc left"></div>
|
||||
<div class="shape-border waiting">
|
||||
<div class="shape"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-acceleration-timeline-tooltip
|
||||
[accelerationInfo]="hoverInfo"
|
||||
[cursorPosition]="tooltipPosition"
|
||||
></app-acceleration-timeline-tooltip>
|
||||
|
||||
</div>
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
.acceleration-timeline {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 1em 0;
|
||||
&.lower-padding {
|
||||
padding: 0.5em 0 1em;
|
||||
}
|
||||
padding: 0.5em 0 1em;
|
||||
|
||||
&::after, &::before {
|
||||
content: '';
|
||||
@@ -55,7 +52,7 @@
|
||||
|
||||
.interval, .interval-spacer {
|
||||
width: 8em;
|
||||
min-width: 8em;
|
||||
min-width: 5em;
|
||||
max-width: 8em;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
@@ -115,20 +112,8 @@
|
||||
background: var(--tertiary);
|
||||
border-radius: 5px;
|
||||
|
||||
&.go-faster {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Cpath style='fill:%239339f4;' d='M 0,0 5,5 0,10 Z'/%3E%3Cpath style='fill:%23653b9c;' d='M 0,0 10,0 15,5 10,10 0,10 5,5 Z'/%3E%3Cpath style='fill:%239339f4;' d='M 10,0 20,0 20,10 10,10 15,5 Z'/%3E%3C/svg%3E%0A"); background-size: 20px 10px;
|
||||
border-radius: 0;
|
||||
|
||||
&.right {
|
||||
left: calc(50% + 5px);
|
||||
margin-right: calc(-4em + 5px);
|
||||
animation: goFasterRight 0.8s infinite linear;
|
||||
}
|
||||
&.left {
|
||||
right: calc(50% + 5px);
|
||||
margin-left: calc(-4em + 5px);
|
||||
animation: goFasterLeft 0.8s infinite linear;
|
||||
}
|
||||
&.loading {
|
||||
animation: acceleratePulse 1s infinite;
|
||||
}
|
||||
|
||||
&.left {
|
||||
@@ -152,16 +137,10 @@
|
||||
margin-bottom: -8px;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
transition: background-color 300ms, padding 300ms;
|
||||
|
||||
&.hovering {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: relative;
|
||||
@@ -169,9 +148,10 @@
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
transition: background-color 300ms, border 300ms;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
&.waiting {
|
||||
.shape {
|
||||
background: var(--grey);
|
||||
@@ -180,14 +160,12 @@
|
||||
|
||||
.connector {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
height: 88px;
|
||||
width: 10px;
|
||||
left: -5px;
|
||||
top: -73px;
|
||||
transform: translateX(120%);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='20'%3E%3Cpath style='fill:%239339f4;' d='M 0,20 5,15 10,20 Z'/%3E%3Cpath style='fill:%23653b9c;' d='M 0,20 5,15 10,20 10,10 5,5 0,10 Z'/%3E%3Cpath style='fill:%239339f4;' d='M 0,10 5,5 10,10 10,0 0,0 Z'/%3E%3C/svg%3E%0A"); // linear-gradient(135deg, var(--tertiary) 34%, transparent 34%),
|
||||
background-size: 10px 20px;
|
||||
background: var(--tertiary);
|
||||
|
||||
&.down {
|
||||
border-top-left-radius: 10px;
|
||||
@@ -198,14 +176,14 @@
|
||||
}
|
||||
|
||||
&.loading {
|
||||
animation: goFasterUp 0.8s infinite linear;
|
||||
animation: acceleratePulse 1s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.accelerated {
|
||||
.shape-border {
|
||||
animation: acceleratePulse 0.4s infinite;
|
||||
animation: acceleratePulse 1s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,14 +194,14 @@
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: -66px;
|
||||
margin-top: -64px;
|
||||
|
||||
.badge.badge-waiting {
|
||||
opacity: 0.5;
|
||||
background-color: var(--grey);
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.badge.badge-accelerated {
|
||||
background-color: var(--tertiary);
|
||||
color: white;
|
||||
@@ -231,20 +209,10 @@
|
||||
}
|
||||
|
||||
.time {
|
||||
margin-top: 32px;
|
||||
margin-top: 33px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
white-space: nowrap;
|
||||
|
||||
&.offset-left {
|
||||
@media (max-width: 650px) {
|
||||
margin-left: -20px;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-margin {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,18 +223,3 @@
|
||||
50% { background-color: var(--mainnet-alt) }
|
||||
100% { background-color: var(--tertiary) }
|
||||
}
|
||||
|
||||
@keyframes goFasterUp {
|
||||
0% { background-position-y: 0; }
|
||||
100% { background-position-y: -40px; }
|
||||
}
|
||||
|
||||
@keyframes goFasterLeft {
|
||||
0% { background-position: left 0px bottom 0px }
|
||||
100% { background-position: left 40px bottom 0px; }
|
||||
}
|
||||
|
||||
@keyframes goFasterRight {
|
||||
0% { background-position: right 0 bottom 0px; }
|
||||
100% { background-position: right -40px bottom 0px; }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
|
||||
import { Component, Input, OnInit, OnChanges } from '@angular/core';
|
||||
import { ETA } from '../../services/eta.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface';
|
||||
import { MiningService } from '../../services/mining.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-timeline',
|
||||
@@ -12,7 +10,6 @@ import { MiningService } from '../../services/mining.service';
|
||||
export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
@Input() transactionTime: number;
|
||||
@Input() tx: Transaction;
|
||||
@Input() accelerationInfo: Acceleration;
|
||||
@Input() eta: ETA;
|
||||
// A mined transaction has standard ETA and accelerated ETA undefined
|
||||
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
|
||||
@@ -22,35 +19,15 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
acceleratedAt: number;
|
||||
now: number;
|
||||
accelerateRatio: number;
|
||||
useAbsoluteTime: boolean = false;
|
||||
interval: number;
|
||||
|
||||
tooltipPosition = null;
|
||||
hoverInfo: any = null;
|
||||
poolsData: { [id: number]: SinglePoolStats } = {};
|
||||
|
||||
constructor(
|
||||
private miningService: MiningService,
|
||||
) {}
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||
|
||||
this.miningService.getPools().subscribe(pools => {
|
||||
for (const pool of pools) {
|
||||
this.poolsData[pool.unique_id] = pool;
|
||||
}
|
||||
});
|
||||
|
||||
this.interval = window.setInterval(() => {
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
// Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65
|
||||
|
||||
// if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) {
|
||||
@@ -63,46 +40,4 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
onHover(event, status: string): void {
|
||||
if (status === 'seen') {
|
||||
this.hoverInfo = {
|
||||
status,
|
||||
fee: this.tx.fee,
|
||||
weight: this.tx.weight
|
||||
};
|
||||
} else if (status === 'accelerated') {
|
||||
this.hoverInfo = {
|
||||
status,
|
||||
fee: this.accelerationInfo?.effectiveFee || this.tx.fee,
|
||||
weight: this.tx.weight,
|
||||
feeDelta: this.accelerationInfo?.feeDelta || this.tx.feeDelta,
|
||||
pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
|
||||
poolsData: this.poolsData
|
||||
};
|
||||
} else if (status === 'mined') {
|
||||
this.hoverInfo = {
|
||||
status,
|
||||
fee: this.accelerationInfo?.effectiveFee,
|
||||
weight: this.tx.weight,
|
||||
bidBoost: this.accelerationInfo?.bidBoost,
|
||||
minedByPoolUniqueId: this.accelerationInfo?.minedByPoolUniqueId,
|
||||
pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
|
||||
poolsData: this.poolsData
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onBlur(event): void {
|
||||
this.hoverInfo = null;
|
||||
}
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null, opacity: isLoading ? 0.5 : 1 }" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" [style]="{opacity: isLoading ? 0.5 : 1}">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { EChartsOption } from '../../../graphs/echarts';
|
||||
import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs';
|
||||
import { startWith, switchMap, tap } from 'rxjs/operators';
|
||||
@@ -8,11 +8,10 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils';
|
||||
import { StorageService } from '../../../services/storage.service';
|
||||
import { MiningService } from '../../../services/mining.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Acceleration } from '../../../interfaces/node-api.interface';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-fees-graph',
|
||||
@@ -23,7 +22,7 @@ import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -33,7 +32,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
||||
@Input() height: number = 300;
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
@Input() period: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
|
||||
@Input() period: '3d' | '1w' | '1m' = '1w';
|
||||
@Input() accelerations$: Observable<Acceleration[]>;
|
||||
|
||||
miningWindowPreference: string;
|
||||
@@ -49,7 +48,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
||||
isLoading = true;
|
||||
formatNumber = formatNumber;
|
||||
timespan = '';
|
||||
periodSubject$: Subject<'24h' | '3d' | '1w' | '1m' | 'all'> = new Subject();
|
||||
periodSubject$: Subject<'3d' | '1w' | '1m'> = new Subject();
|
||||
chartInstance: any = undefined;
|
||||
daysAvailable: number = 0;
|
||||
|
||||
@@ -63,8 +62,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
||||
private route: ActivatedRoute,
|
||||
public stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1w' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1w');
|
||||
@@ -81,7 +78,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
this.route.fragment.subscribe((fragment) => {
|
||||
if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) {
|
||||
if (['24h', '3d', '1w', '1m', '3m'].indexOf(fragment) > -1) {
|
||||
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
||||
}
|
||||
});
|
||||
@@ -297,19 +294,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
||||
|
||||
onChartInit(ec) {
|
||||
this.chartInstance = ec;
|
||||
|
||||
this.chartInstance.on('click', (e) => {
|
||||
this.zone.run(() => {
|
||||
if (['24h', '3d'].includes(this.timespan)) {
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/block/${e.data[2]}`);
|
||||
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
|
||||
window.open(url);
|
||||
} else {
|
||||
this.router.navigate([url]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.totalRequested }}</div>
|
||||
<div class="symbol" i18n="accelerator.total-accelerated-plural">accelerated</div>
|
||||
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
|
||||
@@ -16,7 +16,7 @@ export type AccelerationStats = {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AccelerationStatsComponent implements OnInit, OnChanges {
|
||||
@Input() timespan: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
|
||||
@Input() timespan: '3d' | '1w' | '1m' = '1w';
|
||||
accelerationStats$: Observable<AccelerationStats>;
|
||||
blocksInPeriod: number = 7 * 144;
|
||||
|
||||
@@ -35,9 +35,6 @@ export class AccelerationStatsComponent implements OnInit, OnChanges {
|
||||
updateStats(): void {
|
||||
this.accelerationStats$ = this.servicesApiService.getAccelerationStats$({ timeframe: this.timespan });
|
||||
switch (this.timespan) {
|
||||
case '24h':
|
||||
this.blocksInPeriod = 144;
|
||||
break;
|
||||
case '3d':
|
||||
this.blocksInPeriod = 3 * 144;
|
||||
break;
|
||||
@@ -47,9 +44,6 @@ export class AccelerationStatsComponent implements OnInit, OnChanges {
|
||||
case '1m':
|
||||
this.blocksInPeriod = 30 * 144;
|
||||
break;
|
||||
case 'all':
|
||||
this.blocksInPeriod = Infinity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<ng-container *ngIf="!pending">
|
||||
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
|
||||
<th class="block text-right" i18n="shared.block-title">Block</th>
|
||||
<th class="pool text-right" i18n="mining.pool-name" *ngIf="!this.widget">Pool</th>
|
||||
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
|
||||
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
|
||||
</ng-container>
|
||||
@@ -50,20 +49,9 @@
|
||||
<a *ngIf="acceleration.blockHeight" [routerLink]="['/block' | relativeUrl, acceleration.blockHeight]">{{ acceleration.blockHeight }}</a>
|
||||
<span *ngIf="!acceleration.blockHeight">~</span>
|
||||
</td>
|
||||
<td class="pool text-right" *ngIf="!this.widget">
|
||||
@if (acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]) {
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pools[acceleration.minedByPoolUniqueId].slug]" class="badge" style="color: #FFF;padding:0;">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + pools[acceleration.minedByPoolUniqueId].slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pools[acceleration.minedByPoolUniqueId].name + ' mining pool'">
|
||||
{{ pools[acceleration.minedByPoolUniqueId].name }}
|
||||
</a>
|
||||
} @else {
|
||||
~
|
||||
}
|
||||
</td>
|
||||
<td class="status text-right">
|
||||
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
|
||||
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success" i18n="accelerator.completed">Completed <span *ngIf="acceleration.status === 'completed_provisional'">🔄</span></span>
|
||||
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success" i18n="accelerator.mined">Mined <span *ngIf="acceleration.status === 'completed_provisional'">🔄</span></span>
|
||||
<span *ngIf="acceleration.status.includes('completed')" class="badge badge-success" i18n="">Completed <span *ngIf="acceleration.status === 'completed_provisional'">🔄</span></span>
|
||||
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger" i18n="accelerator.canceled">Failed <span *ngIf="acceleration.status === 'failed_provisional'">🔄</span></span>
|
||||
</td>
|
||||
<td class="date text-right" *ngIf="!this.widget">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.container-xl.legacy {
|
||||
max-width: 1200px;
|
||||
max-width: 1140px;
|
||||
}
|
||||
.container-xl.widget-container {
|
||||
min-height: 335px;
|
||||
@@ -72,25 +72,9 @@ tr, td, th {
|
||||
|
||||
.block {
|
||||
width: 15%;
|
||||
@media (max-width: 900px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.pool {
|
||||
width: 15%;
|
||||
|
||||
@media (max-width: 700px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, Subscription, catchError, filter, of, switchMap, tap, throttleTime } from 'rxjs';
|
||||
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MiningService } from '../../../services/mining.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerations-list',
|
||||
@@ -31,13 +30,11 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
keyNavigationSubscription: Subscription;
|
||||
dir: 'rtl' | 'ltr' = 'ltr';
|
||||
paramSubscription: Subscription;
|
||||
pools: { [id: number]: SinglePoolStats } = {};
|
||||
|
||||
constructor(
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private websocketService: WebsocketService,
|
||||
public stateService: StateService,
|
||||
private miningService: MiningService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private seoService: SeoService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -82,12 +79,6 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
).subscribe(() => {
|
||||
this.pageChange(this.page);
|
||||
});
|
||||
|
||||
this.miningService.getPools().subscribe(pools => {
|
||||
for (const pool of pools) {
|
||||
this.pools[pool.unique_id] = pool;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
|
||||
|
||||
@@ -23,18 +23,12 @@
|
||||
<div class="main-title">
|
||||
<span [attr.data-cy]="'acceleration-stats'" i18n="accelerator.acceleration-stats">Acceleration stats</span>
|
||||
@switch (timespan) {
|
||||
@case ('24h') {
|
||||
<span style="font-size: xx-small" i18n="mining.1-day">(1 day)</span>
|
||||
}
|
||||
@case ('1w') {
|
||||
<span style="font-size: xx-small" i18n="mining.1-week">(1 week)</span>
|
||||
}
|
||||
@case ('1m') {
|
||||
<span style="font-size: xx-small" i18n="mining.1-month">(1 month)</span>
|
||||
}
|
||||
@case ('all') {
|
||||
<span style="font-size: xx-small" i18n="mining.all-time">(all time)</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="card-wrapper">
|
||||
@@ -42,17 +36,11 @@
|
||||
<div class="card-body more-padding">
|
||||
<app-acceleration-stats [timespan]="timespan"></app-acceleration-stats>
|
||||
<div class="widget-toggler">
|
||||
<a href="" (click)="setTimespan('24h')" class="toggler-option"
|
||||
[ngClass]="{'inactive': timespan === '24h'}"><small>24h</small></a>
|
||||
<span style="color: #ffffff66; font-size: 8px"> | </span>
|
||||
<a href="" (click)="setTimespan('1w')" class="toggler-option"
|
||||
[ngClass]="{'inactive': timespan === '1w'}"><small>1w</small></a>
|
||||
<span style="color: #ffffff66; font-size: 8px"> | </span>
|
||||
<a href="" (click)="setTimespan('1m')" class="toggler-option"
|
||||
[ngClass]="{'inactive': timespan === '1m'}"><small>1m</small></a>
|
||||
<span style="color: #ffffff66; font-size: 8px"> | </span>
|
||||
<a href="" (click)="setTimespan('all')" class="toggler-option"
|
||||
[ngClass]="{'inactive': timespan === 'all'}"><small>all</small></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
|
||||
webGlEnabled = true;
|
||||
seen: Set<string> = new Set();
|
||||
firstLoad = true;
|
||||
timespan: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
|
||||
timespan: '3d' | '1w' | '1m' = '1w';
|
||||
|
||||
accelerationDeltaSubscription: Subscription;
|
||||
|
||||
@@ -99,7 +99,7 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
|
||||
this.minedAccelerations$ = this.stateService.chainTip$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap(() => {
|
||||
return this.serviceApiServices.getAccelerationHistory$({ status: 'completed_provisional,completed', pageLength: 6 }).pipe(
|
||||
return this.serviceApiServices.getAccelerationHistory$({ status: 'completed', pageLength: 6 }).pipe(
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
}),
|
||||
|
||||
@@ -18,12 +18,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
@if (hasCpfp) {
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
}
|
||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -32,15 +27,6 @@
|
||||
<ng-container i18n="accelerator.x-of-hash-rate">{{ acceleratedByPercentage }} <span class="symbol hashrate-label">of hashrate</span></ng-container>
|
||||
</td>
|
||||
</tr>
|
||||
@if (hasCpfp && chartPositionLeft) {
|
||||
<tr>
|
||||
<td colspan="3" class="pt-0">
|
||||
<div class="d-flex justify-content-end align-items-start">
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Transaction } from '../../../interfaces/electrs.interface';
|
||||
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
|
||||
@@ -27,10 +27,8 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
@Input() accelerationInfo: Acceleration;
|
||||
@Input() miningStats: MiningStats;
|
||||
@Input() pools: number[];
|
||||
@Input() hasCpfp: boolean = false;
|
||||
@Input() chartOnly: boolean = false;
|
||||
@Input() chartPositionLeft: boolean = false;
|
||||
@Output() toggleCpfp = new EventEmitter();
|
||||
|
||||
acceleratedByPercentage: string = '';
|
||||
|
||||
@@ -135,8 +133,4 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
}
|
||||
this.chartInstance = ec;
|
||||
}
|
||||
|
||||
onToggleCpfp(): void {
|
||||
this.toggleCpfp.emit();
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ const periodSeconds = {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
<span i18n="address.error.loading-address-data">Error loading address data.</span>
|
||||
<br>
|
||||
<ng-container i18n="Electrum server limit exceeded error">
|
||||
<i>There are too many transactions on this address, more than your backend can handle. See more on <a href="/docs/faq#address-lookup-issues">setting up a stronger backend</a>.</i>
|
||||
<i>There many transactions on this address, more than your backend can handle. See more on <a href="/docs/faq#address-lookup-issues">setting up a stronger backend</a>.</i>
|
||||
<br><br>
|
||||
Consider viewing this address on the official Mempool website instead:
|
||||
</ng-container>
|
||||
@@ -249,7 +249,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #pendingBalanceRow>
|
||||
<td i18n="accelerator.pending-state" class="font-italic">Pending</td>
|
||||
<td i18n="address.unconfirmed-balance" class="font-italic">Unconfirmed balance</td>
|
||||
<td *ngIf="mempoolStats.funded_txo_sum !== undefined; else confidentialTd" class="font-italic wrap-cell"><app-amount [satoshis]="mempoolStats.balance" [noFiat]="true" [addPlus]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolStats.balance"></app-fiat></span></td>
|
||||
</ng-template>
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #pendingUtxoRow>
|
||||
<td i18n="address.pending-utxos" class="font-italic">Pending UTXOs</td>
|
||||
<td i18n="address.unconfirmed-utxos" class="font-italic">Unconfirmed UTXOs</td>
|
||||
<td class="font-italic wrap-cell">{{ mempoolStats.utxos > 0 ? '+' : ''}}{{ mempoolStats.utxos }}</td>
|
||||
</ng-template>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Subscription, of, timer } from 'rxjs';
|
||||
import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { retry, switchMap, tap } from 'rxjs/operators';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
|
||||
@Component({
|
||||
@@ -73,11 +73,8 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.paymentStatus = 4;
|
||||
}
|
||||
this.paymentStatusSubscription = this.apiService.getPaymentStatus$(this.invoice.btcpayInvoiceId).pipe(
|
||||
retry({ delay: () => timer(2000)}),
|
||||
repeat({delay: 2000}),
|
||||
filter((response) => response.status !== 204 && response.status !== 404),
|
||||
take(1),
|
||||
).subscribe(() => {
|
||||
retry({ delay: () => timer(2000)})
|
||||
).subscribe((result) => {
|
||||
this.paymentStatus = 3;
|
||||
this.completed.emit();
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
|
||||
.menu-toggle {
|
||||
width: 3em;
|
||||
min-width: 3em;
|
||||
height: 1.8em;
|
||||
padding: 0px 1px;
|
||||
opacity: 0;
|
||||
@@ -43,7 +42,6 @@
|
||||
border: none;
|
||||
border-radius: 0.35em;
|
||||
pointer-events: all;
|
||||
align-self: normal;
|
||||
}
|
||||
|
||||
.filter-menu {
|
||||
|
||||
@@ -21,7 +21,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -68,7 +68,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
||||
this.effectiveRate = this.tx.rate;
|
||||
const txFlags = BigInt(this.tx.flags) || 0n;
|
||||
this.acceleration = this.tx.acc || (txFlags & TransactionFlags.acceleration);
|
||||
this.hasEffectiveRate = this.tx.acc || !(Math.abs((this.fee / this.vsize) - this.effectiveRate) <= 0.1 && Math.abs((this.fee / Math.ceil(this.vsize)) - this.effectiveRate) <= 0.1)
|
||||
this.hasEffectiveRate = this.tx.acc || Math.abs((this.fee / this.vsize) - this.effectiveRate) > 0.05
|
||||
|| (txFlags && (txFlags & (TransactionFlags.cpfp_child | TransactionFlags.cpfp_parent)) > 0n);
|
||||
this.filters = this.tx.flags ? toFilters(txFlags).filter(f => f.tooltip) : [];
|
||||
this.activeFilters = {}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -52,9 +52,9 @@
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<td i18n="block.miner">Miner</td>
|
||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
{{ block.extras.pool.name }}
|
||||
<a [attr.data-cy]="'block-details-miner-badge'" placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block?.extras.pool.slug]" class="badge"
|
||||
[class]="!block?.extras.pool.name || block?.extras.pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||
{{ block?.extras.pool.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||
|
||||
@@ -65,11 +65,3 @@
|
||||
.badge {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loading>
|
||||
<div class="tx-skeleton">
|
||||
<div class="text-center mb-4" class="tx-skeleton">
|
||||
<ng-container *ngIf="(txsLoadingStatus$ | async) as txsLoadingStatus; else headerLoader">
|
||||
<div class="header-bg box">
|
||||
<div class="progress progress-dark" style="margin: 4px; height: 14px;">
|
||||
|
||||
@@ -181,8 +181,8 @@
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<td i18n="block.miner">Miner</td>
|
||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
||||
[class]="block.extras.pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||
{{ block.extras.pool.name }}
|
||||
</a>
|
||||
</td>
|
||||
@@ -338,7 +338,7 @@
|
||||
<ngb-pagination class="pagination-container float-right" [disabled]="true" [collectionSize]="block.tx_count" [rotate]="true" [pageSize]="stateService.env.ITEMS_PER_PAGE" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="tx-skeleton">
|
||||
<div class="text-center mb-4" class="tx-skeleton">
|
||||
|
||||
<div class="header-bg box">
|
||||
<span class="skeleton-loader"></span>
|
||||
@@ -411,7 +411,7 @@
|
||||
<td class="text-wrap">
|
||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
<span *ngIf="oobFees" class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band">
|
||||
<app-amount [satoshis]="oobFees" digitsInfo="1.8-8" [noFiat]="true" [addPlus]="true"></app-amount>
|
||||
<app-amount [satoshis]="oobFees" digitsInfo="1.2-8" [noFiat]="true" [addPlus]="true"></app-amount>
|
||||
</span>
|
||||
<span *ngIf="blockAudit.feeDelta" class="difference" [class.positive]="blockAudit.feeDelta <= 0" [class.negative]="blockAudit.feeDelta > 0">
|
||||
{{ blockAudit.feeDelta < 0 ? '+' : '' }}{{ (-blockAudit.feeDelta * 100) | amountShortener: 2 }}%
|
||||
|
||||
@@ -272,11 +272,3 @@ h1 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
@@ -59,11 +59,10 @@
|
||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [precision]="1" minUnit="minute"></app-time></div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="animated" *ngIf="block.extras?.pool != undefined && showPools">
|
||||
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
{{ block.extras.pool.name}}
|
||||
</a>
|
||||
<div class="animated" [class]="markHeight === block.height ? 'hide' : 'show'" *ngIf="block.extras?.pool != undefined && showPools">
|
||||
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
|
||||
[routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
{{ block.extras.pool.name}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -86,7 +85,7 @@
|
||||
</ng-template>
|
||||
</div>
|
||||
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="arrowTransition"
|
||||
[ngStyle]="{'left': arrowLeftPx + 8 + 'px' }"></div>
|
||||
[ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingBlocksTemplate>
|
||||
|
||||
@@ -125,12 +125,12 @@
|
||||
#arrow-up {
|
||||
position: relative;
|
||||
left: calc(var(--block-size) * 0.6);
|
||||
top: calc(var(--block-size) * 1.28);
|
||||
top: calc(var(--block-size) * 1.12);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: calc(var(--block-size) * 0.2) solid transparent;
|
||||
border-right: calc(var(--block-size) * 0.2) solid transparent;
|
||||
border-bottom: calc(var(--block-size) * 0.2) solid var(--fg);
|
||||
border-left: calc(var(--block-size) * 0.3) solid transparent;
|
||||
border-right: calc(var(--block-size) * 0.3) solid transparent;
|
||||
border-bottom: calc(var(--block-size) * 0.3) solid var(--fg);
|
||||
}
|
||||
|
||||
.flashing {
|
||||
@@ -157,20 +157,17 @@
|
||||
position: relative;
|
||||
top: 15px;
|
||||
z-index: 101;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.animated {
|
||||
transition: all 0.15s ease-in-out;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.hide {
|
||||
opacity: 0.4;
|
||||
pointer-events : none;
|
||||
}
|
||||
|
||||
.time-ltr {
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
}
|
||||
|
||||
.blockchain-wrapper {
|
||||
height: 260px;
|
||||
height: 250px;
|
||||
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE10+/Edge */
|
||||
@@ -56,7 +57,7 @@
|
||||
color: var(--fg);
|
||||
font-size: 0.8rem;
|
||||
position: absolute;
|
||||
bottom: -2.2em;
|
||||
bottom: -1.8em;
|
||||
left: 1px;
|
||||
transform: translateX(-50%);
|
||||
background: none;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
.loadingGraphs.widget {
|
||||
top: 75%;
|
||||
|
||||
@@ -28,7 +28,7 @@ interface Hashrate {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -17,7 +17,7 @@ const OUTLIERS_MEDIAN_MULTIPLIER = 4;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { StateService } from '../../services/state.service';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
templateUrl: './lbtc-pegs-graph.component.html',
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
.sticky-loading {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
font-size: 14px;
|
||||
@media (width >= 992px) {
|
||||
left: 32px;
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
#arrow-up {
|
||||
position: relative;
|
||||
right: calc(var(--block-size) * 0.6);
|
||||
top: calc(var(--block-size) * 1.20);
|
||||
top: calc(var(--block-size) * 1.12);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: calc(var(--block-size) * 0.3) solid transparent;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/grap
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 16px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -34,12 +34,7 @@
|
||||
<li class="nav-item d-flex justify-content-start align-items-center menu-click">
|
||||
<fa-icon class="menu-click" [icon]="['fas', item.faIcon]" [fixedWidth]="true"></fa-icon>
|
||||
<button *ngIf="item.link === 'logout'" class="btn nav-link menu-click" role="tab" (click)="logout()">{{ item.title }}</button>
|
||||
<a *ngIf="item.title !== 'Logout'" class="nav-link menu-click" [routerLink]="[item.link]" role="tab">
|
||||
{{ item.title }}
|
||||
@if (item.isExternal === true) {
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="margin-left: 5px; font-size: 13px; color: lightgray"></fa-icon>
|
||||
}
|
||||
</a>
|
||||
<a *ngIf="item.title !== 'Logout'" class="nav-link menu-click" [routerLink]="[item.link]" role="tab">{{ item.title }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.pool-distribution {
|
||||
|
||||
@@ -167,7 +167,7 @@ div.scrollable {
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
top: 475px;
|
||||
@media (max-width: 992px) {
|
||||
top: 600px;
|
||||
|
||||
@@ -5,107 +5,73 @@
|
||||
<br><br>
|
||||
|
||||
<h2>Privacy Policy</h2>
|
||||
<h6>Updated: August 02, 2024</h6>
|
||||
<h6>Updated: November 23, 2023</h6>
|
||||
|
||||
<br><br>
|
||||
|
||||
<div class="text-left">
|
||||
|
||||
<h4>USE YOUR OWN SELF-HOSTED MEMPOOL EXPLORER</h4>
|
||||
|
||||
<p>For maximum privacy, We recommend that you use your own self-hosted instance of The Mempool Open Source Project®; on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See Our project's GitHub page for more details about self-hosting this website. By using your own self-hosted instance you will have maximum security, privacy and freedom.</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bitcoin.gob.sv/">bitcoin.gob.sv</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
|
||||
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
|
||||
|
||||
<p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p>
|
||||
|
||||
<br>
|
||||
|
||||
<h5>By accessing this Website, you agree to the following Privacy Policy:</h5>
|
||||
|
||||
<br>
|
||||
|
||||
<h4>USING THIS WEBSITE</h4>
|
||||
<h4>TRUSTED THIRD PARTIES ARE SECURITY HOLES</h4>
|
||||
|
||||
<p *ngIf="officialMempoolSpace">Out of respect for the Bitcoin community, this Website does not use any third-party analytics, third-party trackers, or third-party cookies, and We do not share any private user data with third-parties. Additionally, to mitigate the risk of surveillance by malicious third-parties, We self-host this Website on Our own hardware and network infrastructure, so there are no "hosting companies" or "cloud providers" involved with the operation of this Website.</p>
|
||||
<p>Out of respect for the Bitcoin community, this website does not use any third-party analytics, third-party trackers, or third-party cookies, and we do not share any private user data with third-parties. Additionally, to mitigate the risk of surveillance by malicious third-parties, we self-host this website on our own hardware and network infrastructure, so there are no "hosting companies" or "cloud providers" involved with the operation of this website.</p>
|
||||
|
||||
<p>Out of respect for the Bitcoin community, this Website does not use any first-party cookies, except to store your preferred language setting (if any). However, We do use minimal first-party analytics and logging as needed for the operation of this Website, as follows:</p>
|
||||
<br>
|
||||
|
||||
<h4>TRUSTED FIRST PARTIES ARE ALSO SECURITY HOLES</h4>
|
||||
|
||||
<p>Out of respect for the Bitcoin community, this website does not use any first-party cookies, except to store your preferred language setting (if any). However, we do use minimal first-party analytics and logging as needed for the operation of this website, as follows:</p>
|
||||
|
||||
<ul>
|
||||
|
||||
<li>We use basic webserver logging (nginx) for sysadmin purposes, which collects your IP address along with the requests you make. These logs are deleted after 10 days, and We do not share this data with any third-party.</li>
|
||||
<li>We use basic webserver logging (nginx) for sysadmin purposes, which collects your IP address along with the requests you make. These logs are deleted after 10 days, and we do not share this data with any third-party. To conceal your IP address from our webserver logs, we recommend that you use Tor Browser with our Tor v3 hidden service onion hostname.</li>
|
||||
|
||||
<br>
|
||||
|
||||
<li>We use a self-hosted statistics application (matomo) for analytics purposes, which collects your IP address along with the requests you make. Our matomo instance is configured to respect your privacy by redacting your IP address and other methods, and We do not share this data with any third-party. To conceal your activity from Our analytics, We recommend that you use a privacy protecting browser extension that blocks matomo from being loaded.</li>
|
||||
<li>We use a self-hosted statistics application (matomo) for analytics purposes, which collects your IP address along with the requests you make. Our matomo instance is configured to respect your privacy by redacting your IP address and other methods, and we do not share this data with any third-party. To conceal your activity from our analytics, we recommend that you use a privacy protecting browser extension that blocks matomo from being loaded.</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<br>
|
||||
|
||||
<h4>USING MEMPOOL ACCELERATOR™</h4>
|
||||
<h4>TRUST YOUR OWN SELF-HOSTED MEMPOOL EXPLORER</h4>
|
||||
|
||||
<p *ngIf="officialMempoolSpace">If you use Mempool Accelerator™ your acceleration request will be sent to Mempool and relayed to Mempool mining pool partners. We will store the TXID of the transactions you accelerate with Us. We share this information with Mempool mining pool partners, and publicly display accelerated transaction details on Our website and via Our APIs. No personal information or account identifiers will be shared with any third party including Our mining pool partners.</p>
|
||||
|
||||
<p *ngIf="!officialMempoolSpace">When using Mempool Accelerator™ the mempool.space privacy policy will apply: <a href="https://mempool.space/privacy-policy">https://mempool.space/privacy-policy</a>.</p>
|
||||
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project® on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website.</p>
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
|
||||
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
|
||||
<h4>DONATING TO MEMPOOL.SPACE</h4>
|
||||
|
||||
<p>If you sign up for an account on mempool.space, We may collect the following:</p>
|
||||
<p>If you donate to mempool.space, your payment information and your Twitter identity (if provided) will be collected in a database, which may be used to publicly display the sponsor profiles on <a href="https://mempool.space/about">mempool.space/about</a>. Thank you for supporting The Mempool Open Source Project.</p>
|
||||
|
||||
<ul>
|
||||
<br>
|
||||
|
||||
<li>Your e-mail address and/or country; We may use this information to manage your user account, for billing purposes, or to update you about Our services. We will not share this with any third-party, except as necessary for Our fiat payment processor (see "Payments" below).</li>
|
||||
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
|
||||
|
||||
<li>If you connect your X (fka Twitter) account, We may store your X identity, e-mail address, and profile photo. We may publicly display your profile photo or link to your profile on Our website, if you sponsor The Mempool Open Source Project®, claim your Lightning node, or other such use cases.</li>
|
||||
<p>If you sign up for an account on mempool.space, we may collect the following:</p>
|
||||
|
||||
<li>If you sign up for a subscription to Mempool Enterprise™ We also collect your company name which is not shared with any third-party.</li>
|
||||
<ol>
|
||||
|
||||
<li>If you sign up for an account on mempool.space and use Mempool Accelerator™ Pro your accelerated transactions will be associated with your account for the purposes of accounting.</li>
|
||||
<li>If you provide your name, country, and/or e-mail address, we may use this information to manage your user account, for billing purposes, or to update you about our services. We will not share this with any third-party, except as detailed below if you sponsor The Mempool Open Source Project®, purchase a subscription to Mempool Enterprise®, or accelerate transactions using Mempool Accelerator™.</li>
|
||||
|
||||
</ul>
|
||||
<li>If you connect your Twitter account, we may store your Twitter identity, e-mail address, and profile photo. We may publicly display your profile photo or link to your profile on our website, if you sponsor The Mempool Open Source Project, claim your Lightning node, or other such use cases.</li>
|
||||
|
||||
<br>
|
||||
<li>If you make a credit card payment, we will process your payment using Square (Block, Inc.), and we will store details about the transaction in our database. Please see "Information we collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy</li>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
|
||||
<h4>PAYMENTS AND DONATIONS</h4>
|
||||
<li>If you make a Bitcoin or Liquid payment, we will process your payment using our self-hosted BTCPay Server instance and not share these details with any third-party.</li>
|
||||
|
||||
<p>If you make any payment to Mempool or donation to The Mempool Open Source Project®, We may collect the following:</p>
|
||||
<li>If you accelerate transactions using Mempool Accelerator™, we will store the TXID of your transactions you accelerate with us. We share this information with our mining pool partners, as well as publicly display accelerated transaction details on our website and APIs.</li>
|
||||
|
||||
<ul>
|
||||
</ol>
|
||||
|
||||
<li>Your e-mail address and/or country; We may use this information to manage your user account, for billing purposes, or to update you about Our services. We will not share this with any third-party, except as necessary for Our fiat payment processor.</li>
|
||||
|
||||
<li>If you make a payment using Bitcoin, We will process your payment using Our self-hosted BTCPay Server instance. We will not share your payment details with any third-party. For payments made over the Lightning network, We may utilize third party LSPs / lightning liquidity providers.</li>
|
||||
|
||||
<li>If you make a payment using Fiat We will collect your payment details. We will share your payment details with Our fiat payment processor Square (Block, Inc.) - Please see "Information We collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy.</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<br>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
|
||||
<h4>DATA RETENTION AND ACCOUNT INACTIVITY</h4>
|
||||
|
||||
<p>We aim to retain your data only as long as necessary:</p>
|
||||
<ul>
|
||||
<li>An account is considered inactive if all of the following conditions are met: a) No login activity within the past 6 months, b) No active subscriptions associated with the account, c) No Mempool Accelerator™ Pro account credit</li>
|
||||
<li>If an account meets the criteria for inactivity as defined above, We will automatically delete the associated account data after a period of 6 months of continuous inactivity, except in the case of payment disputes or account irregularities.</li>
|
||||
</ul>
|
||||
|
||||
</ng-container>
|
||||
<br>
|
||||
|
||||
<p>EOF</p>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
ngbTooltip="Miners Reward" placement="bottom" #minersreward [disableTooltip]="!isEllipsisActive(minersreward)">Miners Reward</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.rewards-desc" ngbTooltip="Amount being paid to miners in the past 144 blocks" placement="bottom">
|
||||
<div class="fee-text">
|
||||
<app-amount [satoshis]="rewardStats.totalReward" digitsInfo="1.2-2" [noFiat]="true" [ignoreViewMode]="true"></app-amount>
|
||||
<app-amount [satoshis]="rewardStats.totalReward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
||||
</div>
|
||||
<span class="fiat">
|
||||
<app-fiat [value]="rewardStats.totalReward" digitsInfo="1.0-0" ></app-fiat>
|
||||
|
||||
@@ -303,6 +303,7 @@ export class SearchFormComponent implements OnInit {
|
||||
(error) => { console.log(error); this.isSearching = false; }
|
||||
);
|
||||
} else {
|
||||
this.searchResults.searchButtonClick();
|
||||
this.isSearching = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
<div class="card-title" i18n="search.mining-pools">Mining Pools</div>
|
||||
<ng-template ngFor [ngForOf]="results.pools" let-pool let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.active]="results.hashQuickMatch + results.addresses.length + i === activeIdx" [class.inactive]="!pool.active" type="button" role="option" class="dropdown-item">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||
<ngb-highlight [result]="pool.name" [term]="results.searchText"></ngb-highlight>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
@@ -26,11 +26,3 @@
|
||||
.active {
|
||||
background-color: var(--active-bg);
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
this.minScrollWidth = 40 + (8 * this.blockWidth) + (this.pageWidth * 2);
|
||||
|
||||
if (firstVisibleBlock != null) {
|
||||
this.scrollToBlock(firstVisibleBlock, offset + (this.isMobile ? this.blockWidth : 0));
|
||||
this.scrollToBlock(firstVisibleBlock, offset);
|
||||
} else {
|
||||
this.updatePages();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<br /><br />
|
||||
|
||||
<h2>Terms of Service</h2>
|
||||
<h6>Updated: July 10, 2024</h6>
|
||||
<h6>Updated: August 02, 2021</h6>
|
||||
|
||||
<br><br>
|
||||
|
||||
@@ -67,38 +67,6 @@
|
||||
|
||||
</ng-container>
|
||||
|
||||
<h4>MEMPOOL ACCELERATOR™</h4>
|
||||
|
||||
<p><a href="https://mempool.space/accelerator">Mempool Accelerator™</a> enables members of the Bitcoin community to submit requests for transaction prioritization. </p>
|
||||
<ul>
|
||||
|
||||
<li>Mempool will use reasonable commercial efforts to relay user acceleration requests to Mempool's mining pool partners, but it is at the discretion of Mempool and Mempool's mining pool partners to accept acceleration requests. </li>
|
||||
|
||||
<br>
|
||||
|
||||
<li>Acceleration requests cannot be canceled by the user once submitted. </li>
|
||||
|
||||
<br>
|
||||
|
||||
<li>Mempool reserves the right to cancel acceleration requests for any reason, including but not limited to the ejection of an accelerated transaction from Mempool's mempool. Canceled accelerations will not be refunded.</li>
|
||||
|
||||
<br>
|
||||
|
||||
<li>All acceleration payments and Mempool Accelerator™ account credit top-ups are non-refundable. </li>
|
||||
|
||||
<br>
|
||||
|
||||
<li>Mempool Accelerator™ account credit top-ups are prepayment for future accelerations and cannot be withdrawn or transferred.</li>
|
||||
|
||||
<br>
|
||||
|
||||
<li>Mempool does not provide acceleration services to persons in Cuba, Iran, North Korea, Russia, Syria, Crimea, Donetsk or Luhansk Regions of Ukraine.</li>
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
<br>
|
||||
|
||||
<p>EOF</p>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,6 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
||||
@Input() minUnit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' = 'second';
|
||||
@Input() fractionDigits: number = 0;
|
||||
@Input() lowercaseStart = false;
|
||||
|
||||
constructor(
|
||||
private ref: ChangeDetectorRef,
|
||||
@@ -107,9 +106,6 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return $localize`:@@date-base.immediately:Immediately`;
|
||||
} else if (seconds < 60) {
|
||||
if (this.relative || this.kind === 'since') {
|
||||
if (this.lowercaseStart) {
|
||||
return $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1);
|
||||
}
|
||||
return $localize`:@@date-base.just-now:Just now`;
|
||||
} else if (this.kind === 'until' || this.kind === 'within') {
|
||||
seconds = 60;
|
||||
|
||||
@@ -75,6 +75,9 @@
|
||||
} @else {
|
||||
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
}
|
||||
<!-- @if (!showAccelerationSummary && isMobile && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) {
|
||||
<a class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
} -->
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-template #etaSkeleton>
|
||||
@@ -117,79 +120,72 @@
|
||||
<div class="spinner-border text-light" style="width: 1em; height: 1em"></div>
|
||||
</div>
|
||||
<span class="explainer"> </span>
|
||||
} @else if (showAccelerationSummary) {
|
||||
<ng-container *ngIf="(ETA$ | async) as eta;">
|
||||
<app-accelerate-checkout
|
||||
*ngIf="(da$ | async) as da;"
|
||||
[cashappEnabled]="cashappEligible"
|
||||
[advancedEnabled]="false"
|
||||
[forceMobile]="true"
|
||||
[tx]="tx"
|
||||
[accelerating]="isAcceleration"
|
||||
[miningStats]="miningStats"
|
||||
[eta]="eta"
|
||||
[scrollEvent]="scrollIntoAccelPreview"
|
||||
(unavailable)="eligibleForAcceleration = false"
|
||||
class="h-100 w-100"
|
||||
></app-accelerate-checkout>
|
||||
</ng-container>
|
||||
} @else {
|
||||
@if (!tx.status?.confirmed && showAccelerationSummary) {
|
||||
<ng-container *ngIf="(ETA$ | async) as eta;">
|
||||
<app-accelerate-checkout
|
||||
*ngIf="(da$ | async) as da;"
|
||||
[cashappEnabled]="cashappEligible"
|
||||
[advancedEnabled]="false"
|
||||
[forceMobile]="true"
|
||||
[tx]="tx"
|
||||
[accelerating]="isAcceleration"
|
||||
[miningStats]="miningStats"
|
||||
[eta]="eta"
|
||||
[scrollEvent]="scrollIntoAccelPreview"
|
||||
(unavailable)="eligibleForAcceleration = false"
|
||||
class="w-100"
|
||||
></app-accelerate-checkout>
|
||||
</ng-container>
|
||||
}
|
||||
<div class="status-panel d-flex flex-column h-100 w-100 justify-content-center align-items-center" [class.small-status]="!tx.status?.confirmed && showAccelerationSummary">
|
||||
@if (tx?.acceleration && !tx.status?.confirmed) {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'wand-magic-sparkles']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.accelerated">Your transaction has been accelerated</span>
|
||||
} @else {
|
||||
@switch (trackerStage) {
|
||||
@case ('waiting') {
|
||||
<div class="progress-icon">
|
||||
<div class="spinner-border text-light" style="width: 1em; height: 1em"></div>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.waiting">Waiting for your transaction to appear in the mempool</span>
|
||||
}
|
||||
@case ('pending') {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'hourglass-start']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.pending">Your transaction is in the mempool, but it will not be confirmed for some time.</span>
|
||||
}
|
||||
@case ('soon') {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'hourglass-half']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.soon">Your transaction is near the top of the mempool, and is expected to confirm soon.</span>
|
||||
}
|
||||
@case ('next') {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'hourglass-end']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.next-block">Your transaction is expected to confirm in the next block</span>
|
||||
}
|
||||
@case ('confirmed') {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'circle-check']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.confirmed">Your transaction is confirmed!</span>
|
||||
}
|
||||
@case ('replaced') {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'timeline']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.replaced">Your transaction has been replaced by a newer version!</span>
|
||||
}
|
||||
@if (tx?.acceleration && !tx.status?.confirmed) {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'wand-magic-sparkles']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.accelerated">Your transaction has been accelerated</span>
|
||||
} @else {
|
||||
@switch (trackerStage) {
|
||||
@case ('waiting') {
|
||||
<div class="progress-icon">
|
||||
<div class="spinner-border text-light" style="width: 1em; height: 1em"></div>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.waiting">Waiting for your transaction to appear in the mempool</span>
|
||||
}
|
||||
@case ('pending') {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'hourglass-start']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.pending">Your transaction is in the mempool, but it will not be confirmed for some time.</span>
|
||||
}
|
||||
@case ('soon') {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'hourglass-half']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.soon">Your transaction is near the top of the mempool, and is expected to confirm soon.</span>
|
||||
}
|
||||
@case ('next') {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'hourglass-end']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.next-block">Your transaction is expected to confirm in the next block</span>
|
||||
}
|
||||
@case ('confirmed') {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'circle-check']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.confirmed">Your transaction is confirmed!</span>
|
||||
}
|
||||
@case ('replaced') {
|
||||
<div class="progress-icon">
|
||||
<fa-icon [icon]="['fas', 'timeline']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<span class="explainer" i18n="tracker.explain.replaced">Your transaction has been replaced by a newer version!</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="footer-link"
|
||||
[routerLink]="['/tx' | relativeUrl, tx?.txid]"
|
||||
[queryParams]="{ mode: 'details' }"
|
||||
queryParamsHandling="merge"
|
||||
>
|
||||
<div class="footer-link" [routerLink]="['/tx' | relativeUrl, tx?.txid]">
|
||||
<span><ng-container i18n="accelerator.show-more-details">See more details</ng-container> <fa-icon [icon]="['fas', 'arrow-alt-circle-right']"></fa-icon></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
position: relative;
|
||||
background: var(--nav-bg);
|
||||
box-shadow: 0 -5px 15px #000;
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
justify-content: center;
|
||||
|
||||
.explainer {
|
||||
margin: 0 1em;
|
||||
@@ -178,15 +178,6 @@
|
||||
font-size: clamp(30px, 20vw, 150px);
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
&.small-status {
|
||||
min-height: 180px;
|
||||
.progress-icon {
|
||||
font-size: clamp(20px, 13vw, 100px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -31,8 +31,6 @@ import { TrackerStage } from './tracker-bar.component';
|
||||
import { MiningService, MiningStats } from '../../services/mining.service';
|
||||
import { ETA, EtaService } from '../../services/eta.service';
|
||||
import { getTransactionFlags, getUnacceleratedFeeRate } from '../../shared/transaction.utils';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
|
||||
interface Pool {
|
||||
id: number;
|
||||
@@ -142,7 +140,6 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||
private priceService: PriceService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private miningService: MiningService,
|
||||
private router: Router,
|
||||
private cd: ChangeDetectorRef,
|
||||
private zone: NgZone,
|
||||
@Inject(ZONE_SERVICE) private zoneService: any,
|
||||
@@ -599,9 +596,6 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||
this.isAccelerated$,
|
||||
this.txChanged$,
|
||||
]).pipe(
|
||||
filter(([position, mempoolBlocks, da, isAccelerated]) => {
|
||||
return this.tx && !this.tx?.status?.confirmed && position && this.tx.txid === position.txid;
|
||||
}),
|
||||
map(([position, mempoolBlocks, da, isAccelerated]) => {
|
||||
return this.etaService.calculateETA(
|
||||
this.network,
|
||||
@@ -734,6 +728,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||
if (!this.txId) {
|
||||
return;
|
||||
}
|
||||
this.enterpriseService.goal(8);
|
||||
this.accelerationFlowCompleted = false;
|
||||
if (this.showAccelerationSummary) {
|
||||
this.scrollIntoAccelPreview = true;
|
||||
|
||||
@@ -62,8 +62,6 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<span id="accelerate"></span>
|
||||
|
||||
<ng-template [ngIf]="!isLoadingTx && !error">
|
||||
|
||||
<!-- CPFP Details -->
|
||||
@@ -169,7 +167,7 @@
|
||||
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [accelerationInfo]="accelerationInfo" [eta]="(ETA$ | async)" [standardETA]="(standardETA$ | async)?.time"></app-acceleration-timeline>
|
||||
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [eta]="(ETA$ | async)" [standardETA]="(standardETA$ | async)?.time"></app-acceleration-timeline>
|
||||
<br>
|
||||
</ng-container>
|
||||
|
||||
@@ -451,7 +449,7 @@
|
||||
<tr>
|
||||
<td i18n="block.timestamp">Timestamp</td>
|
||||
<td>
|
||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
||||
‎{{ 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"></app-time>)</i>
|
||||
</div>
|
||||
@@ -483,7 +481,7 @@
|
||||
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
|
||||
<td><i><app-time kind="since" [time]="transactionTime" [fastRender]="true" [showTooltip]="true"></app-time></i></td>
|
||||
</tr>
|
||||
} @else if (isLoadingFirstSeen) {
|
||||
} @else {
|
||||
<tr>
|
||||
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
@@ -552,19 +550,19 @@
|
||||
<td>
|
||||
<ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton">
|
||||
@if (eta.blocks >= 7) {
|
||||
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
|
||||
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
|
||||
<span i18n="transaction.eta.not-any-time-soon|Transaction ETA mot any time soon">Not any time soon</span>
|
||||
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
|
||||
<a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
<a class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
}
|
||||
</span>
|
||||
} @else if (network === 'liquid' || network === 'liquidtestnet') {
|
||||
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
} @else {
|
||||
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool justify-content-end align-items-center' : ''">
|
||||
<span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
|
||||
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) {
|
||||
<a class="btn btn-sm accelerateDeepMempool btn-small-height float-right" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
<a class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
}
|
||||
</span>
|
||||
<span class="eta justify-content-end">
|
||||
@@ -641,7 +639,7 @@
|
||||
}
|
||||
<td>
|
||||
<div class="effective-fee-container">
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize || tx.acceleration)) {
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
|
||||
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
} @else {
|
||||
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
@@ -665,7 +663,7 @@
|
||||
<ng-template #acceleratingRow>
|
||||
<tr>
|
||||
<td rowspan="2" colspan="2" style="padding: 0;">
|
||||
<app-active-acceleration-box [tx]="tx" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="showCpfpDetails = !showCpfpDetails" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
|
||||
<app-active-acceleration-box [tx]="tx" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
@@ -678,9 +676,9 @@
|
||||
<td class="td-width" i18n="block.miner">Miner</td>
|
||||
@if (pool) {
|
||||
<td class="wrap-cell">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||
{{ pool.name }}
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge mr-1"
|
||||
[class]="pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||
{{ pool.name }}
|
||||
</a>
|
||||
</td>
|
||||
} @else {
|
||||
|
||||
@@ -297,6 +297,7 @@
|
||||
}
|
||||
|
||||
.etaDeepMempool {
|
||||
display: flex !important;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
@@ -323,12 +324,4 @@
|
||||
.goggles-icon {
|
||||
display: block;
|
||||
width: 2.7em;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,7 @@ import {
|
||||
tap,
|
||||
map,
|
||||
retry,
|
||||
startWith,
|
||||
repeat,
|
||||
take
|
||||
startWith
|
||||
} from 'rxjs/operators';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { of, merge, Subscription, Observable, Subject, from, throwError, combineLatest, BehaviorSubject } from 'rxjs';
|
||||
@@ -44,7 +42,7 @@ interface Pool {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface TxAuditStatus {
|
||||
interface AuditStatus {
|
||||
seen?: boolean;
|
||||
expected?: boolean;
|
||||
added?: boolean;
|
||||
@@ -78,7 +76,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
transactionTime = -1;
|
||||
subscription: Subscription;
|
||||
fetchCpfpSubscription: Subscription;
|
||||
transactionTimesSubscription: Subscription;
|
||||
fetchRbfSubscription: Subscription;
|
||||
fetchCachedTxSubscription: Subscription;
|
||||
fetchAccelerationSubscription: Subscription;
|
||||
@@ -91,7 +88,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
blocksSubscription: Subscription;
|
||||
miningSubscription: Subscription;
|
||||
auditSubscription: Subscription;
|
||||
txConfirmedSubscription: Subscription;
|
||||
currencyChangeSubscription: Subscription;
|
||||
fragmentParams: URLSearchParams;
|
||||
rbfTransaction: undefined | Transaction;
|
||||
@@ -104,13 +100,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
sigops: number | null;
|
||||
adjustedVsize: number | null;
|
||||
pool: Pool | null;
|
||||
auditStatus: TxAuditStatus | null;
|
||||
auditStatus: AuditStatus | null;
|
||||
isAcceleration: boolean = false;
|
||||
filters: Filter[] = [];
|
||||
showCpfpDetails = false;
|
||||
miningStats: MiningStats;
|
||||
fetchCpfp$ = new Subject<string>();
|
||||
transactionTimes$ = new Subject<string>();
|
||||
fetchRbfHistory$ = new Subject<string>();
|
||||
fetchCachedTx$ = new Subject<string>();
|
||||
fetchAcceleration$ = new Subject<number>();
|
||||
@@ -137,8 +132,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
tooltipPosition: { x: number, y: number };
|
||||
isMobile: boolean;
|
||||
firstLoad = true;
|
||||
waitingForAccelerationInfo: boolean = false;
|
||||
isLoadingFirstSeen = false;
|
||||
|
||||
featuresEnabled: boolean;
|
||||
segwitEnabled: boolean;
|
||||
@@ -146,7 +139,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
taprootEnabled: boolean;
|
||||
hasEffectiveFeeRate: boolean;
|
||||
accelerateCtaType: 'alert' | 'button' = 'button';
|
||||
acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR_BUTTON && this.stateService.network === '';
|
||||
acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR && this.stateService.network === '';
|
||||
eligibleForAcceleration: boolean = false;
|
||||
forceAccelerationSummary = false;
|
||||
hideAccelerationSummary = false;
|
||||
@@ -200,7 +193,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.stateService.networkChanged$.subscribe(
|
||||
(network) => {
|
||||
this.network = network;
|
||||
this.acceleratorAvailable = this.stateService.env.ACCELERATOR_BUTTON && this.stateService.network === '';
|
||||
this.acceleratorAvailable = this.stateService.env.ACCELERATOR && this.stateService.network === '';
|
||||
}
|
||||
);
|
||||
|
||||
@@ -230,25 +223,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.latestBlock = blocks[0];
|
||||
});
|
||||
|
||||
this.transactionTimesSubscription = this.transactionTimes$.pipe(
|
||||
tap(() => {
|
||||
this.isLoadingFirstSeen = true;
|
||||
}),
|
||||
switchMap((txid) => this.apiService.getTransactionTimes$([txid]).pipe(
|
||||
retry({ count: 2, delay: 2000 }),
|
||||
// Try again until we either get a valid response, or the transaction is confirmed
|
||||
repeat({ delay: 2000 }),
|
||||
filter((transactionTimes) => transactionTimes?.length && transactionTimes[0] > 0 && !this.tx.status?.confirmed),
|
||||
take(1),
|
||||
)),
|
||||
)
|
||||
.subscribe((transactionTimes) => {
|
||||
this.isLoadingFirstSeen = false;
|
||||
if (transactionTimes?.length && transactionTimes[0]) {
|
||||
this.transactionTime = transactionTimes[0];
|
||||
}
|
||||
});
|
||||
|
||||
this.fetchCpfpSubscription = this.fetchCpfp$
|
||||
.pipe(
|
||||
switchMap((txId) =>
|
||||
@@ -343,19 +317,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.setIsAccelerated();
|
||||
}),
|
||||
switchMap((blockHeight: number) => {
|
||||
return this.servicesApiService.getAccelerationHistory$({ blockHeight }).pipe(
|
||||
switchMap((accelerationHistory: Acceleration[]) => {
|
||||
if (this.tx.acceleration && !accelerationHistory.length) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry
|
||||
return throwError('retry');
|
||||
}
|
||||
return of(accelerationHistory);
|
||||
}),
|
||||
retry({ count: 3, delay: 2000 }),
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
})
|
||||
);
|
||||
return this.servicesApiService.getAccelerationHistory$({ blockHeight });
|
||||
}),
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
})
|
||||
).subscribe((accelerationHistory) => {
|
||||
for (const acceleration of accelerationHistory) {
|
||||
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
|
||||
@@ -364,14 +330,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
acceleration.boost = boostCost;
|
||||
this.tx.acceleratedAt = acceleration.added;
|
||||
this.accelerationInfo = acceleration;
|
||||
this.waitingForAccelerationInfo = false;
|
||||
this.setIsAccelerated();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.miningSubscription = this.fetchMiningInfo$.pipe(
|
||||
filter((target) => target.txid === this.txId && !this.pool),
|
||||
filter((target) => target.txid === this.txId),
|
||||
tap(() => {
|
||||
this.pool = null;
|
||||
}),
|
||||
@@ -399,41 +364,33 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const auditAvailable = this.isAuditAvailable(height);
|
||||
const isCoinbase = this.tx.vin.some(v => v.is_coinbase);
|
||||
const fetchAudit = auditAvailable && !isCoinbase;
|
||||
if (fetchAudit) {
|
||||
// If block audit is already cached, use it to get transaction audit
|
||||
const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash);
|
||||
if (blockAuditLoaded) {
|
||||
return this.apiService.getBlockAudit$(hash).pipe(
|
||||
map(audit => {
|
||||
const isAdded = audit.addedTxs.includes(txid);
|
||||
const isPrioritized = audit.prioritizedTxs.includes(txid);
|
||||
const isAccelerated = audit.acceleratedTxs.includes(txid);
|
||||
const isConflict = audit.fullrbfTxs.includes(txid);
|
||||
const isExpected = audit.template.some(tx => tx.txid === txid);
|
||||
const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
|
||||
return {
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
expected: isExpected,
|
||||
added: isAdded,
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
firstSeen,
|
||||
};
|
||||
})
|
||||
)
|
||||
} else {
|
||||
return this.apiService.getBlockTxAudit$(hash, txid).pipe(
|
||||
retry({ count: 3, delay: 2000 }),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return of(isCoinbase ? { coinbase: true } : null);
|
||||
}
|
||||
return fetchAudit ? this.apiService.getBlockAudit$(hash).pipe(
|
||||
map(audit => {
|
||||
const isAdded = audit.addedTxs.includes(txid);
|
||||
const isPrioritized = audit.prioritizedTxs.includes(txid);
|
||||
const isAccelerated = audit.acceleratedTxs.includes(txid);
|
||||
const isConflict = audit.fullrbfTxs.includes(txid);
|
||||
const isExpected = audit.template.some(tx => tx.txid === txid);
|
||||
const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
|
||||
return {
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
expected: isExpected,
|
||||
added: isAdded,
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
firstSeen,
|
||||
};
|
||||
}),
|
||||
retry({ count: 3, delay: 2000 }),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
) : of(isCoinbase ? { coinbase: true } : null);
|
||||
}),
|
||||
catchError((e) => {
|
||||
return of(null);
|
||||
})
|
||||
).subscribe(auditStatus => {
|
||||
this.auditStatus = auditStatus;
|
||||
if (this.auditStatus?.firstSeen) {
|
||||
@@ -518,13 +475,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (window.innerWidth <= 767.98) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
|
||||
queryParamsHandling: 'merge',
|
||||
preserveFragment: true,
|
||||
queryParams: { mode: 'details' },
|
||||
});
|
||||
}
|
||||
this.seoService.setTitle(
|
||||
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
||||
);
|
||||
@@ -603,7 +553,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (tx.firstSeen) {
|
||||
this.transactionTime = tx.firstSeen;
|
||||
} else {
|
||||
this.transactionTimes$.next(tx.txid);
|
||||
this.getTransactionTime();
|
||||
}
|
||||
} else {
|
||||
this.fetchAcceleration$.next(tx.status.block_height);
|
||||
@@ -629,8 +579,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
ancestors: tx.ancestors,
|
||||
bestDescendant: tx.bestDescendant,
|
||||
});
|
||||
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant || tx.descendants);
|
||||
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && tx.effectiveFeePerVsize !== (this.tx.fee / (this.tx.weight / 4)) && tx.effectiveFeePerVsize !== (tx.fee / Math.ceil(tx.weight / 4)));
|
||||
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant);
|
||||
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
|
||||
} else {
|
||||
this.fetchCpfp$.next(this.tx.txid);
|
||||
}
|
||||
@@ -656,18 +606,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
);
|
||||
|
||||
this.txConfirmedSubscription = this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
|
||||
this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
|
||||
if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
|
||||
if (this.tx.acceleration) {
|
||||
this.waitingForAccelerationInfo = true;
|
||||
}
|
||||
this.tx.status = {
|
||||
confirmed: true,
|
||||
block_height: block.height,
|
||||
block_hash: block.id,
|
||||
block_time: block.timestamp,
|
||||
};
|
||||
this.pool = block.extras.pool;
|
||||
this.txChanged$.next(true);
|
||||
this.stateService.markBlock$.next({ blockHeight: block.height });
|
||||
if (this.tx.acceleration || (this.accelerationInfo && ['accelerating', 'completed_provisional', 'completed'].includes(this.accelerationInfo.status))) {
|
||||
@@ -760,7 +706,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.accelerationPositions,
|
||||
);
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
@@ -778,6 +724,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
document.location.hash = '#accelerate';
|
||||
this.enterpriseService.goal(8);
|
||||
this.openAccelerator();
|
||||
this.scrollIntoAccelPreview = true;
|
||||
return false;
|
||||
@@ -794,6 +741,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
getTransactionTime() {
|
||||
this.apiService
|
||||
.getTransactionTimes$([this.tx.txid])
|
||||
.subscribe((transactionTimes) => {
|
||||
if (transactionTimes?.length && transactionTimes[0]) {
|
||||
this.transactionTime = transactionTimes[0];
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.getTransactionTime();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setCpfpInfo(cpfpInfo: CpfpInfo): void {
|
||||
if (!cpfpInfo || !this.tx) {
|
||||
this.cpfpInfo = null;
|
||||
@@ -823,7 +784,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.tx.acceleration = cpfpInfo.acceleration;
|
||||
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
||||
this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
|
||||
this.tx.feeDelta = cpfpInfo.feeDelta;
|
||||
this.setIsAccelerated(firstCpfp);
|
||||
}
|
||||
|
||||
@@ -838,12 +798,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.sigops = this.cpfpInfo.sigops;
|
||||
this.adjustedVsize = this.cpfpInfo.adjustedVsize;
|
||||
}
|
||||
this.hasCpfp =!!(this.cpfpInfo && relatives.length);
|
||||
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && this.tx.effectiveFeePerVsize !== (this.tx.fee / (this.tx.weight / 4)) && this.tx.effectiveFeePerVsize !== (this.tx.fee / Math.ceil(this.tx.weight / 4)));
|
||||
this.hasCpfp =!!(this.cpfpInfo && (this.cpfpInfo.bestDescendant || this.cpfpInfo.descendants?.length || this.cpfpInfo.ancestors?.length));
|
||||
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
|
||||
}
|
||||
|
||||
setIsAccelerated(initialState: boolean = false) {
|
||||
this.isAcceleration = ((this.tx.acceleration && (!this.tx.status.confirmed || this.waitingForAccelerationInfo)) || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
|
||||
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
|
||||
if (this.isAcceleration) {
|
||||
if (initialState) {
|
||||
this.accelerationFlowCompleted = true;
|
||||
@@ -851,7 +811,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
if (this.isAcceleration) {
|
||||
// this immediately returns cached stats if we fetched them recently
|
||||
this.miningService.getMiningStats('1m').subscribe(stats => {
|
||||
this.miningService.getMiningStats('1w').subscribe(stats => {
|
||||
this.miningStats = stats;
|
||||
this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable
|
||||
});
|
||||
@@ -1067,7 +1027,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
this.fetchCpfpSubscription.unsubscribe();
|
||||
this.transactionTimesSubscription.unsubscribe();
|
||||
this.fetchRbfSubscription.unsubscribe();
|
||||
this.fetchCachedTxSubscription.unsubscribe();
|
||||
this.fetchAccelerationSubscription.unsubscribe();
|
||||
@@ -1081,7 +1040,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.blocksSubscription.unsubscribe();
|
||||
this.miningSubscription?.unsubscribe();
|
||||
this.auditSubscription?.unsubscribe();
|
||||
this.txConfirmedSubscription?.unsubscribe();
|
||||
this.currencyChangeSubscription?.unsubscribe();
|
||||
this.leaveTransaction();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<app-truncate [text]="tx.txid"></app-truncate>
|
||||
</a>
|
||||
<div>
|
||||
<ng-template [ngIf]="tx.status.confirmed">‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}</ng-template>
|
||||
<ng-template [ngIf]="tx.status.confirmed">‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</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>
|
||||
|
||||
@@ -8993,7 +8993,7 @@ export const restApiDocsData = [
|
||||
fragment: "accelerator-estimate",
|
||||
title: "POST Calculate Estimated Costs",
|
||||
description: {
|
||||
default: "<p>Returns estimated costs to accelerate a transaction. Optionally set the <code>X-Mempool-Auth</code> header to get customized estimation.</p>"
|
||||
default: "<p>Returns estimated costs to accelerate a transaction. Optionally set the <code>api_key</code> header to get customized estimation.</p>"
|
||||
},
|
||||
urlString: "/v1/services/accelerator/estimate",
|
||||
showConditions: [""],
|
||||
@@ -9009,7 +9009,7 @@ export const restApiDocsData = [
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: ["txInput=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29"],
|
||||
headers: "X-Mempool-Auth: stacksats",
|
||||
headers: "api_key: stacksats",
|
||||
response: `{
|
||||
"txSummary": {
|
||||
"txid": "ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29",
|
||||
@@ -9017,89 +9017,18 @@ export const restApiDocsData = [
|
||||
"effectiveFee": 154,
|
||||
"ancestorCount": 1
|
||||
},
|
||||
"cost": 1386,
|
||||
"targetFeeRate": 10,
|
||||
"nextBlockFee": 1540,
|
||||
"userBalance": 0,
|
||||
"mempoolBaseFee": 50000,
|
||||
"vsizeFee": 0,
|
||||
"pools": [
|
||||
111,
|
||||
102,
|
||||
112,
|
||||
142,
|
||||
115
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"fee": 1500
|
||||
},
|
||||
{
|
||||
"fee": 3000
|
||||
},
|
||||
{
|
||||
"fee": 12500
|
||||
}
|
||||
],
|
||||
"hasAccess": false,
|
||||
"availablePaymentMethods": {
|
||||
"bitcoin": {
|
||||
"enabled": true,
|
||||
"min": 1000,
|
||||
"max": 10000000
|
||||
},
|
||||
"cashapp": {
|
||||
"enabled": true,
|
||||
"min": 10,
|
||||
"max": 200
|
||||
}
|
||||
},
|
||||
"unavailable": false
|
||||
"cost": 3850,
|
||||
"targetFeeRate": 26,
|
||||
"nextBlockFee": 4004,
|
||||
"userBalance": 99900000,
|
||||
"mempoolBaseFee": 40000,
|
||||
"vsizeFee": 50000,
|
||||
"hasAccess": true
|
||||
}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
options: { officialOnly: true },
|
||||
type: "endpoint",
|
||||
category: "accelerator-public",
|
||||
httpRequestMethod: "POST",
|
||||
fragment: "accelerator-get-invoice",
|
||||
title: "POST Generate Acceleration Invoice",
|
||||
description: {
|
||||
default: "<p>Request a LN invoice to accelerate a transaction.</p>"
|
||||
},
|
||||
urlString: "/v1/services/payments/bitcoin",
|
||||
showConditions: [""],
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
codeTemplate: {
|
||||
curl: `%{1}" "[[hostname]][[baseNetworkUrl]]/api/v1/services/payments/bitcoin`, //custom interpolation technique handled in replaceCurlPlaceholder()
|
||||
commonJS: ``,
|
||||
esModule: ``
|
||||
},
|
||||
codeSampleMainnet: {
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: ["product=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29&amount=12500"],
|
||||
headers: "",
|
||||
response: `[
|
||||
{
|
||||
"btcpayInvoiceId": "4Ww53d7VgSa596jmCFufe7",
|
||||
"btcDue": "0.000625",
|
||||
"addresses": {
|
||||
"BTC": "bc1qcvqx2kr5mktd7gvym0atrrx0sn27mwv5kkghl3m78kegndm5t8ksvcqpja",
|
||||
"BTC_LNURLPAY": null,
|
||||
"BTC_LightningLike": "lnbc625u1pngl0wzpp56j7cqghsw2y5q7vdu9shmpxgpzsx4pqra4wcm9vdnvqegutplk2qdxj2pskjepqw3hjqnt9d4cx7mmvypqkxcm9d3jhyct5daezq2z0wfjx2u3qf9zr5grpvd3k2mr9wfshg6t0dckk2ef3xdjkyc3e8ymrxv3nxumkxvf4vvungwfcxqen2dmxxcmngepj8q6kzce5xyengdfjxq6nqvpnx9jkzdnyvdjrxefevgexgcej8yknzdejxqmrjd3jx5mrgdpj9ycqzpuxqrpr5sp58593dzj2uauaj3afa7x47qeam8k9yyqrh9qasj2ssdzstew6qv3q9qxpqysgqj8qshfkxmj0gfkly5xfydysvsx55uhnc6fgpw66uf6hl8leu07454axe2kq0q788yysg8guel2r36d6f75546nkhmdcmec4mmlft8dsq62rnsj"
|
||||
}
|
||||
}
|
||||
]`,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
options: { officialOnly: true },
|
||||
type: "endpoint",
|
||||
@@ -9190,18 +9119,14 @@ export const restApiDocsData = [
|
||||
"txid": "d7e1796d8eb4a09d4e6c174e36cfd852f1e6e6c9f7df4496339933cd32cbdd1d",
|
||||
"status": "completed",
|
||||
"added": 1707421053,
|
||||
"lastUpdated": 1719134667,
|
||||
"lastUpdated": 1707422952,
|
||||
"effectiveFee": 146,
|
||||
"effectiveVsize": 141,
|
||||
"feeDelta": 14000,
|
||||
"blockHash": "00000000000000000000482f0746d62141694b9210a813b97eb8445780a32003",
|
||||
"blockHeight": 829559,
|
||||
"bidBoost": 3239,
|
||||
"boostVersion": "v1",
|
||||
"pools": [
|
||||
111
|
||||
],
|
||||
"minedByPoolUniqueId": 111
|
||||
"bidBoost": 6102,
|
||||
"pools": [111]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
@@ -9240,7 +9165,7 @@ export const restApiDocsData = [
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: [],
|
||||
headers: "X-Mempool-Auth: stacksats",
|
||||
headers: "api_key: stacksats",
|
||||
response: `[
|
||||
{
|
||||
"type": "Bitcoin",
|
||||
@@ -9288,7 +9213,7 @@ export const restApiDocsData = [
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: [],
|
||||
headers: "X-Mempool-Auth: stacksats",
|
||||
headers: "api_key: stacksats",
|
||||
response: `{
|
||||
"balance": 99900000,
|
||||
"hold": 101829,
|
||||
@@ -9304,7 +9229,7 @@ export const restApiDocsData = [
|
||||
category: "accelerator-private",
|
||||
httpRequestMethod: "POST",
|
||||
fragment: "accelerator-accelerate",
|
||||
title: "POST Accelerate A Transaction (Pro)",
|
||||
title: "POST Accelerate A Transaction",
|
||||
description: {
|
||||
default: "<p>Sends a request to accelerate a transaction.</p>"
|
||||
},
|
||||
@@ -9322,7 +9247,7 @@ export const restApiDocsData = [
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: ["txInput=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29&userBid=21000000"],
|
||||
headers: "X-Mempool-Auth: stacksats",
|
||||
headers: "api_key: stacksats",
|
||||
response: `HTTP/1.1 200 OK`,
|
||||
},
|
||||
}
|
||||
@@ -9352,7 +9277,7 @@ export const restApiDocsData = [
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: [],
|
||||
headers: "X-Mempool-Auth: stacksats",
|
||||
headers: "api_key: stacksats",
|
||||
response: `[
|
||||
{
|
||||
"id": 89,
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="how-to-get-transaction-confirmed-quickly">
|
||||
<p>To get your transaction confirmed quicker, you will need to increase its effective feerate.</p><p>If your transaction was created with RBF enabled, your stuck transaction can simply be replaced with a new one that has a higher fee. Otherwise, if you control any of the stuck transaction's outputs, you can use CPFP to increase your stuck transaction's effective feerate.</p><p>If you are not sure how to do RBF or CPFP, work with the tool you used to make the transaction (wallet software, exchange company, etc).</p><p>Another option to get your transaction confirmed more quickly is <a [href]="[ isMempoolSpaceBuild ? '/accelerator' : 'https://mempool.space/accelerator']" [target]="isMempoolSpaceBuild ? '' : 'blank'">Mempool Accelerator™</a>.</p>
|
||||
<p>To get your transaction confirmed quicker, you will need to increase its effective feerate.</p><p>If your transaction was created with RBF enabled, your stuck transaction can simply be replaced with a new one that has a higher fee. Otherwise, if you control any of the stuck transaction's outputs, you can use CPFP to increase your stuck transaction's effective feerate.</p><p>If you are not sure how to do RBF or CPFP, work with the tool you used to make the transaction (wallet software, exchange company, etc).</p><p *ngIf="officialMempoolInstance">Another option to get your transaction confirmed more quickly is Mempool Accelerator™. This service is still in development, but you can <a href="https://mempool.space/accelerator">sign up for the waitlist</a> to be notified when it's ready.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="how-prevent-stuck-transaction">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user