Fix db version conflicts
This commit is contained in:
		
						commit
						847b90f167
					
				
							
								
								
									
										1
									
								
								.github/workflows/on-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/on-tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -100,5 +100,6 @@ jobs: | |||||||
|           --cache-to "type=local,dest=/tmp/.buildx-cache" \ |           --cache-to "type=local,dest=/tmp/.buildx-cache" \ | ||||||
|           --platform linux/amd64,linux/arm64 \ |           --platform linux/amd64,linux/arm64 \ | ||||||
|           --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ |           --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ | ||||||
|  |           --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ | ||||||
|           --output "type=registry" ./${{ matrix.service }}/ \ |           --output "type=registry" ./${{ matrix.service }}/ \ | ||||||
|           --build-arg commitHash=$SHORT_SHA |           --build-arg commitHash=$SHORT_SHA | ||||||
|  | |||||||
							
								
								
									
										738
									
								
								backend/src/api/acceleration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										738
									
								
								backend/src/api/acceleration.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,738 @@ | |||||||
|  | import logger from '../logger'; | ||||||
|  | import { MempoolTransactionExtended } from '../mempool.interfaces'; | ||||||
|  | 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; | ||||||
|  | 
 | ||||||
|  | type Acceleration = { | ||||||
|  |   txid: string; | ||||||
|  |   max_bid: number; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | interface TxSummary { | ||||||
|  |   txid: string; // txid of the current transaction
 | ||||||
|  |   effectiveVsize: number; // Total vsize of the dependency tree
 | ||||||
|  |   effectiveFee: number;  // Total fee of the dependency tree in sats
 | ||||||
|  |   ancestorCount: number; // Number of ancestors
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface AccelerationInfo { | ||||||
|  |   txSummary: TxSummary; | ||||||
|  |   targetFeeRate: number; // target fee rate (recommended next block fee, or median fee for mined block)
 | ||||||
|  |   nextBlockFee: number; // fee in sats required to be in the next block (using recommended next block fee, or median fee for mined block)
 | ||||||
|  |   cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface GraphTx { | ||||||
|  |   txid: string; | ||||||
|  |   vsize: number; | ||||||
|  |   weight: number; | ||||||
|  |   fees: { | ||||||
|  |     base: number; | ||||||
|  |   }; | ||||||
|  |   depends: string[]; | ||||||
|  |   spentby: string[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface MempoolTx extends GraphTx { | ||||||
|  |   ancestorcount: number; | ||||||
|  |   ancestorsize: number; | ||||||
|  |   fees: { | ||||||
|  |     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 | ||||||
|  |    * Returns the "fair" boost rate to charge accelerations | ||||||
|  |    * | ||||||
|  |    * @param accelerationsx | ||||||
|  |    * @param verboseBlock | ||||||
|  |    */ | ||||||
|  |   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); | ||||||
|  | 
 | ||||||
|  |     // initialize working maps for fast tx lookups
 | ||||||
|  |     const accMap = {}; | ||||||
|  |     const txMap = {}; | ||||||
|  |     for (const acceleration of accelerations) { | ||||||
|  |       accMap[acceleration.txid] = acceleration; | ||||||
|  |     } | ||||||
|  |     for (const tx of template) { | ||||||
|  |       txMap[tx.txid] = tx; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Identify and exclude accelerated and otherwise prioritized transactions
 | ||||||
|  |     const excludeMap = {}; | ||||||
|  |     let totalWeight = 0; | ||||||
|  |     let minAcceleratedPackage = Infinity; | ||||||
|  |     let lastEffectiveRate = 0; | ||||||
|  |     // Iterate over the mined template from bottom to top.
 | ||||||
|  |     // Transactions should appear in ascending order of mining priority.
 | ||||||
|  |     for (const blockTx of [...blockTxs].reverse()) { | ||||||
|  |       const txid = blockTx.txid; | ||||||
|  |       const tx = txMap[txid]; | ||||||
|  |       totalWeight += tx.weight; | ||||||
|  |       const isAccelerated = accMap[txid] != null; | ||||||
|  |       // If a cluster has a in-band effective fee rate than the previous cluster,
 | ||||||
|  |       // it must have been prioritized out-of-band (in order to have a higher mining priority)
 | ||||||
|  |       // so exclude from the analysis.
 | ||||||
|  |       const isPrioritized = tx.effectiveFeePerVsize < lastEffectiveRate; | ||||||
|  |       if (isPrioritized || isAccelerated) { | ||||||
|  |         let packageWeight = 0; | ||||||
|  |         // exclude this whole CPFP cluster
 | ||||||
|  |         for (const clusterTxid of tx.cluster) { | ||||||
|  |           packageWeight += txMap[clusterTxid].weight; | ||||||
|  |           if (!excludeMap[clusterTxid]) { | ||||||
|  |             excludeMap[clusterTxid] = true; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         // keep track of the smallest accelerated CPFP cluster for later
 | ||||||
|  |         if (isAccelerated) { | ||||||
|  |           minAcceleratedPackage = Math.min(minAcceleratedPackage, packageWeight); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (!isPrioritized) { | ||||||
|  |         if (!isAccelerated || !lastEffectiveRate) { | ||||||
|  |           lastEffectiveRate = tx.effectiveFeePerVsize; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // The Bid Boost Rate is calculated by disregarding the bottom X weight units of the block,
 | ||||||
|  |     // where X is the larger of BID_BOOST_MIN_OFFSET or the smallest accelerated package weight (the "offset"),
 | ||||||
|  |     // then taking the average fee rate of the following BID_BOOST_WINDOW weight units
 | ||||||
|  |     // (ignoring accelerated transactions and their ancestors).
 | ||||||
|  |     //
 | ||||||
|  |     // Transactions within the offset might pay less than the fair rate due to bin-packing effects
 | ||||||
|  |     // But the average rate paid by the next chunk of non-accelerated transactions provides a good
 | ||||||
|  |     // upper bound on the "next best rate" of alternatives to including the accelerated transactions
 | ||||||
|  |     // (since, if there were any better options, they would have been included instead)
 | ||||||
|  |     const spareWeight = BLOCK_WEIGHT_UNITS - totalWeight; | ||||||
|  |     const windowOffset = Math.min(Math.max(minAcceleratedPackage, BID_BOOST_MIN_OFFSET, spareWeight), BID_BOOST_MAX_OFFSET); | ||||||
|  |     const leftBound = windowOffset; | ||||||
|  |     const rightBound = windowOffset + BID_BOOST_WINDOW; | ||||||
|  |     let totalFeeInWindow = 0; | ||||||
|  |     let totalWeightInWindow = Math.max(0, spareWeight - leftBound); | ||||||
|  |     let txIndex = blockTxs.length - 1; | ||||||
|  |     for (let offset = spareWeight; offset < BLOCK_WEIGHT_UNITS && txIndex >= 0; txIndex--) { | ||||||
|  |       const txid = blockTxs[txIndex].txid; | ||||||
|  |       const tx = txMap[txid]; | ||||||
|  |       if (excludeMap[txid]) { | ||||||
|  |         // skip prioritized transactions and their ancestors
 | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const left = offset; | ||||||
|  |       const right = offset + tx.weight; | ||||||
|  |       offset += tx.weight; | ||||||
|  |       if (right < leftBound) { | ||||||
|  |         // not within window yet
 | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |       if (left > rightBound) { | ||||||
|  |         // past window
 | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |       // count fees for weight units within the window
 | ||||||
|  |       const overlapLeft = Math.max(leftBound, left); | ||||||
|  |       const overlapRight = Math.min(rightBound, right); | ||||||
|  |       const overlapUnits = overlapRight - overlapLeft; | ||||||
|  |       totalFeeInWindow += (tx.effectiveFeePerVsize * (overlapUnits / 4)); | ||||||
|  |       totalWeightInWindow += overlapUnits; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (totalWeightInWindow < BID_BOOST_WINDOW) { | ||||||
|  |       // not enough un-prioritized transactions to calculate a fair rate
 | ||||||
|  |       // just charge everyone their max bids
 | ||||||
|  |       return Infinity; | ||||||
|  |     } | ||||||
|  |     // Divide the total fee by the size of the BID_BOOST_WINDOW in vbytes
 | ||||||
|  |     const averageRate = totalFeeInWindow / (BID_BOOST_WINDOW / 4); | ||||||
|  |     return averageRate; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * 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  | ||||||
|  |    */ | ||||||
|  |   public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo { | ||||||
|  |     // Get same-block transaction ancestors
 | ||||||
|  |     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: tx.vsize, | ||||||
|  |       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: tx.vsize, | ||||||
|  |       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: MempoolTx, relatives: Map<string, MempoolTx>, targetFeeRate: number): AccelerationInfo { | ||||||
|  |     // add root tx to the ancestor map
 | ||||||
|  |     relatives.set(tx.txid, tx); | ||||||
|  | 
 | ||||||
|  |     // Check for high-sigop transactions (not supported)
 | ||||||
|  |     relatives.forEach(entry => { | ||||||
|  |       if (entry.vsize > Math.ceil(entry.weight / 4)) { | ||||||
|  |         throw new Error(`high_sigop_tx`); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Initialize individual & ancestor fee rates
 | ||||||
|  |     relatives.forEach(entry => this.setAncestorScores(entry)); | ||||||
|  | 
 | ||||||
|  |     // Sort by descending ancestor score
 | ||||||
|  |     let sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator); | ||||||
|  | 
 | ||||||
|  |     let includedInCluster: Map<string, MempoolTx> | null = null; | ||||||
|  | 
 | ||||||
|  |     // While highest score >= targetFeeRate
 | ||||||
|  |     let maxIterations = MAX_RELATIVE_GRAPH_SIZE; | ||||||
|  |     while (sortedRelatives.length && sortedRelatives[0].score && sortedRelatives[0].score >= targetFeeRate && maxIterations > 0) { | ||||||
|  |       maxIterations--; | ||||||
|  |       // Grab the highest scoring entry
 | ||||||
|  |       const best = sortedRelatives.shift(); | ||||||
|  |       if (best) { | ||||||
|  |         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
 | ||||||
|  |         this.removeAncestors(cluster, relatives); | ||||||
|  | 
 | ||||||
|  |         // re-sort
 | ||||||
|  |         sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // sanity check for infinite loops / too many ancestors (should never happen)
 | ||||||
|  |     if (maxIterations <= 0) { | ||||||
|  |       logger.warn(`acceleration dependency calculation failed: calculateAccelerationAncestors loop exceeded ${MAX_RELATIVE_GRAPH_SIZE} iterations, unable to proceed`); | ||||||
|  |       throw new Error('invalid_tx_dependencies'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let totalFee = Math.round(tx.fees.ancestor * 100_000_000); | ||||||
|  | 
 | ||||||
|  |     // transaction is already CPFP-d above the target rate by some descendant
 | ||||||
|  |     if (includedInCluster) { | ||||||
|  |       let clusterSize = 0; | ||||||
|  |       let clusterFee = 0; | ||||||
|  |       includedInCluster.forEach(entry => { | ||||||
|  |         clusterSize += entry.vsize; | ||||||
|  |         clusterFee += (entry.fees.base * 100_000_000); | ||||||
|  |       }); | ||||||
|  |       const clusterRate = clusterFee / clusterSize; | ||||||
|  |       totalFee = Math.ceil(tx.ancestorsize * clusterRate); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Whatever remains in the accelerated tx's dependencies needs to be boosted to the targetFeeRate
 | ||||||
|  |     // Cost = (totalVsize * targetFeeRate) - totalFee
 | ||||||
|  |     return { | ||||||
|  |       txSummary: { | ||||||
|  |         txid: tx.txid, | ||||||
|  |         effectiveVsize: tx.ancestorsize, | ||||||
|  |         effectiveFee: totalFee, | ||||||
|  |         ancestorCount: tx.ancestorcount, | ||||||
|  |       }, | ||||||
|  |       cost: Math.max(0, Math.ceil(tx.ancestorsize * targetFeeRate) - totalFee), | ||||||
|  |       targetFeeRate, | ||||||
|  |       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 * 100_000_000) / tx.vsize; | ||||||
|  |     tx.ancestorRate = (tx.fees.ancestor * 100_000_000) / 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; | ||||||
|  | 
 | ||||||
|  | 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)
 | ||||||
|  | */ | ||||||
|  | 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 | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @ -7,6 +7,24 @@ import { isIP } from 'net'; | |||||||
| import transactionUtils from './transaction-utils'; | import transactionUtils from './transaction-utils'; | ||||||
| import { isPoint } from '../utils/secp256k1'; | import { isPoint } from '../utils/secp256k1'; | ||||||
| import logger from '../logger'; | import logger from '../logger'; | ||||||
|  | import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script'; | ||||||
|  | 
 | ||||||
|  | // Bitcoin Core default policy settings
 | ||||||
|  | const TX_MAX_STANDARD_VERSION = 2; | ||||||
|  | const MAX_STANDARD_TX_WEIGHT = 400_000; | ||||||
|  | const MAX_BLOCK_SIGOPS_COST = 80_000; | ||||||
|  | const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); | ||||||
|  | const MIN_STANDARD_TX_NONWITNESS_SIZE = 65; | ||||||
|  | const MAX_P2SH_SIGOPS = 15; | ||||||
|  | const MAX_STANDARD_P2WSH_STACK_ITEMS = 100; | ||||||
|  | const MAX_STANDARD_P2WSH_STACK_ITEM_SIZE = 80; | ||||||
|  | const MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE = 80; | ||||||
|  | const MAX_STANDARD_P2WSH_SCRIPT_SIZE = 3600; | ||||||
|  | const MAX_STANDARD_SCRIPTSIG_SIZE = 1650; | ||||||
|  | const DUST_RELAY_TX_FEE = 3; | ||||||
|  | const MAX_OP_RETURN_RELAY = 83; | ||||||
|  | const DEFAULT_PERMIT_BAREMULTISIG = true; | ||||||
|  | 
 | ||||||
| export class Common { | export class Common { | ||||||
|   static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ? |   static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ? | ||||||
|     '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49' |     '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49' | ||||||
| @ -177,6 +195,141 @@ export class Common { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Validates most standardness rules | ||||||
|  |    * | ||||||
|  |    * returns true early if any standardness rule is violated, otherwise false | ||||||
|  |    * (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced) | ||||||
|  |    */ | ||||||
|  |   static isNonStandard(tx: TransactionExtended): boolean { | ||||||
|  |     // version
 | ||||||
|  |     if (tx.version > TX_MAX_STANDARD_VERSION) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // tx-size
 | ||||||
|  |     if (tx.weight > MAX_STANDARD_TX_WEIGHT) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // tx-size-small
 | ||||||
|  |     if (this.getNonWitnessSize(tx) < MIN_STANDARD_TX_NONWITNESS_SIZE) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // bad-txns-too-many-sigops
 | ||||||
|  |     if (tx.sigops && tx.sigops > MAX_STANDARD_TX_SIGOPS_COST) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // input validation
 | ||||||
|  |     for (const vin of tx.vin) { | ||||||
|  |       if (vin.is_coinbase) { | ||||||
|  |         // standardness rules don't apply to coinbase transactions
 | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       // scriptsig-size
 | ||||||
|  |       if ((vin.scriptsig.length / 2) > MAX_STANDARD_SCRIPTSIG_SIZE) { | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |       // scriptsig-not-pushonly
 | ||||||
|  |       if (vin.scriptsig_asm) { | ||||||
|  |         for (const op of vin.scriptsig_asm.split(' ')) { | ||||||
|  |           if (opcodes[op] && opcodes[op] > opcodes['OP_16']) { | ||||||
|  |             return true; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       // bad-txns-nonstandard-inputs
 | ||||||
|  |       if (vin.prevout?.scriptpubkey_type === 'p2sh') { | ||||||
|  |         // TODO: evaluate script (https://github.com/bitcoin/bitcoin/blob/1ac627c485a43e50a9a49baddce186ee3ad4daad/src/policy/policy.cpp#L177)
 | ||||||
|  |         // countScriptSigops returns the witness-scaled sigops, so divide by 4 before comparison with MAX_P2SH_SIGOPS
 | ||||||
|  |         const sigops = (transactionUtils.countScriptSigops(vin.inner_redeemscript_asm) / 4); | ||||||
|  |         if (sigops > MAX_P2SH_SIGOPS) { | ||||||
|  |           return true; | ||||||
|  |         } | ||||||
|  |       } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |       // TODO: bad-witness-nonstandard
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // output validation
 | ||||||
|  |     let opreturnCount = 0; | ||||||
|  |     for (const vout of tx.vout) { | ||||||
|  |       // scriptpubkey
 | ||||||
|  |       if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) { | ||||||
|  |         // (non-standard output type)
 | ||||||
|  |         return true; | ||||||
|  |       } else if (vout.scriptpubkey_type === 'multisig') { | ||||||
|  |         if (!DEFAULT_PERMIT_BAREMULTISIG) { | ||||||
|  |           // bare-multisig
 | ||||||
|  |           return true; | ||||||
|  |         } | ||||||
|  |         const mOfN = parseMultisigScript(vout.scriptpubkey_asm); | ||||||
|  |         if (!mOfN || mOfN.n < 1 || mOfN.n > 3 || mOfN.m < 1 || mOfN.m > mOfN.n) { | ||||||
|  |           // (non-standard bare multisig threshold)
 | ||||||
|  |           return true; | ||||||
|  |         } | ||||||
|  |       } else if (vout.scriptpubkey_type === 'op_return') { | ||||||
|  |         opreturnCount++; | ||||||
|  |         if ((vout.scriptpubkey.length / 2) > MAX_OP_RETURN_RELAY) { | ||||||
|  |           // over default datacarrier limit
 | ||||||
|  |           return true; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       // dust
 | ||||||
|  |       // (we could probably hardcode this for the different output types...)
 | ||||||
|  |       if (vout.scriptpubkey_type !== 'op_return') { | ||||||
|  |         let dustSize = (vout.scriptpubkey.length / 2); | ||||||
|  |         // add varint length overhead
 | ||||||
|  |         dustSize += getVarIntLength(dustSize); | ||||||
|  |         // add value size
 | ||||||
|  |         dustSize += 8; | ||||||
|  |         if (['v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) { | ||||||
|  |           dustSize += 67; | ||||||
|  |         } else { | ||||||
|  |           dustSize += 148; | ||||||
|  |         } | ||||||
|  |         if (vout.value < (dustSize * DUST_RELAY_TX_FEE)) { | ||||||
|  |           // under minimum output size
 | ||||||
|  |           console.log(`NON-STANDARD | dust | ${vout.value} | ${dustSize} ${dustSize * DUST_RELAY_TX_FEE} `, tx.txid); | ||||||
|  |           return true; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // multi-op-return
 | ||||||
|  |     if (opreturnCount > 1) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // TODO: non-mandatory-script-verify-flag
 | ||||||
|  | 
 | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static getNonWitnessSize(tx: TransactionExtended): number { | ||||||
|  |     let weight = tx.weight; | ||||||
|  |     let hasWitness = false; | ||||||
|  |     for (const vin of tx.vin) { | ||||||
|  |       if (vin.witness?.length) { | ||||||
|  |         hasWitness = true; | ||||||
|  |         // witness count
 | ||||||
|  |         weight -= getVarIntLength(vin.witness.length); | ||||||
|  |         for (const witness of vin.witness) { | ||||||
|  |           // witness item size + content
 | ||||||
|  |           weight -= getVarIntLength(witness.length / 2) + (witness.length / 2); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (hasWitness) { | ||||||
|  |       // marker & segwit flag
 | ||||||
|  |       weight -= 2; | ||||||
|  |     } | ||||||
|  |     return Math.ceil(weight / 4); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   static setSegwitSighashFlags(flags: bigint, witness: string[]): bigint { |   static setSegwitSighashFlags(flags: bigint, witness: string[]): bigint { | ||||||
|     for (const w of witness) { |     for (const w of witness) { | ||||||
|       if (this.isDERSig(w)) { |       if (this.isDERSig(w)) { | ||||||
| @ -351,6 +504,10 @@ export class Common { | |||||||
|       flags |= TransactionFlags.batch_payout; |       flags |= TransactionFlags.batch_payout; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (this.isNonStandard(tx)) { | ||||||
|  |       flags |= TransactionFlags.nonstandard; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     return Number(flags); |     return Number(flags); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository'; | |||||||
| import { RowDataPacket } from 'mysql2'; | import { RowDataPacket } from 'mysql2'; | ||||||
| 
 | 
 | ||||||
| class DatabaseMigration { | class DatabaseMigration { | ||||||
|   private static currentVersion = 69; |   private static currentVersion = 71; | ||||||
|   private queryTimeout = 3600_000; |   private queryTimeout = 3600_000; | ||||||
|   private statisticsAddedIndexed = false; |   private statisticsAddedIndexed = false; | ||||||
|   private uniqueLogs: string[] = []; |   private uniqueLogs: string[] = []; | ||||||
| @ -581,7 +581,17 @@ class DatabaseMigration { | |||||||
|       await this.updateToSchemaVersion(68); |       await this.updateToSchemaVersion(68); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (databaseSchemaVersion < 69 && config.MEMPOOL.NETWORK === "liquid") { |     if (databaseSchemaVersion < 69 && config.MEMPOOL.NETWORK === 'mainnet') { | ||||||
|  |       await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations')); | ||||||
|  |       await this.updateToSchemaVersion(69); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (databaseSchemaVersion < 70 && config.MEMPOOL.NETWORK === 'mainnet') { | ||||||
|  |       await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;'); | ||||||
|  |       await this.updateToSchemaVersion(70); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (databaseSchemaVersion < 71 && config.MEMPOOL.NETWORK === 'liquid') { | ||||||
|       await this.$executeQuery('TRUNCATE TABLE elements_pegs'); |       await this.$executeQuery('TRUNCATE TABLE elements_pegs'); | ||||||
|       await this.$executeQuery('TRUNCATE TABLE federation_txos'); |       await this.$executeQuery('TRUNCATE TABLE federation_txos'); | ||||||
|       await this.$executeQuery('SET FOREIGN_KEY_CHECKS = 0'); |       await this.$executeQuery('SET FOREIGN_KEY_CHECKS = 0'); | ||||||
| @ -594,7 +604,7 @@ class DatabaseMigration { | |||||||
|       await this.$executeQuery('ALTER TABLE `federation_txos` ADD timelock INT NOT NULL DEFAULT 0'); |       await this.$executeQuery('ALTER TABLE `federation_txos` ADD timelock INT NOT NULL DEFAULT 0'); | ||||||
|       await this.$executeQuery('ALTER TABLE `federation_txos` ADD expiredAt INT NOT NULL DEFAULT 0'); |       await this.$executeQuery('ALTER TABLE `federation_txos` ADD expiredAt INT NOT NULL DEFAULT 0'); | ||||||
|       await this.$executeQuery('ALTER TABLE `federation_txos` ADD emergencyKey TINYINT NOT NULL DEFAULT 0'); |       await this.$executeQuery('ALTER TABLE `federation_txos` ADD emergencyKey TINYINT NOT NULL DEFAULT 0'); | ||||||
|       await this.updateToSchemaVersion(69); |       await this.updateToSchemaVersion(71); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -1139,6 +1149,23 @@ class DatabaseMigration { | |||||||
|     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 |     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private getCreateAccelerationsTableQuery(): string { | ||||||
|  |     return `CREATE TABLE IF NOT EXISTS accelerations (
 | ||||||
|  |       txid varchar(65) NOT NULL, | ||||||
|  |       added datetime NOT NULL, | ||||||
|  |       height int(10) NOT NULL, | ||||||
|  |       pool smallint unsigned NULL, | ||||||
|  |       effective_vsize int(10) NOT NULL, | ||||||
|  |       effective_fee bigint(20) unsigned NOT NULL, | ||||||
|  |       boost_rate float unsigned, | ||||||
|  |       boost_cost bigint(20) unsigned NOT NULL, | ||||||
|  |       PRIMARY KEY (txid), | ||||||
|  |       INDEX (added), | ||||||
|  |       INDEX (height), | ||||||
|  |       INDEX (pool) | ||||||
|  |     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   public async $blocksReindexingTruncate(): Promise<void> { |   public async $blocksReindexingTruncate(): Promise<void> { | ||||||
|     logger.warn(`Truncating pools, blocks, hashrates and difficulty_adjustments tables for re-indexing (using '--reindex-blocks'). You can cancel this command within 5 seconds`); |     logger.warn(`Truncating pools, blocks, hashrates and difficulty_adjustments tables for re-indexing (using '--reindex-blocks'). You can cancel this command within 5 seconds`); | ||||||
|     await Common.sleep$(5000); |     await Common.sleep$(5000); | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import HashratesRepository from '../../repositories/HashratesRepository'; | |||||||
| import bitcoinClient from '../bitcoin/bitcoin-client'; | import bitcoinClient from '../bitcoin/bitcoin-client'; | ||||||
| import mining from "./mining"; | import mining from "./mining"; | ||||||
| import PricesRepository from '../../repositories/PricesRepository'; | import PricesRepository from '../../repositories/PricesRepository'; | ||||||
|  | import AccelerationRepository from '../../repositories/AccelerationRepository'; | ||||||
| 
 | 
 | ||||||
| class MiningRoutes { | class MiningRoutes { | ||||||
|   public initRoutes(app: Application) { |   public initRoutes(app: Application) { | ||||||
| @ -34,6 +35,10 @@ class MiningRoutes { | |||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit) |       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp) |       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'historical-price', this.$getHistoricalPrice) |       .get(config.MEMPOOL.API_URL_PREFIX + 'historical-price', this.$getHistoricalPrice) | ||||||
|  | 
 | ||||||
|  |       .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/pool/:slug', this.$getAccelerationsByPool) | ||||||
|  |       .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight) | ||||||
|  |       .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations) | ||||||
|     ; |     ; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -352,6 +357,52 @@ class MiningRoutes { | |||||||
|       res.status(500).send(e instanceof Error ? e.message : e); |       res.status(500).send(e instanceof Error ? e.message : e); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   private async $getAccelerationsByPool(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(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); | ||||||
|  |     } catch (e) { | ||||||
|  |       res.status(500).send(e instanceof Error ? e.message : e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async $getAccelerationsByHeight(req: Request, res: Response): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       res.header('Pragma', 'public'); | ||||||
|  |       res.header('Cache-control', 'public'); | ||||||
|  |       res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); | ||||||
|  |       if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { | ||||||
|  |         res.status(400).send('Acceleration data is not available.'); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); | ||||||
|  |       res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); | ||||||
|  |     } catch (e) { | ||||||
|  |       res.status(500).send(e instanceof Error ? e.message : e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async $getRecentAccelerations(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(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); | ||||||
|  |     } catch (e) { | ||||||
|  |       res.status(500).send(e instanceof Error ? e.message : e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new MiningRoutes(); | export default new MiningRoutes(); | ||||||
|  | |||||||
| @ -145,6 +145,10 @@ class TransactionUtils { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number { |   public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number { | ||||||
|  |     if (!script?.length) { | ||||||
|  |       return 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     let sigops = 0; |     let sigops = 0; | ||||||
|     // count OP_CHECKSIG and OP_CHECKSIGVERIFY
 |     // count OP_CHECKSIG and OP_CHECKSIGVERIFY
 | ||||||
|     sigops += (script.match(/OP_CHECKSIG/g)?.length || 0); |     sigops += (script.match(/OP_CHECKSIG/g)?.length || 0); | ||||||
|  | |||||||
| @ -24,6 +24,8 @@ import { ApiPrice } from '../repositories/PricesRepository'; | |||||||
| import accelerationApi from './services/acceleration'; | import accelerationApi from './services/acceleration'; | ||||||
| import mempool from './mempool'; | import mempool from './mempool'; | ||||||
| import statistics from './statistics/statistics'; | import statistics from './statistics/statistics'; | ||||||
|  | import accelerationCosts from './acceleration'; | ||||||
|  | import accelerationRepository from '../repositories/AccelerationRepository'; | ||||||
| 
 | 
 | ||||||
| interface AddressTransactions { | interface AddressTransactions { | ||||||
|   mempool: MempoolTransactionExtended[], |   mempool: MempoolTransactionExtended[], | ||||||
| @ -728,6 +730,28 @@ class WebsocketHandler { | |||||||
| 
 | 
 | ||||||
|     const _memPool = memPool.getMempool(); |     const _memPool = memPool.getMempool(); | ||||||
| 
 | 
 | ||||||
|  |     const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations())); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     if (isAccelerated) { | ||||||
|  |       const blockTxs: { [txid: string]: MempoolTransactionExtended } = {}; | ||||||
|  |       for (const tx of transactions) { | ||||||
|  |         blockTxs[tx.txid] = tx; | ||||||
|  |       } | ||||||
|  |       const accelerations = Object.values(mempool.getAccelerations()); | ||||||
|  |       const boostRate = accelerationCosts.calculateBoostRate( | ||||||
|  |         accelerations.map(acc => ({ txid: acc.txid, max_bid: acc.feeDelta })), | ||||||
|  |         transactions | ||||||
|  |       ); | ||||||
|  |       for (const acc of accelerations) { | ||||||
|  |         if (blockTxs[acc.txid]) { | ||||||
|  |           const tx = blockTxs[acc.txid]; | ||||||
|  |           const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions); | ||||||
|  |           accelerationRepository.$saveAcceleration(accelerationInfo, block, block.extras.pool.id); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); |     const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); | ||||||
|     memPool.handleMinedRbfTransactions(rbfTransactions); |     memPool.handleMinedRbfTransactions(rbfTransactions); | ||||||
|     memPool.removeFromSpendMap(transactions); |     memPool.removeFromSpendMap(transactions); | ||||||
| @ -735,7 +759,6 @@ class WebsocketHandler { | |||||||
|     if (config.MEMPOOL.AUDIT && memPool.isInSync()) { |     if (config.MEMPOOL.AUDIT && memPool.isInSync()) { | ||||||
|       let projectedBlocks; |       let projectedBlocks; | ||||||
|       let auditMempool = _memPool; |       let auditMempool = _memPool; | ||||||
|       const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations())); |  | ||||||
|       // template calculation functions have mempool side effects, so calculate audits using
 |       // template calculation functions have mempool side effects, so calculate audits using
 | ||||||
|       // a cloned copy of the mempool if we're running a different algorithm for mempool updates
 |       // a cloned copy of the mempool if we're running a different algorithm for mempool updates
 | ||||||
|       const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL; |       const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL; | ||||||
|  | |||||||
| @ -209,6 +209,7 @@ export const TransactionFlags = { | |||||||
|   v1:                                                          0b00000100n, |   v1:                                                          0b00000100n, | ||||||
|   v2:                                                          0b00001000n, |   v2:                                                          0b00001000n, | ||||||
|   v3:                                                          0b00010000n, |   v3:                                                          0b00010000n, | ||||||
|  |   nonstandard:                                                 0b00100000n, | ||||||
|   // address types
 |   // address types
 | ||||||
|   p2pk:                                               0b00000001_00000000n, |   p2pk:                                               0b00000001_00000000n, | ||||||
|   p2ms:                                               0b00000010_00000000n, |   p2ms:                                               0b00000010_00000000n, | ||||||
|  | |||||||
							
								
								
									
										109
									
								
								backend/src/repositories/AccelerationRepository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								backend/src/repositories/AccelerationRepository.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,109 @@ | |||||||
|  | import { AccelerationInfo } from '../api/acceleration'; | ||||||
|  | import { ResultSetHeader, RowDataPacket } from 'mysql2'; | ||||||
|  | import DB from '../database'; | ||||||
|  | import logger from '../logger'; | ||||||
|  | import { IEsploraApi } from '../api/bitcoin/esplora-api.interface'; | ||||||
|  | import { Common } from '../api/common'; | ||||||
|  | import config from '../config'; | ||||||
|  | 
 | ||||||
|  | export interface PublicAcceleration { | ||||||
|  |   txid: string, | ||||||
|  |   height: number, | ||||||
|  |   pool: { | ||||||
|  |     id: number, | ||||||
|  |     slug: string, | ||||||
|  |     name: string, | ||||||
|  |   }, | ||||||
|  |   effective_vsize: number, | ||||||
|  |   effective_fee: number, | ||||||
|  |   boost_rate: number, | ||||||
|  |   boost_cost: number, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class AccelerationRepository { | ||||||
|  |   public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       await DB.query(` | ||||||
|  |         INSERT INTO accelerations(txid, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost) | ||||||
|  |         VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?) | ||||||
|  |         ON DUPLICATE KEY UPDATE | ||||||
|  |           added = FROM_UNIXTIME(?), | ||||||
|  |           height = ?, | ||||||
|  |           pool = ?, | ||||||
|  |           effective_vsize = ?, | ||||||
|  |           effective_fee = ?, | ||||||
|  |           boost_rate = ?, | ||||||
|  |           boost_cost = ? | ||||||
|  |       `, [
 | ||||||
|  |         acceleration.txSummary.txid, | ||||||
|  |         block.timestamp, | ||||||
|  |         block.height, | ||||||
|  |         pool_id, | ||||||
|  |         acceleration.txSummary.effectiveVsize, | ||||||
|  |         acceleration.txSummary.effectiveFee, | ||||||
|  |         acceleration.targetFeeRate, acceleration.cost, | ||||||
|  |         block.timestamp, | ||||||
|  |         block.height, | ||||||
|  |         pool_id, | ||||||
|  |         acceleration.txSummary.effectiveVsize, | ||||||
|  |         acceleration.txSummary.effectiveFee, | ||||||
|  |         acceleration.targetFeeRate, acceleration.cost, | ||||||
|  |       ]); | ||||||
|  |     } catch (e: any) { | ||||||
|  |       logger.err(`Cannot save acceleration (${acceleration.txSummary.txid}) into db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |       // We don't throw, not a critical issue if we miss some accelerations
 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async $getAccelerationInfo(poolSlug: string | null = null, height: number | null = null, interval: string | null = null): Promise<PublicAcceleration[]> { | ||||||
|  |     interval = Common.getSqlInterval(interval); | ||||||
|  | 
 | ||||||
|  |     if (!config.MEMPOOL_SERVICES.ACCELERATIONS || (interval == null && poolSlug == null && height == null)) { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let query = ` | ||||||
|  |       SELECT * FROM accelerations | ||||||
|  |       JOIN pools on pools.unique_id = accelerations.pool | ||||||
|  |     `;
 | ||||||
|  |     let params: any[] = []; | ||||||
|  | 
 | ||||||
|  |     if (interval) { | ||||||
|  |       query += ` WHERE accelerations.added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() `; | ||||||
|  |     } else if (height != null) { | ||||||
|  |       query += ` WHERE accelerations.height = ? `; | ||||||
|  |       params.push(height); | ||||||
|  |     } else if (poolSlug != null) { | ||||||
|  |       query += ` WHERE pools.slug = ? `; | ||||||
|  |       params.push(poolSlug); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     query += ` ORDER BY accelerations.added DESC `; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       const [rows] = await DB.query(query, params) as RowDataPacket[][]; | ||||||
|  |       if (rows?.length) { | ||||||
|  |         return rows.map(row => ({ | ||||||
|  |           txid: row.txid, | ||||||
|  |           height: row.height, | ||||||
|  |           pool: { | ||||||
|  |             id: row.id, | ||||||
|  |             slug: row.slug, | ||||||
|  |             name: row.name, | ||||||
|  |           }, | ||||||
|  |           effective_vsize: row.effective_vsize, | ||||||
|  |           effective_fee: row.effective_fee, | ||||||
|  |           boost_rate: row.boost_rate, | ||||||
|  |           boost_cost: row.boost_cost, | ||||||
|  |         })); | ||||||
|  |       } else { | ||||||
|  |         return []; | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err(`Cannot query acceleration info. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |       throw e; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default new AccelerationRepository(); | ||||||
							
								
								
									
										203
									
								
								backend/src/utils/bitcoin-script.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								backend/src/utils/bitcoin-script.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,203 @@ | |||||||
|  | const opcodes = { | ||||||
|  |   OP_FALSE: 0, | ||||||
|  |   OP_0: 0, | ||||||
|  |   OP_PUSHDATA1: 76, | ||||||
|  |   OP_PUSHDATA2: 77, | ||||||
|  |   OP_PUSHDATA4: 78, | ||||||
|  |   OP_1NEGATE: 79, | ||||||
|  |   OP_PUSHNUM_NEG1: 79, | ||||||
|  |   OP_RESERVED: 80, | ||||||
|  |   OP_TRUE: 81, | ||||||
|  |   OP_1: 81, | ||||||
|  |   OP_2: 82, | ||||||
|  |   OP_3: 83, | ||||||
|  |   OP_4: 84, | ||||||
|  |   OP_5: 85, | ||||||
|  |   OP_6: 86, | ||||||
|  |   OP_7: 87, | ||||||
|  |   OP_8: 88, | ||||||
|  |   OP_9: 89, | ||||||
|  |   OP_10: 90, | ||||||
|  |   OP_11: 91, | ||||||
|  |   OP_12: 92, | ||||||
|  |   OP_13: 93, | ||||||
|  |   OP_14: 94, | ||||||
|  |   OP_15: 95, | ||||||
|  |   OP_16: 96, | ||||||
|  |   OP_PUSHNUM_1: 81, | ||||||
|  |   OP_PUSHNUM_2: 82, | ||||||
|  |   OP_PUSHNUM_3: 83, | ||||||
|  |   OP_PUSHNUM_4: 84, | ||||||
|  |   OP_PUSHNUM_5: 85, | ||||||
|  |   OP_PUSHNUM_6: 86, | ||||||
|  |   OP_PUSHNUM_7: 87, | ||||||
|  |   OP_PUSHNUM_8: 88, | ||||||
|  |   OP_PUSHNUM_9: 89, | ||||||
|  |   OP_PUSHNUM_10: 90, | ||||||
|  |   OP_PUSHNUM_11: 91, | ||||||
|  |   OP_PUSHNUM_12: 92, | ||||||
|  |   OP_PUSHNUM_13: 93, | ||||||
|  |   OP_PUSHNUM_14: 94, | ||||||
|  |   OP_PUSHNUM_15: 95, | ||||||
|  |   OP_PUSHNUM_16: 96, | ||||||
|  |   OP_NOP: 97, | ||||||
|  |   OP_VER: 98, | ||||||
|  |   OP_IF: 99, | ||||||
|  |   OP_NOTIF: 100, | ||||||
|  |   OP_VERIF: 101, | ||||||
|  |   OP_VERNOTIF: 102, | ||||||
|  |   OP_ELSE: 103, | ||||||
|  |   OP_ENDIF: 104, | ||||||
|  |   OP_VERIFY: 105, | ||||||
|  |   OP_RETURN: 106, | ||||||
|  |   OP_TOALTSTACK: 107, | ||||||
|  |   OP_FROMALTSTACK: 108, | ||||||
|  |   OP_2DROP: 109, | ||||||
|  |   OP_2DUP: 110, | ||||||
|  |   OP_3DUP: 111, | ||||||
|  |   OP_2OVER: 112, | ||||||
|  |   OP_2ROT: 113, | ||||||
|  |   OP_2SWAP: 114, | ||||||
|  |   OP_IFDUP: 115, | ||||||
|  |   OP_DEPTH: 116, | ||||||
|  |   OP_DROP: 117, | ||||||
|  |   OP_DUP: 118, | ||||||
|  |   OP_NIP: 119, | ||||||
|  |   OP_OVER: 120, | ||||||
|  |   OP_PICK: 121, | ||||||
|  |   OP_ROLL: 122, | ||||||
|  |   OP_ROT: 123, | ||||||
|  |   OP_SWAP: 124, | ||||||
|  |   OP_TUCK: 125, | ||||||
|  |   OP_CAT: 126, | ||||||
|  |   OP_SUBSTR: 127, | ||||||
|  |   OP_LEFT: 128, | ||||||
|  |   OP_RIGHT: 129, | ||||||
|  |   OP_SIZE: 130, | ||||||
|  |   OP_INVERT: 131, | ||||||
|  |   OP_AND: 132, | ||||||
|  |   OP_OR: 133, | ||||||
|  |   OP_XOR: 134, | ||||||
|  |   OP_EQUAL: 135, | ||||||
|  |   OP_EQUALVERIFY: 136, | ||||||
|  |   OP_RESERVED1: 137, | ||||||
|  |   OP_RESERVED2: 138, | ||||||
|  |   OP_1ADD: 139, | ||||||
|  |   OP_1SUB: 140, | ||||||
|  |   OP_2MUL: 141, | ||||||
|  |   OP_2DIV: 142, | ||||||
|  |   OP_NEGATE: 143, | ||||||
|  |   OP_ABS: 144, | ||||||
|  |   OP_NOT: 145, | ||||||
|  |   OP_0NOTEQUAL: 146, | ||||||
|  |   OP_ADD: 147, | ||||||
|  |   OP_SUB: 148, | ||||||
|  |   OP_MUL: 149, | ||||||
|  |   OP_DIV: 150, | ||||||
|  |   OP_MOD: 151, | ||||||
|  |   OP_LSHIFT: 152, | ||||||
|  |   OP_RSHIFT: 153, | ||||||
|  |   OP_BOOLAND: 154, | ||||||
|  |   OP_BOOLOR: 155, | ||||||
|  |   OP_NUMEQUAL: 156, | ||||||
|  |   OP_NUMEQUALVERIFY: 157, | ||||||
|  |   OP_NUMNOTEQUAL: 158, | ||||||
|  |   OP_LESSTHAN: 159, | ||||||
|  |   OP_GREATERTHAN: 160, | ||||||
|  |   OP_LESSTHANOREQUAL: 161, | ||||||
|  |   OP_GREATERTHANOREQUAL: 162, | ||||||
|  |   OP_MIN: 163, | ||||||
|  |   OP_MAX: 164, | ||||||
|  |   OP_WITHIN: 165, | ||||||
|  |   OP_RIPEMD160: 166, | ||||||
|  |   OP_SHA1: 167, | ||||||
|  |   OP_SHA256: 168, | ||||||
|  |   OP_HASH160: 169, | ||||||
|  |   OP_HASH256: 170, | ||||||
|  |   OP_CODESEPARATOR: 171, | ||||||
|  |   OP_CHECKSIG: 172, | ||||||
|  |   OP_CHECKSIGVERIFY: 173, | ||||||
|  |   OP_CHECKMULTISIG: 174, | ||||||
|  |   OP_CHECKMULTISIGVERIFY: 175, | ||||||
|  |   OP_NOP1: 176, | ||||||
|  |   OP_NOP2: 177, | ||||||
|  |   OP_CHECKLOCKTIMEVERIFY: 177, | ||||||
|  |   OP_CLTV: 177, | ||||||
|  |   OP_NOP3: 178, | ||||||
|  |   OP_CHECKSEQUENCEVERIFY: 178, | ||||||
|  |   OP_CSV: 178, | ||||||
|  |   OP_NOP4: 179, | ||||||
|  |   OP_NOP5: 180, | ||||||
|  |   OP_NOP6: 181, | ||||||
|  |   OP_NOP7: 182, | ||||||
|  |   OP_NOP8: 183, | ||||||
|  |   OP_NOP9: 184, | ||||||
|  |   OP_NOP10: 185, | ||||||
|  |   OP_CHECKSIGADD: 186, | ||||||
|  |   OP_PUBKEYHASH: 253, | ||||||
|  |   OP_PUBKEY: 254, | ||||||
|  |   OP_INVALIDOPCODE: 255, | ||||||
|  | }; | ||||||
|  | // add unused opcodes
 | ||||||
|  | for (let i = 187; i <= 255; i++) { | ||||||
|  |   opcodes[`OP_RETURN_${i}`] = i; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { opcodes }; | ||||||
|  | 
 | ||||||
|  | /** extracts m and n from a multisig script (asm), returns nothing if it is not a multisig script */ | ||||||
|  | export function parseMultisigScript(script: string): void | { m: number, n: number } { | ||||||
|  |   if (!script) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   const ops = script.split(' '); | ||||||
|  |   if (ops.length < 3 || ops.pop() !== 'OP_CHECKMULTISIG') { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   const opN = ops.pop(); | ||||||
|  |   if (!opN) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (!opN.startsWith('OP_PUSHNUM_')) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); | ||||||
|  |   if (ops.length < n * 2 + 1) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   // pop n public keys
 | ||||||
|  |   for (let i = 0; i < n; i++) { | ||||||
|  |     if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop() || '')) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop() || '')) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   const opM = ops.pop(); | ||||||
|  |   if (!opM) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (!opM.startsWith('OP_PUSHNUM_')) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); | ||||||
|  | 
 | ||||||
|  |   if (ops.length) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return { m, n }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getVarIntLength(n: number): number { | ||||||
|  |   if (n < 0xfd) { | ||||||
|  |     return 1; | ||||||
|  |   } else if (n <= 0xffff) { | ||||||
|  |     return 3; | ||||||
|  |   } else if (n <= 0xffffffff) { | ||||||
|  |     return 5; | ||||||
|  |   } else { | ||||||
|  |     return 9; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -23,6 +23,9 @@ | |||||||
|         <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1m'"> |         <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1m'"> | ||||||
|           <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 1M |           <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 1M | ||||||
|         </label> |         </label> | ||||||
|  |         <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'"> | ||||||
|  |           <input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 3M | ||||||
|  |         </label> | ||||||
|       </div> |       </div> | ||||||
|     </form> |     </form> | ||||||
|   </div> |   </div> | ||||||
|  | |||||||
| @ -25,7 +25,8 @@ | |||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   padding: 0px 15px; |   padding: 0px 15px; | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   height: calc(100vh - 250px); |   height: calc(100vh - 225px); | ||||||
|  |   min-height: 400px; | ||||||
|   @media (min-width: 992px) { |   @media (min-width: 992px) { | ||||||
|     height: calc(100vh - 150px); |     height: calc(100vh - 150px); | ||||||
|   } |   } | ||||||
| @ -35,6 +36,7 @@ | |||||||
|   display: flex; |   display: flex; | ||||||
|   flex: 1; |   flex: 1; | ||||||
|   width: 100%; |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|   padding-bottom: 20px; |   padding-bottom: 20px; | ||||||
|   padding-right: 10px; |   padding-right: 10px; | ||||||
|   @media (max-width: 992px) { |   @media (max-width: 992px) { | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core'; | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core'; | ||||||
| import { EChartsOption, graphic } from 'echarts'; | import { EChartsOption } from 'echarts'; | ||||||
| import { Observable, Subscription, combineLatest, fromEvent } from 'rxjs'; | import { Observable, Subscription, combineLatest, fromEvent } from 'rxjs'; | ||||||
| import { map, max, startWith, switchMap, tap } from 'rxjs/operators'; | import { startWith, switchMap, tap } from 'rxjs/operators'; | ||||||
| import { SeoService } from '../../../services/seo.service'; | import { SeoService } from '../../../services/seo.service'; | ||||||
| import { formatNumber } from '@angular/common'; | import { formatNumber } from '@angular/common'; | ||||||
| import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; | import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; | ||||||
| @ -11,7 +11,6 @@ import { MiningService } from '../../../services/mining.service'; | |||||||
| import { ActivatedRoute } from '@angular/router'; | import { ActivatedRoute } from '@angular/router'; | ||||||
| import { Acceleration } from '../../../interfaces/node-api.interface'; | import { Acceleration } from '../../../interfaces/node-api.interface'; | ||||||
| import { ServicesApiServices } from '../../../services/services-api.service'; | import { ServicesApiServices } from '../../../services/services-api.service'; | ||||||
| import { ApiService } from '../../../services/api.service'; |  | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-acceleration-fees-graph', |   selector: 'app-acceleration-fees-graph', | ||||||
| @ -29,7 +28,7 @@ import { ApiService } from '../../../services/api.service'; | |||||||
| }) | }) | ||||||
| export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { | export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { | ||||||
|   @Input() widget: boolean = false; |   @Input() widget: boolean = false; | ||||||
|   @Input() height: number | string = '200'; |   @Input() height: number = 300; | ||||||
|   @Input() right: number | string = 45; |   @Input() right: number | string = 45; | ||||||
|   @Input() left: number | string = 75; |   @Input() left: number | string = 75; | ||||||
|   @Input() accelerations$: Observable<Acceleration[]>; |   @Input() accelerations$: Observable<Acceleration[]>; | ||||||
| @ -55,7 +54,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { | |||||||
|   constructor( |   constructor( | ||||||
|     @Inject(LOCALE_ID) public locale: string, |     @Inject(LOCALE_ID) public locale: string, | ||||||
|     private seoService: SeoService, |     private seoService: SeoService, | ||||||
|     private apiService: ApiService, |  | ||||||
|     private servicesApiService: ServicesApiServices, |     private servicesApiService: ServicesApiServices, | ||||||
|     private formBuilder: UntypedFormBuilder, |     private formBuilder: UntypedFormBuilder, | ||||||
|     private storageService: StorageService, |     private storageService: StorageService, | ||||||
| @ -69,104 +67,56 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.isLoading = true; |  | ||||||
|     if (this.widget) { |     if (this.widget) { | ||||||
|       this.miningWindowPreference = '1m'; |       this.miningWindowPreference = '3m'; | ||||||
|       this.timespan = this.miningWindowPreference; |  | ||||||
| 
 |  | ||||||
|       this.statsObservable$ = combineLatest([ |  | ||||||
|         (this.accelerations$ || this.servicesApiService.getAccelerationHistory$({ timeframe: this.miningWindowPreference })), |  | ||||||
|         this.apiService.getHistoricalBlockFees$(this.miningWindowPreference), |  | ||||||
|         fromEvent(window, 'resize').pipe(startWith(null)), |  | ||||||
|       ]).pipe( |  | ||||||
|         tap(([accelerations, blockFeesResponse]) => { |  | ||||||
|           this.prepareChartOptions(accelerations, blockFeesResponse.body); |  | ||||||
|         }), |  | ||||||
|         map(([accelerations, blockFeesResponse]) => { |  | ||||||
|           return { |  | ||||||
|             avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0) / accelerations.length |  | ||||||
|           }; |  | ||||||
|         }), |  | ||||||
|       ); |  | ||||||
|     } else { |     } else { | ||||||
|       this.seoService.setTitle($localize`:@@bcf34abc2d9ed8f45a2f65dd464c46694e9a181e:Acceleration Fees`); |       this.seoService.setTitle($localize`:@@bcf34abc2d9ed8f45a2f65dd464c46694e9a181e:Acceleration Fees`); | ||||||
|       this.miningWindowPreference = this.miningService.getDefaultTimespan('1w'); |       this.miningWindowPreference = this.miningService.getDefaultTimespan('3m'); | ||||||
|       this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); |  | ||||||
|       this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); |  | ||||||
|       this.route.fragment.subscribe((fragment) => { |  | ||||||
|         if (['24h', '3d', '1w', '1m'].indexOf(fragment) > -1) { |  | ||||||
|           this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|       this.statsObservable$ = combineLatest([ |  | ||||||
|         this.radioGroupForm.get('dateSpan').valueChanges.pipe( |  | ||||||
|           startWith(this.radioGroupForm.controls.dateSpan.value), |  | ||||||
|           switchMap((timespan) => { |  | ||||||
|             this.isLoading = true; |  | ||||||
|             this.storageService.setValue('miningWindowPreference', timespan); |  | ||||||
|             this.timespan = timespan; |  | ||||||
|             return this.servicesApiService.getAccelerationHistory$({}); |  | ||||||
|           }) |  | ||||||
|         ), |  | ||||||
|         this.radioGroupForm.get('dateSpan').valueChanges.pipe( |  | ||||||
|           startWith(this.radioGroupForm.controls.dateSpan.value), |  | ||||||
|           switchMap((timespan) => { |  | ||||||
|             return this.apiService.getHistoricalBlockFees$(timespan); |  | ||||||
|           }) |  | ||||||
|         ) |  | ||||||
|       ]).pipe( |  | ||||||
|         tap(([accelerations, blockFeesResponse]) => { |  | ||||||
|           this.prepareChartOptions(accelerations, blockFeesResponse.body); |  | ||||||
|         }) |  | ||||||
|       ); |  | ||||||
|     } |     } | ||||||
|     this.statsSubscription = this.statsObservable$.subscribe(() => { |     this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); | ||||||
|       this.isLoading = false; |     this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); | ||||||
|       this.cd.markForCheck(); |      | ||||||
|  |     this.route.fragment.subscribe((fragment) => { | ||||||
|  |       if (['24h', '3d', '1w', '1m', '3m'].indexOf(fragment) > -1) { | ||||||
|  |         this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
|  |     this.statsObservable$ = combineLatest([ | ||||||
|  |       this.radioGroupForm.get('dateSpan').valueChanges.pipe( | ||||||
|  |         startWith(this.radioGroupForm.controls.dateSpan.value), | ||||||
|  |         switchMap((timespan) => { | ||||||
|  |           if (!this.widget) { | ||||||
|  |             this.storageService.setValue('miningWindowPreference', timespan); | ||||||
|  |           } | ||||||
|  |           this.isLoading = true; | ||||||
|  |           this.timespan = timespan; | ||||||
|  |           return this.servicesApiService.getAggregatedAccelerationHistory$({timeframe: this.timespan}); | ||||||
|  |         }) | ||||||
|  |       ), | ||||||
|  |       fromEvent(window, 'resize').pipe(startWith(null)), | ||||||
|  |     ]).pipe( | ||||||
|  |       tap(([history]) => { | ||||||
|  |         this.isLoading = false; | ||||||
|  |         this.prepareChartOptions(history); | ||||||
|  |         this.cd.markForCheck(); | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     this.statsObservable$.subscribe(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   prepareChartOptions(accelerations, blockFees) { |   prepareChartOptions(data) { | ||||||
|     let title: object; |     let title: object; | ||||||
| 
 |     if (data.length === 0) { | ||||||
|     const blockAccelerations = {}; |       title = { | ||||||
| 
 |         textStyle: { | ||||||
|     for (const acceleration of accelerations) { |           color: 'grey', | ||||||
|       if (acceleration.status === 'completed') { |           fontSize: 15 | ||||||
|         if (!blockAccelerations[acceleration.blockHeight]) { |         }, | ||||||
|           blockAccelerations[acceleration.blockHeight] = []; |         text: $localize`No accelerated transaction for this timeframe`, | ||||||
|         } |         left: 'center', | ||||||
|         blockAccelerations[acceleration.blockHeight].push(acceleration); |         top: 'center' | ||||||
|       } |       }; | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let last = null; |  | ||||||
|     let minValue = Infinity; |  | ||||||
|     let maxValue = 0; |  | ||||||
|     const data = []; |  | ||||||
|     for (const val of blockFees) { |  | ||||||
|       if (last == null) { |  | ||||||
|         last = val.avgHeight; |  | ||||||
|       } |  | ||||||
|       let totalFeeDelta = 0; |  | ||||||
|       let totalFeePaid = 0; |  | ||||||
|       let totalCount = 0; |  | ||||||
|       let blockCount = 0; |  | ||||||
|       while (last <= val.avgHeight) { |  | ||||||
|         blockCount++; |  | ||||||
|         totalFeeDelta += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feeDelta, 0); |  | ||||||
|         totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0); |  | ||||||
|         totalCount += (blockAccelerations[last] || []).length; |  | ||||||
|         last++; |  | ||||||
|       } |  | ||||||
|       minValue = Math.min(minValue, val.avgFees); |  | ||||||
|       maxValue = Math.max(maxValue, val.avgFees); |  | ||||||
|       data.push({ |  | ||||||
|         ...val, |  | ||||||
|         feeDelta: totalFeeDelta, |  | ||||||
|         avgFeePaid: (totalFeePaid / blockCount), |  | ||||||
|         accelerations: totalCount / blockCount, |  | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.chartOptions = { |     this.chartOptions = { | ||||||
| @ -177,11 +127,11 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { | |||||||
|       ], |       ], | ||||||
|       animation: false, |       animation: false, | ||||||
|       grid: { |       grid: { | ||||||
|         height: this.height, |         height: (this.widget && this.height) ? this.height - 30 : undefined, | ||||||
|  |         top: this.widget ? 20 : 40, | ||||||
|  |         bottom: this.widget ? 30 : 80, | ||||||
|         right: this.right, |         right: this.right, | ||||||
|         left: this.left, |         left: this.left, | ||||||
|         bottom: this.widget ? 30 : 80, |  | ||||||
|         top: this.widget ? 20 : (this.isMobile() ? 10 : 50), |  | ||||||
|       }, |       }, | ||||||
|       tooltip: { |       tooltip: { | ||||||
|         show: !this.isMobile(), |         show: !this.isMobile(), | ||||||
| @ -197,29 +147,23 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { | |||||||
|           align: 'left', |           align: 'left', | ||||||
|         }, |         }, | ||||||
|         borderColor: '#000', |         borderColor: '#000', | ||||||
|         formatter: function (data) { |         formatter: (ticks) => { | ||||||
|           if (data.length <= 0) { |           let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10))}</b><br>`; | ||||||
|             return ''; |  | ||||||
|           } |  | ||||||
|           let tooltip = `<b style="color: white; margin-left: 2px">
 |  | ||||||
|             ${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))}</b><br>`;
 |  | ||||||
| 
 | 
 | ||||||
|           for (const tick of data.reverse()) { |           if (ticks[0].data[1] > 10_000_000) { | ||||||
|             if (tick.data[1] >= 1_000_000) { |             tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1] / 100_000_000, this.locale, '1.0-0')} BTC<br>`; | ||||||
|               tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1] / 100_000_000, this.locale, '1.0-3')} BTC<br>`; |           } else { | ||||||
|             } else { |             tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1], this.locale, '1.0-0')} sats<br>`; | ||||||
|               tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')} sats<br>`; |  | ||||||
|             } |  | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           if (['24h', '3d'].includes(this.timespan)) { |           if (['24h', '3d'].includes(this.timespan)) { | ||||||
|             tooltip += `<small>` + $localize`At block: ${data[0].data[2]}` + `</small>`; |             tooltip += `<small>` + $localize`At block: ${ticks[0].data[2]}` + `</small>`; | ||||||
|           } else { |           } else { | ||||||
|             tooltip += `<small>` + $localize`Around block: ${data[0].data[2]}` + `</small>`; |             tooltip += `<small>` + $localize`Around block: ${ticks[0].data[2]}` + `</small>`; | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           return tooltip; |           return tooltip; | ||||||
|         }.bind(this) |         } | ||||||
|       }, |       }, | ||||||
|       xAxis: data.length === 0 ? undefined : |       xAxis: data.length === 0 ? undefined : | ||||||
|       { |       { | ||||||
| @ -228,7 +172,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { | |||||||
|         nameTextStyle: { |         nameTextStyle: { | ||||||
|           padding: [10, 0, 0, 0], |           padding: [10, 0, 0, 0], | ||||||
|         }, |         }, | ||||||
|         type: 'category', |         type: 'time', | ||||||
|         boundaryGap: false, |         boundaryGap: false, | ||||||
|         axisLine: { onZero: true }, |         axisLine: { onZero: true }, | ||||||
|         axisLabel: { |         axisLabel: { | ||||||
| @ -243,15 +187,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { | |||||||
|       legend: { |       legend: { | ||||||
|         data: [ |         data: [ | ||||||
|           { |           { | ||||||
|             name: 'In-band fees per block', |             name: 'Total bid boost', | ||||||
|             inactiveColor: 'rgb(110, 112, 121)', |  | ||||||
|             textStyle: { |  | ||||||
|               color: 'white', |  | ||||||
|             }, |  | ||||||
|             icon: 'roundRect', |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             name: 'Total bid boost per block', |  | ||||||
|             inactiveColor: 'rgb(110, 112, 121)', |             inactiveColor: 'rgb(110, 112, 121)', | ||||||
|             textStyle: { |             textStyle: { | ||||||
|               color: 'white', |               color: 'white', | ||||||
| @ -260,8 +196,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { | |||||||
|           }, |           }, | ||||||
|         ], |         ], | ||||||
|         selected: { |         selected: { | ||||||
|           'In-band fees per block': false, |           'Total bid boost': true, | ||||||
|           'Total bid boost per block': true, |  | ||||||
|         }, |         }, | ||||||
|         show: !this.widget, |         show: !this.widget, | ||||||
|       }, |       }, | ||||||
| @ -304,21 +239,13 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { | |||||||
|         { |         { | ||||||
|           legendHoverLink: false, |           legendHoverLink: false, | ||||||
|           zlevel: 1, |           zlevel: 1, | ||||||
|           name: 'Total bid boost per block', |           name: 'Total bid boost', | ||||||
|           data: data.map(block =>  [block.timestamp * 1000, block.avgFeePaid, block.avgHeight]), |           data: data.map(h =>  { | ||||||
|  |             return [h.timestamp * 1000, h.sumBidBoost, h.avgHeight] | ||||||
|  |           }), | ||||||
|           stack: 'Total', |           stack: 'Total', | ||||||
|           type: 'bar', |           type: 'bar', | ||||||
|           barWidth: '100%', |           barWidth: '90%', | ||||||
|           large: true, |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           legendHoverLink: false, |  | ||||||
|           zlevel: 0, |  | ||||||
|           name: 'In-band fees per block', |  | ||||||
|           data: data.map(block =>  [block.timestamp * 1000, block.avgFees, block.avgHeight]), |  | ||||||
|           stack: 'Total', |  | ||||||
|           type: 'bar', |  | ||||||
|           barWidth: '100%', |  | ||||||
|           large: true, |           large: true, | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
| @ -347,17 +274,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { | |||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|       }], |       }], | ||||||
|       visualMap: { |  | ||||||
|         type: 'continuous', |  | ||||||
|         min: minValue, |  | ||||||
|         max: maxValue, |  | ||||||
|         dimension: 1, |  | ||||||
|         seriesIndex: 1, |  | ||||||
|         show: false, |  | ||||||
|         inRange: { |  | ||||||
|           color: ['#F4511E7f', '#FB8C007f', '#FFB3007f', '#FDD8357f', '#7CB3427f'].reverse() // Gradient color range
 |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -3,16 +3,16 @@ | |||||||
|     <div class="item"> |     <div class="item"> | ||||||
|       <h5 class="card-title" i18n="accelerator.requests">Requests</h5> |       <h5 class="card-title" i18n="accelerator.requests">Requests</h5> | ||||||
|       <div class="card-text"> |       <div class="card-text"> | ||||||
|         <div>{{ stats.count }}</div> |         <div>{{ stats.totalRequested }}</div> | ||||||
|         <div class="symbol" i18n="accelerator.total-accelerated">accelerated</div> |         <div class="symbol" i18n="accelerator.total-accelerated">accelerated</div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <div class="item"> |     <div class="item"> | ||||||
|       <h5 class="card-title" i18n="accelerator.total-boost">Total Bid Boost</h5> |       <h5 class="card-title" i18n="accelerator.total-boost">Total Bid Boost</h5> | ||||||
|       <div class="card-text"> |       <div class="card-text"> | ||||||
|         <div>{{ stats.totalFeesPaid / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div> |         <div>{{ stats.totalBidBoost / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div> | ||||||
|         <span class="fiat"> |         <span class="fiat"> | ||||||
|           <app-fiat [value]="stats.totalFeesPaid"></app-fiat> |           <app-fiat [value]="stats.totalBidBoost"></app-fiat> | ||||||
|         </span> |         </span> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  | |||||||
| @ -1,9 +1,12 @@ | |||||||
| import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | ||||||
| import { Observable, of } from 'rxjs'; | import { Observable } from 'rxjs'; | ||||||
| import { switchMap } from 'rxjs/operators'; | import { ServicesApiServices } from '../../../services/services-api.service'; | ||||||
| import { ApiService } from '../../../services/api.service'; | 
 | ||||||
| import { StateService } from '../../../services/state.service'; | export type AccelerationStats = { | ||||||
| import { Acceleration } from '../../../interfaces/node-api.interface'; |   totalRequested: number; | ||||||
|  |   totalBidBoost: number; | ||||||
|  |   successRate: number; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-acceleration-stats', |   selector: 'app-acceleration-stats', | ||||||
| @ -12,35 +15,13 @@ import { Acceleration } from '../../../interfaces/node-api.interface'; | |||||||
|   changeDetection: ChangeDetectionStrategy.OnPush, |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
| }) | }) | ||||||
| export class AccelerationStatsComponent implements OnInit { | export class AccelerationStatsComponent implements OnInit { | ||||||
|   @Input() timespan: '24h' | '1w' | '1m' = '24h'; |   accelerationStats$: Observable<AccelerationStats>; | ||||||
|   @Input() accelerations$: Observable<Acceleration[]>; |  | ||||||
|   public accelerationStats$: Observable<any>; |  | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     private apiService: ApiService, |     private servicesApiService: ServicesApiServices | ||||||
|     private stateService: StateService, |  | ||||||
|   ) { } |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.accelerationStats$ = this.accelerations$.pipe( |     this.accelerationStats$ = this.servicesApiService.getAccelerationStats$(); | ||||||
|       switchMap(accelerations => { |  | ||||||
|         let totalFeesPaid = 0; |  | ||||||
|         let totalSucceeded = 0; |  | ||||||
|         let totalCanceled = 0; |  | ||||||
|         for (const acc of accelerations) { |  | ||||||
|           if (acc.status === 'completed') { |  | ||||||
|             totalSucceeded++; |  | ||||||
|             totalFeesPaid += (acc.feePaid - acc.baseFee - acc.vsizeFee) || 0; |  | ||||||
|           } else if (acc.status === 'failed') { |  | ||||||
|             totalCanceled++; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         return of({ |  | ||||||
|           count: totalSucceeded, |  | ||||||
|           totalFeesPaid, |  | ||||||
|           successRate: (totalSucceeded + totalCanceled > 0) ? ((totalSucceeded / (totalSucceeded + totalCanceled)) * 100) : 0.0, |  | ||||||
|         }); |  | ||||||
|       }) |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| <div class="container-xl widget-container" [class.widget]="widget" [class.full-height]="!widget"> | <div class="container-lg widget-container" [class.widget]="widget" [class.full-height]="!widget"> | ||||||
|   <h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1> |   <h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1> | ||||||
|   <div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div> |   <div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div> | ||||||
| 
 | 
 | ||||||
| @ -17,6 +17,7 @@ | |||||||
|           <th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th> |           <th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th> | ||||||
|           <th class="block text-right" i18n="accelerator.block">Block</th> |           <th class="block text-right" i18n="accelerator.block">Block</th> | ||||||
|           <th class="status text-right" i18n="transaction.status|Transaction Status">Status</th> |           <th class="status text-right" i18n="transaction.status|Transaction Status">Status</th> | ||||||
|  |           <th class="date text-right" i18n="" *ngIf="!this.widget">Requested</th> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|       </thead> |       </thead> | ||||||
|       <tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> |       <tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> | ||||||
| @ -49,9 +50,13 @@ | |||||||
|             </td> |             </td> | ||||||
|             <td class="status text-right"> |             <td class="status text-right"> | ||||||
|               <span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span> |               <span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span> | ||||||
|               <span *ngIf="acceleration.status === 'mined' || acceleration.status === 'completed'" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span> |               <span *ngIf="acceleration.status === 'mined'" class="badge badge-info" i18n="transaction.rbf.mined">Mined</span> | ||||||
|  |               <span *ngIf="acceleration.status === 'completed'" class="badge badge-success" i18n="">Completed</span> | ||||||
|               <span *ngIf="acceleration.status === 'failed'" class="badge badge-danger" i18n="accelerator.canceled">Canceled</span> |               <span *ngIf="acceleration.status === 'failed'" class="badge badge-danger" i18n="accelerator.canceled">Canceled</span> | ||||||
|             </td> |             </td> | ||||||
|  |             <td class="date text-right" *ngIf="!this.widget"> | ||||||
|  |               <app-time kind="since" [time]="acceleration.added" [fastRender]="true"></app-time> | ||||||
|  |             </td> | ||||||
|           </ng-container> |           </ng-container> | ||||||
|         </tr> |         </tr> | ||||||
|       </tbody> |       </tbody> | ||||||
| @ -75,6 +80,11 @@ | |||||||
|       </ng-template> |       </ng-template> | ||||||
|     </table> |     </table> | ||||||
| 
 | 
 | ||||||
|  |     <ngb-pagination *ngIf="!widget" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''" | ||||||
|  |       [collectionSize]="this.accelerationCount" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page" | ||||||
|  |       (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false"> | ||||||
|  |     </ngb-pagination> | ||||||
|  | 
 | ||||||
|     <ng-template [ngIf]="!widget"> |     <ng-template [ngIf]="!widget"> | ||||||
|       <div class="clearfix"></div> |       <div class="clearfix"></div> | ||||||
|       <br> |       <br> | ||||||
|  | |||||||
| @ -63,16 +63,28 @@ tr, td, th { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .txid { | .txid { | ||||||
|   @media (max-width: 500px) { |   width: 20%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .fee { | ||||||
|  |   width: 15%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .block { | ||||||
|  |   width: 15%; | ||||||
|  |   @media (max-width: 700px) { | ||||||
|     display: none; |     display: none; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .fee, .block, .status { | .status { | ||||||
|   width: 15%; |   width: 13%; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|   @media (max-width: 720px) { | .date { | ||||||
|     width: 20%; |   width: 20%; | ||||||
|  |   @media (max-width: 600px) { | ||||||
|  |     display: none; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -83,23 +95,12 @@ tr, td, th { | |||||||
|     text-overflow: ellipsis; |     text-overflow: ellipsis; | ||||||
|     white-space: nowrap; |     white-space: nowrap; | ||||||
|     max-width: 30%; |     max-width: 30%; | ||||||
|     @media (max-width: 1060px) and (min-width: 768px) { |  | ||||||
|       display: none; |  | ||||||
|     } |  | ||||||
|     @media (max-width: 500px) { |  | ||||||
|       display: none; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .fee-rate { |   .fee-rate { | ||||||
|     width: 20%; |     width: 20%; | ||||||
|     @media (max-width: 1060px) and (min-width: 768px) { |     text-align: end !important; | ||||||
|       text-align: start !important; |     @media (max-width: 975px) and (min-width: 768px) { | ||||||
|     } |  | ||||||
|     @media (max-width: 500px) { |  | ||||||
|       text-align: start !important; |  | ||||||
|     } |  | ||||||
|     @media (max-width: 840px) and (min-width: 768px) { |  | ||||||
|       display: none; |       display: none; | ||||||
|     } |     } | ||||||
|     @media (max-width: 410px) { |     @media (max-width: 410px) { | ||||||
| @ -108,32 +109,31 @@ tr, td, th { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .bid { |   .bid { | ||||||
|  |     text-align: end !important; | ||||||
|     width: 30%; |     width: 30%; | ||||||
|     min-width: 150px; |     min-width: 150px; | ||||||
|     @media (max-width: 840px) and (min-width: 768px) { |  | ||||||
|       text-align: start !important; |  | ||||||
|     } |  | ||||||
|     @media (max-width: 410px) { |  | ||||||
|       text-align: start !important; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .time { |   .time { | ||||||
|     width: 25%; |     width: 25%; | ||||||
|  |     @media (max-width: 600px) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |     @media (max-width: 1200px) and (min-width: 768px) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .fee { |   .fee { | ||||||
|     width: 30%; |     width: 30%; | ||||||
|     @media (max-width: 1060px) and (min-width: 768px) { |     text-align: end !important; | ||||||
|       text-align: start !important; |  | ||||||
|     } |  | ||||||
|     @media (max-width: 500px) { |  | ||||||
|       text-align: start !important; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .block { |   .block { | ||||||
|     width: 20%; |     width: 20%; | ||||||
|  |     @media (max-width: 1200px) and (min-width: 768px) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .status { |   .status { | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core'; | import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core'; | ||||||
| import { Observable, catchError, of, switchMap, tap } from 'rxjs'; | import { combineLatest, BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs'; | ||||||
| import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; | import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; | ||||||
| import { StateService } from '../../../services/state.service'; | import { StateService } from '../../../services/state.service'; | ||||||
| import { WebsocketService } from '../../../services/websocket.service'; | import { WebsocketService } from '../../../services/websocket.service'; | ||||||
| @ -21,9 +21,10 @@ export class AccelerationsListComponent implements OnInit { | |||||||
|   isLoading = true; |   isLoading = true; | ||||||
|   paginationMaxSize: number; |   paginationMaxSize: number; | ||||||
|   page = 1; |   page = 1; | ||||||
|   lastPage = 1; |   accelerationCount: number; | ||||||
|   maxSize = window.innerWidth <= 767.98 ? 3 : 5; |   maxSize = window.innerWidth <= 767.98 ? 3 : 5; | ||||||
|   skeletonLines: number[] = []; |   skeletonLines: number[] = []; | ||||||
|  |   pageSubject: BehaviorSubject<number> = new BehaviorSubject(this.page); | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     private servicesApiService: ServicesApiServices, |     private servicesApiService: ServicesApiServices, | ||||||
| @ -41,33 +42,46 @@ export class AccelerationsListComponent implements OnInit { | |||||||
|     this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; |     this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; | ||||||
|     this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; |     this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; | ||||||
|      |      | ||||||
|     const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistory$({ timeframe: '1m' })); |     this.accelerationList$ = this.pageSubject.pipe( | ||||||
|     this.accelerationList$ = accelerationObservable$.pipe( |       switchMap((page) => { | ||||||
|       switchMap(accelerations => { |         const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ timeframe: '1y', page: page })); | ||||||
|         if (this.pending) { |         return accelerationObservable$.pipe( | ||||||
|           for (const acceleration of accelerations) { |           switchMap(response => { | ||||||
|             acceleration.status = acceleration.status || 'accelerating'; |             let accelerations = response; | ||||||
|           } |             if (response.body) { | ||||||
|         } |               accelerations = response.body; | ||||||
|         for (const acc of accelerations) { |               this.accelerationCount = parseInt(response.headers.get('x-total-count'), 10); | ||||||
|           acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee; |             } | ||||||
|         } |             if (this.pending) { | ||||||
|         if (this.widget) { |               for (const acceleration of accelerations) { | ||||||
|           return of(accelerations.slice(-6).reverse()); |                 acceleration.status = acceleration.status || 'accelerating'; | ||||||
|         } else { |               } | ||||||
|           return of(accelerations.reverse()); |             } | ||||||
|         } |             for (const acc of accelerations) { | ||||||
|       }), |               acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee; | ||||||
|       catchError((err) => { |             } | ||||||
|         this.isLoading = false; |             if (this.widget) { | ||||||
|         return of([]); |               return of(accelerations.slice(0, 6)); | ||||||
|       }), |             } else { | ||||||
|       tap(() => { |               return of(accelerations); | ||||||
|         this.isLoading = false; |             } | ||||||
|  |           }), | ||||||
|  |           catchError((err) => { | ||||||
|  |             this.isLoading = false; | ||||||
|  |             return of([]); | ||||||
|  |           }), | ||||||
|  |           tap(() => { | ||||||
|  |             this.isLoading = false; | ||||||
|  |           }) | ||||||
|  |         ); | ||||||
|       }) |       }) | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   pageChange(page: number): void { | ||||||
|  |     this.pageSubject.next(page); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   trackByBlock(index: number, block: BlockExtended): number { |   trackByBlock(index: number, block: BlockExtended): number { | ||||||
|     return block.height; |     return block.height; | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -22,12 +22,12 @@ | |||||||
|     <div class="col"> |     <div class="col"> | ||||||
|       <div class="main-title"> |       <div class="main-title"> | ||||||
|         <span [attr.data-cy]="'acceleration-stats'" i18n="accelerator.acceleration-stats">Acceleration stats</span>  |         <span [attr.data-cy]="'acceleration-stats'" i18n="accelerator.acceleration-stats">Acceleration stats</span>  | ||||||
|         <span style="font-size: xx-small" i18n="mining.144-blocks">(1 month)</span> |         <span style="font-size: xx-small" i18n="mining.3-months">(3 months)</span> | ||||||
|       </div> |       </div> | ||||||
|       <div class="card-wrapper"> |       <div class="card-wrapper"> | ||||||
|         <div class="card"> |         <div class="card"> | ||||||
|           <div class="card-body more-padding"> |           <div class="card-body more-padding"> | ||||||
|             <app-acceleration-stats timespan="1m" [accelerations$]="minedAccelerations$"></app-acceleration-stats> |             <app-acceleration-stats></app-acceleration-stats> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| @ -59,7 +59,6 @@ | |||||||
|               [height]="graphHeight" |               [height]="graphHeight" | ||||||
|               [attr.data-cy]="'acceleration-fees'" |               [attr.data-cy]="'acceleration-fees'" | ||||||
|               [widget]=true |               [widget]=true | ||||||
|               [accelerations$]="accelerations$" |  | ||||||
|             ></app-acceleration-fees-graph> |             ></app-acceleration-fees-graph> | ||||||
|           </div> |           </div> | ||||||
|           <div class="mt-1"><a [attr.data-cy]="'acceleration-fees-view-more'" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> |           <div class="mt-1"><a [attr.data-cy]="'acceleration-fees-view-more'" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> | ||||||
| @ -84,7 +83,7 @@ | |||||||
|           <div class="title-link"> |           <div class="title-link"> | ||||||
|             <h5 class="card-title d-inline" i18n="accelerator.pending-accelerations">Active Accelerations</h5> |             <h5 class="card-title d-inline" i18n="accelerator.pending-accelerations">Active Accelerations</h5> | ||||||
|           </div> |           </div> | ||||||
|           <app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]="true" [accelerations$]="pendingAccelerations$"></app-accelerations-list> |           <app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]=true [accelerations$]="pendingAccelerations$"></app-accelerations-list> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  | |||||||
| @ -60,7 +60,7 @@ export class AcceleratorDashboardComponent implements OnInit { | |||||||
|     this.accelerations$ = this.stateService.chainTip$.pipe( |     this.accelerations$ = this.stateService.chainTip$.pipe( | ||||||
|       distinctUntilChanged(), |       distinctUntilChanged(), | ||||||
|       switchMap(() => { |       switchMap(() => { | ||||||
|         return this.serviceApiServices.getAccelerationHistory$({ timeframe: '1m' }).pipe( |         return this.serviceApiServices.getAccelerationHistory$({ timeframe: '3m', page: 1, pageLength: 100}).pipe( | ||||||
|           catchError(() => { |           catchError(() => { | ||||||
|             return of([]); |             return of([]); | ||||||
|           }), |           }), | ||||||
| @ -71,7 +71,7 @@ export class AcceleratorDashboardComponent implements OnInit { | |||||||
| 
 | 
 | ||||||
|     this.minedAccelerations$ = this.accelerations$.pipe( |     this.minedAccelerations$ = this.accelerations$.pipe( | ||||||
|       map(accelerations => { |       map(accelerations => { | ||||||
|         return accelerations.filter(acc => ['mined', 'completed', 'failed'].includes(acc.status)); |         return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status)); | ||||||
|       }) |       }) | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
| @ -128,11 +128,11 @@ export class AcceleratorDashboardComponent implements OnInit { | |||||||
|   @HostListener('window:resize', ['$event']) |   @HostListener('window:resize', ['$event']) | ||||||
|   onResize(): void { |   onResize(): void { | ||||||
|     if (window.innerWidth >= 992) { |     if (window.innerWidth >= 992) { | ||||||
|       this.graphHeight = 330; |       this.graphHeight = 380; | ||||||
|     } else if (window.innerWidth >= 768) { |     } else if (window.innerWidth >= 768) { | ||||||
|       this.graphHeight = 245; |       this.graphHeight = 300; | ||||||
|     } else { |     } else { | ||||||
|       this.graphHeight = 210; |       this.graphHeight = 270; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -59,7 +59,7 @@ export default class TxView implements TransactionStripped { | |||||||
|     this.acc = tx.acc; |     this.acc = tx.acc; | ||||||
|     this.rate = tx.rate; |     this.rate = tx.rate; | ||||||
|     this.status = tx.status; |     this.status = tx.status; | ||||||
|     this.bigintFlags = tx.flags ? (BigInt(tx.flags) ^ (this.acc ? TransactionFlags.acceleration : 0n)): 0n; |     this.bigintFlags = tx.flags ? (BigInt(tx.flags) | (this.acc ? TransactionFlags.acceleration : 0n)): 0n; | ||||||
|     this.initialised = false; |     this.initialised = false; | ||||||
|     this.vertexArray = scene.vertexArray; |     this.vertexArray = scene.vertexArray; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -28,7 +28,7 @@ | |||||||
|           <app-fee-rate [fee]="feeRate"></app-fee-rate> |           <app-fee-rate [fee]="feeRate"></app-fee-rate> | ||||||
|         </td> |         </td> | ||||||
|       </tr> |       </tr> | ||||||
|       <tr *ngIf="effectiveRate && effectiveRate !== feeRate"> |       <tr *ngIf="hasEffectiveRate && effectiveRate != null"> | ||||||
|         <td *ngIf="!this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td> |         <td *ngIf="!this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td> | ||||||
|         <td *ngIf="this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td> |         <td *ngIf="this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td> | ||||||
|         <td> |         <td> | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStra | |||||||
| import { Position } from '../../components/block-overview-graph/sprite-types.js'; | import { Position } from '../../components/block-overview-graph/sprite-types.js'; | ||||||
| import { Price } from '../../services/price.service'; | import { Price } from '../../services/price.service'; | ||||||
| import { TransactionStripped } from '../../interfaces/node-api.interface.js'; | import { TransactionStripped } from '../../interfaces/node-api.interface.js'; | ||||||
|  | import { TransactionFlags } from '../../shared/filters.utils'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-block-overview-tooltip', |   selector: 'app-block-overview-tooltip', | ||||||
| @ -22,6 +23,7 @@ export class BlockOverviewTooltipComponent implements OnChanges { | |||||||
|   feeRate = 0; |   feeRate = 0; | ||||||
|   effectiveRate; |   effectiveRate; | ||||||
|   acceleration; |   acceleration; | ||||||
|  |   hasEffectiveRate: boolean = false; | ||||||
| 
 | 
 | ||||||
|   tooltipPosition: Position = { x: 0, y: 0 }; |   tooltipPosition: Position = { x: 0, y: 0 }; | ||||||
| 
 | 
 | ||||||
| @ -55,6 +57,8 @@ export class BlockOverviewTooltipComponent implements OnChanges { | |||||||
|       this.feeRate = this.fee / this.vsize; |       this.feeRate = this.fee / this.vsize; | ||||||
|       this.effectiveRate = tx.rate; |       this.effectiveRate = tx.rate; | ||||||
|       this.acceleration = tx.acc; |       this.acceleration = tx.acc; | ||||||
|  |       this.hasEffectiveRate = Math.abs((this.fee / this.vsize) - this.effectiveRate) > 0.05 | ||||||
|  |         || (tx.bigintFlags && (tx.bigintFlags & (TransactionFlags.cpfp_child | TransactionFlags.cpfp_parent)) > 0n); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ | |||||||
|   [showFilters]="showFilters" |   [showFilters]="showFilters" | ||||||
|   [filterFlags]="filterFlags" |   [filterFlags]="filterFlags" | ||||||
|   [filterMode]="filterMode" |   [filterMode]="filterMode" | ||||||
|  |   [excludeFilters]="['nonstandard']" | ||||||
|   [overrideColors]="overrideColors" |   [overrideColors]="overrideColors" | ||||||
|   (txClickEvent)="onTxClick($event)" |   (txClickEvent)="onTxClick($event)" | ||||||
| ></app-block-overview-graph> | ></app-block-overview-graph> | ||||||
|  | |||||||
| @ -396,8 +396,11 @@ export interface Acceleration { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface AccelerationHistoryParams { | export interface AccelerationHistoryParams { | ||||||
|   timeframe?: string, |   status?: string; | ||||||
|   status?: string, |   timeframe?: string; | ||||||
|   pool?: string, |   poolUniqueId?: number; | ||||||
|   blockHash?: string, |   blockHash?: string; | ||||||
|  |   blockHeight?: number; | ||||||
|  |   page?: number; | ||||||
|  |   pageLength?: number; | ||||||
| } | } | ||||||
| @ -7,6 +7,7 @@ import { MenuGroup } from '../interfaces/services.interface'; | |||||||
| import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap } from 'rxjs'; | import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap } from 'rxjs'; | ||||||
| import { IBackendInfo } from '../interfaces/websocket.interface'; | import { IBackendInfo } from '../interfaces/websocket.interface'; | ||||||
| import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface'; | import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface'; | ||||||
|  | import { AccelerationStats } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; | ||||||
| 
 | 
 | ||||||
| export type ProductType = 'enterprise' | 'community' | 'mining_pool' | 'custom'; | export type ProductType = 'enterprise' | 'community' | 'mining_pool' | 'custom'; | ||||||
| export interface IUser { | export interface IUser { | ||||||
| @ -144,7 +145,19 @@ export class ServicesApiServices { | |||||||
|     return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations`); |     return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations`); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   getAggregatedAccelerationHistory$(params: AccelerationHistoryParams): Observable<Acceleration[]> { | ||||||
|  |     return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history/aggregated`, { params: { ...params } }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   getAccelerationHistory$(params: AccelerationHistoryParams): Observable<Acceleration[]> { |   getAccelerationHistory$(params: AccelerationHistoryParams): Observable<Acceleration[]> { | ||||||
|     return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params } }); |     return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params } }); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable<any> { | ||||||
|  |     return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'}); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getAccelerationStats$(): Observable<AccelerationStats> { | ||||||
|  |     return this.httpClient.get<AccelerationStats>(`${SERVICES_API_PREFIX}/accelerator/accelerations/stats`); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ export const TransactionFlags = { | |||||||
|   v1:                                                          0b00000100n, |   v1:                                                          0b00000100n, | ||||||
|   v2:                                                          0b00001000n, |   v2:                                                          0b00001000n, | ||||||
|   v3:                                                          0b00010000n, |   v3:                                                          0b00010000n, | ||||||
|  |   nonstandard:                                                 0b00100000n, | ||||||
|   // address types
 |   // address types
 | ||||||
|   p2pk:                                               0b00000001_00000000n, |   p2pk:                                               0b00000001_00000000n, | ||||||
|   p2ms:                                               0b00000010_00000000n, |   p2ms:                                               0b00000010_00000000n, | ||||||
| @ -66,6 +67,7 @@ export const TransactionFilters: { [key: string]: Filter } = { | |||||||
|     v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version' }, |     v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version' }, | ||||||
|     v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version' }, |     v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version' }, | ||||||
|     v3: { key: 'v3', label: 'Version 3', flag: TransactionFlags.v3, toggle: 'version' }, |     v3: { key: 'v3', label: 'Version 3', flag: TransactionFlags.v3, toggle: 'version' }, | ||||||
|  |     nonstandard: { key: 'nonstandard', label: 'Non-Standard', flag: TransactionFlags.nonstandard, important: true }, | ||||||
|     /* address types */ |     /* address types */ | ||||||
|     p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk, important: true }, |     p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk, important: true }, | ||||||
|     p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms, important: true }, |     p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms, important: true }, | ||||||
| @ -96,7 +98,7 @@ export const TransactionFilters: { [key: string]: Filter } = { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const FilterGroups: { label: string, filters: Filter[]}[] = [ | export const FilterGroups: { label: string, filters: Filter[]}[] = [ | ||||||
|   { label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'v3'] }, |   { label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'v3', 'nonstandard'] }, | ||||||
|   { label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] }, |   { label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] }, | ||||||
|   { label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement', 'acceleration'] }, |   { label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement', 'acceleration'] }, | ||||||
|   { label: 'Data', filters: ['op_return', 'fake_pubkey', 'inscription'] }, |   { label: 'Data', filters: ['op_return', 'fake_pubkey', 'inscription'] }, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user