Compare commits

..
47 changed files with 256 additions and 559 deletions

No files matched your search

+81 -16
View File
@@ -6,7 +6,7 @@ import websocketHandler from '../websocket-handler';
import mempool from '../mempool';
import feeApi from '../fee-api';
import mempoolBlocks from '../mempool-blocks';
import bitcoinApi from './bitcoin-api-factory';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin-api-factory';
import { Common } from '../common';
import backendInfo from '../backend-info';
import transactionUtils from '../transaction-utils';
@@ -21,6 +21,7 @@ import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
import BlocksRepository from '../../repositories/BlocksRepository';
class BitcoinRoutes {
public initRoutes(app: Application) {
@@ -42,7 +43,6 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
@@ -81,8 +81,87 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix)
;
}
app.get('/api/internal/health', this.generateHealthReport);
}
private async generateHealthReport(req: Request, res: Response): Promise<void> {
let response = {
core: {
height: -1
},
mempool: {
height: -1,
indexing: {
enabled: Common.indexingEnabled(),
blocks: {
count: -1,
progress: -1,
withCpfp: {
count: -1,
progress: -1,
},
withCoinStats: {
count: -1,
progress: -1,
}
},
}
},
};
try {
// Bitcoin Core
let bitcoinCoreIndexes: number | string;
try {
bitcoinCoreIndexes = await bitcoinClient.getIndexInfo();
for (const indexName in bitcoinCoreIndexes as any) {
response.core[indexName.replace(/ /g,'_')] = bitcoinCoreIndexes[indexName];
}
} catch (e: any) {
response.core['error'] = e.message;
}
try {
response.core.height = await bitcoinCoreApi.$getBlockHeightTip();
} catch (e: any) {
response.core['error'] = e.message;
}
// Mempool
response.mempool.height = blocks.getCurrentBlockHeight();
if (Common.indexingEnabled()) {
const indexingBlockAmount = (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 ? response.core.height : config.MEMPOOL.INDEXING_BLOCKS_AMOUNT);
const computeProgress = (count: number): number => Math.min(1.0, Math.round(count / indexingBlockAmount * 100) / 100);
response.mempool.indexing.blocks.count = await BlocksRepository.$getIndexedBlockCount();
response.mempool.indexing.blocks.progress = computeProgress(response.mempool.indexing.blocks.count);
response.mempool.indexing.blocks.withCpfp.count = await BlocksRepository.$getIndexedCpfpBlockCount();
response.mempool.indexing.blocks.withCpfp.progress = computeProgress(response.mempool.indexing.blocks.withCpfp.count);
response.mempool.indexing.blocks.withCoinStats.count = await BlocksRepository.$getIndexedCoinStatsBlockCount();
response.mempool.indexing.blocks.withCoinStats.progress = computeProgress(response.mempool.indexing.blocks.withCoinStats.count);
}
// Esplora
if (config.MEMPOOL.BACKEND === 'esplora') {
try {
response['esplora'] = {
height: await bitcoinApi.$getBlockHeightTip()
};
} catch (e: any) {
response['esplora'] = {
height: -1,
error: e.message
};
}
}
res.json(response);
} catch (e: any) {
logger.err(`Unable to generate health report. Exception: ${JSON.stringify(e)}`);
logger.err(e.stack);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private getInitData(req: Request, res: Response) {
try {
@@ -322,20 +401,6 @@ class BitcoinRoutes {
}
}
private async getStrippedBlockTransaction(req: Request, res: Response) {
try {
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
if (!transaction) {
handleError(req, res, 404, `transaction not found in summary`);
return;
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transaction);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlock(req: Request, res: Response) {
try {
const block = await blocks.$getBlock(req.params.hash);
-5
View File
@@ -1224,11 +1224,6 @@ class Blocks {
return summary.transactions;
}
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
const txs = await this.$getStrippedBlockTransactions(hash);
return txs.find(tx => tx.txid === txid) || null;
}
/**
* Get 15 blocks
*
+1 -92
View File
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 93;
private static currentVersion = 83;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -710,97 +710,6 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
await this.updateToSchemaVersion(83);
}
// add new pools indexes
if (databaseSchemaVersion < 84 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`pools\`
ADD INDEX \`slug\` (\`slug\`),
ADD INDEX \`unique_id\` (\`unique_id\`)
`);
await this.updateToSchemaVersion(84);
}
// lightning channels indexes
if (databaseSchemaVersion < 85 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`channels\`
ADD INDEX \`created\` (\`created\`),
ADD INDEX \`capacity\` (\`capacity\`),
ADD INDEX \`closing_reason\` (\`closing_reason\`),
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
`);
await this.updateToSchemaVersion(85);
}
// lightning nodes indexes
if (databaseSchemaVersion < 86 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`nodes\`
ADD INDEX \`status\` (\`status\`),
ADD INDEX \`channels\` (\`channels\`),
ADD INDEX \`country_id\` (\`country_id\`),
ADD INDEX \`as_number\` (\`as_number\`),
ADD INDEX \`first_seen\` (\`first_seen\`)
`);
await this.updateToSchemaVersion(86);
}
// lightning node sockets indexes
if (databaseSchemaVersion < 87 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(87);
}
// lightning stats indexes
if (databaseSchemaVersion < 88 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
await this.updateToSchemaVersion(88);
}
// geo names indexes
if (databaseSchemaVersion < 89 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
await this.updateToSchemaVersion(89);
}
// hashrates indexes
if (databaseSchemaVersion < 90 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(90);
}
// block audits indexes
if (databaseSchemaVersion < 91 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
await this.updateToSchemaVersion(91);
}
// elements_pegs indexes
if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery(`
ALTER TABLE \`elements_pegs\`
ADD INDEX \`block\` (\`block\`),
ADD INDEX \`datetime\` (\`datetime\`),
ADD INDEX \`amount\` (\`amount\`),
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
`);
await this.updateToSchemaVersion(92);
}
// federation_txos indexes
if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery(`
ALTER TABLE \`federation_txos\`
ADD INDEX \`unspent\` (\`unspent\`),
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
ADD INDEX \`blocktime\` (\`blocktime\`),
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
ADD INDEX \`expiredAt\` (\`expiredAt\`)
`);
await this.updateToSchemaVersion(93);
}
}
/**
+3 -6
View File
@@ -382,7 +382,7 @@ class MempoolBlocks {
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let ancestor: MempoolTransactionExtended;
let ancestor: MempoolTransactionExtended
for (const cluster of clusters) {
for (const memberTxid of cluster) {
const mempoolTx = mempool[memberTxid];
@@ -462,7 +462,7 @@ class MempoolBlocks {
for (let i = 0; i < block.length; i++) {
const txid = block[i];
if (txid in mempool) {
if (txid) {
mempoolTx = mempool[txid];
// save position in projected blocks
mempoolTx.position = {
@@ -481,9 +481,6 @@ class MempoolBlocks {
mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) {
if (!(ancestor.txid in mempool)) {
continue;
}
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
@@ -691,7 +688,7 @@ class MempoolBlocks {
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
} = {};
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).filter(acc => acc.txid in mempoolCache).map(acc => {
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => {
let vsize = mempoolCache[acc.txid].vsize;
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
vsize += (ancestor.weight / 4);
+5 -46
View File
@@ -46,11 +46,8 @@ class AccelerationApi {
private websocketConnected: boolean = false;
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
private websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/';
private _accelerations: Record<string, Acceleration> = {};
private lastPoll = 0;
private lastPing = Date.now();
private lastPong = Date.now();
private forcePoll = false;
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
@@ -245,23 +242,18 @@ class AccelerationApi {
while (this.useWebsocket) {
this.startedWebsocketLoop = true;
if (!this.ws) {
this.ws = new WebSocket(this.websocketPath);
this.lastPing = 0;
this.ws = new WebSocket(`${config.MEMPOOL_SERVICES.API.replace('https://', 'ws://').replace('http://', 'ws://')}/accelerator/ws`);
this.websocketConnected = true;
this.ws.on('open', () => {
logger.info(`Acceleration websocket opened to ${this.websocketPath}`);
this.websocketConnected = true;
logger.info('Acceleration websocket opened');
this.ws?.send(JSON.stringify({
'watch-accelerations': true
}));
});
this.ws.on('error', (error) => {
let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`;
if (error['errors']) {
errMsg += ' - ' + error['errors'].join(' - ');
}
logger.err(errMsg);
logger.err('Acceleration websocket error: ' + error);
this.ws = null;
this.websocketConnected = false;
});
@@ -274,45 +266,12 @@ class AccelerationApi {
this.ws.on('message', (data, isBinary) => {
try {
const msg = (isBinary ? data : data.toString()) as string;
const parsedMsg = msg?.length ? JSON.parse(msg) : null;
const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string);
this.handleWebsocketMessage(parsedMsg);
} catch (e) {
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
}
});
this.ws.on('ping', () => {
logger.debug('received ping from acceleration websocket server');
});
this.ws.on('pong', () => {
logger.debug('received pong from acceleration websocket server');
this.lastPong = Date.now();
});
} else if (this.websocketConnected) {
if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) {
logger.warn('No pong received within 10 seconds, terminating connection');
try {
this.ws?.terminate();
} catch (e) {
logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e));
} finally {
this.ws = null;
this.websocketConnected = false;
this.lastPing = 0;
}
} else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) {
logger.debug('sending ping to acceleration websocket server');
if (this.ws?.readyState === WebSocket.OPEN) {
try {
this.ws?.ping();
this.lastPing = Date.now();
} catch (e) {
logger.warn('failed to send ping to acceleration websocket server: ' + (e instanceof Error ? e.message : e));
}
}
}
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
+57 -6
View File
@@ -391,7 +391,7 @@ class BlocksRepository {
/**
* Get blocks count for a period
*/
public async $blockCountBetweenHeight(startHeight: number, endHeight: number): Promise<number> {
public async $blockCountBetweenHeight(startHeight: number, endHeight: number): Promise<number> {
const params: any[] = [];
let query = `SELECT count(height) as blockCount
FROM blocks
@@ -729,7 +729,7 @@ class BlocksRepository {
/**
* Get the historical averaged block fee rate percentiles
*/
public async $getHistoricalBlockFeeRates(div: number, interval: string | null): Promise<any> {
public async $getHistoricalBlockFeeRates(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avgHeight,
@@ -760,7 +760,7 @@ class BlocksRepository {
/**
* Get the historical averaged block sizes
*/
public async $getHistoricalBlockSizes(div: number, interval: string | null): Promise<any> {
public async $getHistoricalBlockSizes(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avgHeight,
@@ -785,7 +785,7 @@ class BlocksRepository {
/**
* Get the historical averaged block weights
*/
public async $getHistoricalBlockWeights(div: number, interval: string | null): Promise<any> {
public async $getHistoricalBlockWeights(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avgHeight,
@@ -823,7 +823,7 @@ class BlocksRepository {
/**
* Get a list of blocks that have not had CPFP data indexed
*/
public async $getCPFPUnindexedBlocks(): Promise<number[]> {
public async $getCPFPUnindexedBlocks(): Promise<number[]> {
try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
const currentBlockHeight = blockchainInfo.blocks;
@@ -893,7 +893,7 @@ class BlocksRepository {
/**
* Save block price by batch
*/
public async $saveBlockPrices(blockPrices: BlockPrice[]): Promise<void> {
public async $saveBlockPrices(blockPrices: BlockPrice[]): Promise<void> {
try {
let query = `INSERT INTO blocks_prices(height, price_id) VALUES`;
for (const price of blockPrices) {
@@ -1153,6 +1153,57 @@ class BlocksRepository {
blk.extras = <BlockExtension>extras;
return <BlockExtended>blk;
}
/**
* Count how many blocks are indexed
*/
public async $getIndexedBlockCount(): Promise<number> {
try {
const [res]: any[] = await DB.query(`SELECT COUNT(hash) as count FROM blocks`);
if (!res || !res.length) {
logger.err(`Unable to count indexed blocks in our db`);
return -1;
}
return res[0].count;
} catch (e) {
logger.err(`Unable to count indexed blocks in our db. Exception: ${JSON.stringify(e)}`);
return -1;
}
}
/**
* Count how many blocks are indexed with CPFP data
*/
public async $getIndexedCpfpBlockCount(): Promise<number> {
try {
const [res]: any[] = await DB.query(`SELECT COUNT(DISTINCT height) as count FROM compact_cpfp_clusters`);
if (!res || !res.length) {
logger.err(`Unable to count indexed blocks with CPFP data in our db`);
return -1;
}
return res[0].count;
} catch (e) {
logger.err(`Unable to count indexed blocks with CPFP data in our db. Exception: ${JSON.stringify(e)}`);
return -1;
}
}
/**
* Count how many blocks are indexed with coin stats data
*/
public async $getIndexedCoinStatsBlockCount(): Promise<number> {
try {
const [res]: any[] = await DB.query(`SELECT COUNT(hash) as count FROM blocks WHERE utxoset_size IS NOT NULL && total_input_amt IS NOT NULL`);
if (!res || !res.length) {
logger.err(`Unable to count indexed blocks with coin stats data in our db`);
return -1;
}
return res[0].count;
} catch (e) {
logger.err(`Unable to count indexed blocks with coin stats data in our db. Exception: ${JSON.stringify(e)}`);
return -1;
}
}
}
export default new BlocksRepository();
-51
View File
@@ -1,51 +0,0 @@
{
"theme": "contrast",
"enterprise": "meta",
"branding": {
"name": "metaplanet",
"title": "Metaplanet",
"site_id": 21,
"header_img": "/resources/metalogo.svg",
"footer_img": "/resources/metalogo.svg"
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"wallet": "3350"
}
},
{
"component": "twitter",
"mobileOrder": 5,
"props": {
"handle": "Metaplanet_JP"
}
},
{
"component": "wallet",
"mobileOrder": 2,
"props": {
"wallet": "3350",
"period": "all"
}
},
{
"component": "blocks"
},
{
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"wallet": "3350"
}
}
]
}
}
@@ -172,6 +172,10 @@
background-color: var(--tertiary);
}
.btn-small-height {
line-height: 1;
}
.summary-row {
display: flex;
flex-direction: row;
@@ -2,7 +2,7 @@
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
import { ServicesApiServices } from '@app/services/services-api.service';
import { md5 } from '@app/shared/common.utils';
import { md5, insecureRandomUUID } from '@app/shared/common.utils';
import { StateService } from '@app/services/state.service';
import { AudioService } from '@app/services/audio.service';
import { ETA, EtaService } from '@app/services/eta.service';
@@ -94,6 +94,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
auth: IAuth | null = null;
// accelerator stuff
accelerationUUID: string;
accelerationSubscription: Subscription;
difficultySubscription: Subscription;
estimateSubscription: Subscription;
@@ -137,6 +138,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private enterpriseService: EnterpriseService,
) {
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
this.accelerationUUID = insecureRandomUUID();
// Check if Apple Pay available
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
@@ -200,7 +202,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
moveToStep(step: CheckoutStep): void {
this.processing = false;
this._step = step;
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
@@ -386,6 +387,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationSubscription = this.servicesApiService.accelerate$(
this.tx.txid,
this.userBid,
this.accelerationUUID
).subscribe({
next: () => {
this.processing = false;
@@ -519,6 +521,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID,
costUSD
).subscribe({
next: () => {
@@ -618,6 +621,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID,
costUSD
).subscribe({
next: () => {
@@ -708,6 +712,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId,
this.accelerationUUID,
costUSD
).subscribe({
next: () => {
@@ -4,7 +4,7 @@
<div class="clearfix"></div>
<div class="acceleration-list" *ngIf="{ accelerations: accelerationList$ | async } as state">
<div class="acceleration-list">
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
<thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
@@ -21,8 +21,8 @@
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
</ng-container>
</thead>
<tbody *ngIf="state.accelerations && nonEmptyAccelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of state.accelerations; let i= index;">
<tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of accelerations; let i= index;">
<td class="txid text-left">
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>
@@ -20,7 +20,7 @@
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
<div class="d-flex justify-content-between align-items-start">
@if (hasCpfp) {
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
}
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
</div>
@@ -36,7 +36,7 @@
<tr>
<td colspan="3" class="pt-0">
<div class="d-flex justify-content-end align-items-start">
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
</div>
</td>
</tr>
@@ -10,6 +10,7 @@ import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pip
import { StateService } from '@app/services/state.service';
import { PriceService } from '@app/services/price.service';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
const periodSeconds = {
'1d': (60 * 60 * 24),
@@ -76,6 +77,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
private relativeUrlPipe: RelativeUrlPipe,
private priceService: PriceService,
private fiatCurrencyPipe: FiatCurrencyPipe,
private fiatShortenerPipe: FiatShortenerPipe,
private zone: NgZone,
) {}
@@ -243,19 +245,18 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
let tooltip = '<div>';
const hasTx = data[0].data[2].txid;
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
tooltip += `<div>
<div style="text-align: right;">
<div><b>${date}</b></div>`;
if (hasTx) {
const header = data.length === 1
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
: `${data.length} transactions`;
tooltip += `<div><b>${header}</b></div>`;
tooltip += `<span><b>${header}</b></span>`;
}
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
tooltip += `<div>
<div style="text-align: right;">`;
const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal);
const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
@@ -290,7 +291,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
}
}
tooltip += `</div></div>`;
tooltip += `</div><span>${date}</span></div>`;
return tooltip;
}.bind(this)
},
@@ -310,21 +311,18 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
formatter: (val): string => {
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
if (valSpan > 100_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0, undefined, true, true)} BTC`;
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0)} BTC`;
}
else if (valSpan > 1_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2, undefined, true, true)} BTC`;
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2)} BTC`;
} else if (valSpan > 100_000_000) {
return `${(val / 100_000_000).toFixed(1)} BTC`;
} else if (valSpan > 10_000_000) {
return `${(val / 100_000_000).toFixed(2)} BTC`;
} else if (valSpan > 1_000_000) {
if (maxValue > 100_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 3, undefined, true, true)} BTC`;
}
return `${(val / 100_000_000).toFixed(3)} BTC`;
} else {
return `${this.amountShortenerPipe.transform(val, 0, undefined, true, true)} sats`;
return `${this.amountShortenerPipe.transform(val, 0)} sats`;
}
}
},
@@ -338,7 +336,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: function(val) {
return `$${this.amountShortenerPipe.transform(val, 0, undefined, true, true)}`;
return this.fiatShortenerPipe.transform(val, null, 'USD');
}.bind(this)
},
splitLine: {
@@ -9,7 +9,6 @@ import { WebsocketService } from '@app/services/websocket.service';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-blocks-list',
@@ -50,7 +49,6 @@ export class BlocksList implements OnInit {
private ogService: OpenGraphService,
private route: ActivatedRoute,
private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
@Inject(LOCALE_ID) private locale: string,
) {
this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool';
@@ -184,7 +182,7 @@ export class BlocksList implements OnInit {
}
pageChange(page: number): void {
this.router.navigate([this.relativeUrlPipe.transform('/blocks/'), page]);
this.router.navigate(['blocks', page]);
}
trackByBlock(index: number, block: BlockExtended): number {
@@ -1,17 +1,15 @@
<ng-template [ngIf]="button" [ngIfElse]="btnLink">
<button [class]="class" type="button" [disabled]="text === ''" style="box-shadow: none;" (click)="copyText()">
<span style="position: relative;top: -2px;left: 1px;">
<button #btn [attr.data-clipboard-text]="text" [class]="class" type="button" [disabled]="text === ''">
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;top: -2px;left: 1px;">
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
<span *ngIf="showMessage" class="copied-message" style="top: 29px; left: -23.5px;">{{ copiedMessage }}</span>
</span>
</button>
</ng-template>
<ng-template #btnLink>
<span style="position: relative;">
<button class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" style="box-shadow: none;" (click)="copyText()">
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;">
<button #btn class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" [attr.data-clipboard-text]="text">
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
</button>
<span *ngIf="showMessage" class="copied-message" style="top: 29px; left: -23.5px;">{{ copiedMessage }}</span>
</span>
</ng-template>
@@ -7,19 +7,7 @@
padding-left: 0.4rem;
}
.copied-message {
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
color: var(--fg);
font-family: sans-serif;
font-size: .8rem;
font-weight: 400;
text-decoration: none;
text-align: left;
padding: .6em .75rem;
border-radius: 4px;
position: absolute;
white-space: nowrap;
box-shadow: 0 .5rem 1rem -.5rem #000;
z-index: 1000;
opacity: .9;
}
img {
position: relative;
left: -3px;
}
@@ -1,4 +1,6 @@
import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Component, ViewChild, ElementRef, AfterViewInit, Input, ChangeDetectionStrategy } from '@angular/core';
import * as ClipboardJS from 'clipboard';
import * as tlite from 'tlite';
@Component({
selector: 'app-clipboard',
@@ -6,14 +8,15 @@ import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@a
styleUrls: ['./clipboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClipboardComponent {
export class ClipboardComponent implements AfterViewInit {
@ViewChild('btn') btn: ElementRef;
@ViewChild('buttonWrapper') buttonWrapper: ElementRef;
@Input() button = false;
@Input() class = 'btn btn-secondary ml-1';
@Input() size: 'small' | 'normal' | 'large' = 'normal';
@Input() text: string;
@Input() leftPadding = true;
copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`;
showMessage = false;
widths = {
small: '10',
@@ -21,40 +24,22 @@ export class ClipboardComponent {
large: '18',
};
constructor(
private cd: ChangeDetectorRef,
) { }
clipboard: any;
async copyText() {
if (this.text && !this.showMessage) {
try {
await this.copyToClipboard(this.text);
this.showMessage = true;
this.cd.markForCheck();
setTimeout(() => {
this.showMessage = false;
this.cd.markForCheck();
}, 1000);
} catch (error) {
console.error('Clipboard copy failed:', error);
}
}
constructor() { }
ngAfterViewInit() {
this.clipboard = new ClipboardJS(this.btn.nativeElement);
this.clipboard.on('success', () => {
tlite.show(this.buttonWrapper.nativeElement);
setTimeout(() => {
tlite.hide(this.buttonWrapper.nativeElement);
}, 1000);
});
}
async copyToClipboard(text: string) {
if (navigator.clipboard) {
await navigator.clipboard.writeText(text);
} else {
// Use the 'out of viewport hidden text area' trick on non-secure contexts
const textarea = document.createElement('textarea');
textarea.value = this.text;
textarea.style.opacity = '0';
textarea.setAttribute('readonly', 'true'); // Don't trigger keyboard on mobile
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
}
onDestroy() {
this.clipboard.destroy();
}
}
@@ -267,7 +267,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
if (event.key === prevKey) {
if (this.mempoolBlocks[this.markIndex - 1]) {
this.router.navigate([this.relativeUrlPipe.transform('/mempool-block/'), this.markIndex - 1]);
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
} else {
const blocks = this.stateService.blocksSubject$.getValue();
for (const block of (blocks || [])) {
@@ -217,10 +217,10 @@
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span>
@if (isAcceleration && accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sats">sats</span>
}
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + (isAcceleration ? ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0) : 0)"></app-fiat></span>
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
</td>
</tr>
} @else {
@@ -267,7 +267,7 @@
}
</div>
@if (hasCpfp) {
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="toggleCpfp()">CPFP</button>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="toggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
}
</td>
</tr>
@@ -280,7 +280,7 @@
<ng-template #acceleratingRow>
<tr>
<td rowspan="2" colspan="2" style="padding: 0;">
<app-active-acceleration-box [acceleratedBy]="tx.acceleratedBy" [effectiveFeeRate]="tx.effectiveFeePerVsize" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="toggleCpfp()" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
<app-active-acceleration-box [acceleratedBy]="tx.acceleratedBy" [effectiveFeeRate]="tx.effectiveFeePerVsize" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="showCpfpDetails = !showCpfpDetails" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
</td>
</tr>
<tr></tr>
@@ -29,6 +29,7 @@ export class TransactionDetailsComponent implements OnInit {
@Input() hasEffectiveFeeRate: boolean;
@Input() cpfpInfo: CpfpInfo;
@Input() hasCpfp: boolean;
@Input() showCpfpDetails: boolean;
@Input() accelerationInfo: Acceleration;
@Input() acceleratorAvailable: boolean;
@Input() accelerateCtaType: string;
@@ -50,7 +51,7 @@ export class TransactionDetailsComponent implements OnInit {
this.accelerateClicked.emit(true);
}
toggleCpfp(): void {
toggleCpfp(): void {
this.toggleCpfp$.emit();
}
}
@@ -52,6 +52,7 @@
[hasEffectiveFeeRate]="hasEffectiveFeeRate"
[cpfpInfo]="cpfpInfo"
[hasCpfp]="hasCpfp"
[showCpfpDetails]="showCpfpDetails"
[accelerationInfo]="accelerationInfo"
[replaced]="replaced"
[isCached]="isCached"
@@ -68,9 +69,7 @@
<!-- CPFP Details -->
<ng-template [ngIf]="showCpfpDetails">
<br>
<div class="title">
<h2 class="text-left" i18n="transaction.related-transactions|CPFP List">Related Transactions</h2>
</div>
<h2 class="text-left">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="xs"></fa-icon></h2>
<div class="box cpfp-details">
<table class="table table-fixed table-borderless table-striped">
<thead>
@@ -66,6 +66,10 @@
color: white;
}
.btn-small-height {
line-height: 1;
}
.arrow-green {
color: var(--success);
}
@@ -406,30 +406,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
const auditAvailable = this.isAuditAvailable(height);
const isCoinbase = this.tx.vin.some(v => v.is_coinbase);
const fetchAudit = auditAvailable && !isCoinbase;
const addFirstSeen = (audit: TxAuditStatus | null, hash: string, height: number, txid: string, useFullSummary: boolean) => {
if (
this.isFirstSeenAvailable(height)
&& !audit?.firstSeen // firstSeen is not already in audit
&& (!audit || audit?.seen) // audit is disabled or tx is already seen (meaning 'firstSeen' is in block summary)
) {
return useFullSummary ?
this.apiService.getStrippedBlockTransactions$(hash).pipe(
map(strippedTxs => {
return { audit, firstSeen: strippedTxs.find(tx => tx.txid === txid)?.time };
}),
catchError(() => of({ audit }))
) :
this.apiService.getStrippedBlockTransaction$(hash, txid).pipe(
map(strippedTx => {
return { audit, firstSeen: strippedTx?.time };
}),
catchError(() => of({ audit }))
);
}
return of({ audit });
};
if (fetchAudit) {
// If block audit is already cached, use it to get transaction audit
const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash);
@@ -452,31 +428,24 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
accelerated: isAccelerated,
firstSeen,
};
}),
switchMap(audit => addFirstSeen(audit, hash, height, txid, true)),
catchError(() => {
return of({ audit: null });
})
)
} else {
return this.apiService.getBlockTxAudit$(hash, txid).pipe(
retry({ count: 3, delay: 2000 }),
switchMap(audit => addFirstSeen(audit, hash, height, txid, false)),
catchError(() => {
return of({ audit: null });
return of(null);
})
)
}
} else {
const audit = isCoinbase ? { coinbase: true } : null;
return addFirstSeen(audit, hash, height, txid, this.apiService.getBlockSummaryLoaded(hash));
return of(isCoinbase ? { coinbase: true } : null);
}
}),
).subscribe(auditStatus => {
this.auditStatus = auditStatus?.audit;
const firstSeen = this.auditStatus?.firstSeen || auditStatus['firstSeen'];
if (firstSeen) {
this.transactionTime = firstSeen;
this.auditStatus = auditStatus;
if (this.auditStatus?.firstSeen) {
this.transactionTime = this.auditStatus.firstSeen;
}
this.setIsAccelerated();
});
@@ -879,9 +848,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
this.tx.feeDelta = cpfpInfo.feeDelta;
this.setIsAccelerated(firstCpfp);
} else if (this.tx.acceleration) { // Acceleration was cancelled while on the tx page, reset acceleration state
this.tx.acceleration = false;
this.setIsAccelerated(firstCpfp);
}
if (this.notAcceleratedOnLoad === null) {
@@ -956,11 +922,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return false;
}
break;
case 'testnet4':
if (blockHeight < this.stateService.env.TESTNET4_BLOCK_AUDIT_START_HEIGHT) {
return false;
}
break;
case 'signet':
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
return false;
@@ -974,34 +935,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return true;
}
isFirstSeenAvailable(blockHeight: number): boolean {
if (this.stateService.env.BASE_MODULE !== 'mempool') {
return false;
}
switch (this.stateService.network) {
case 'testnet':
if (this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT) {
return true;
}
break;
case 'testnet4':
if (this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT) {
return true;
}
break;
case 'signet':
if (this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT) {
return true;
}
break;
default:
if (this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT) {
return true;
}
}
return false;
}
resetTransaction() {
this.firstLoad = false;
this.gotInitialPosition = false;
@@ -9339,7 +9339,7 @@ export const restApiDocsData = [
fragment: "accelerator-history",
title: "GET Acceleration History",
description: {
default: "<p>Returns the user's past acceleration requests.</p><p>Pass one of the following for <code>:status</code> (required): <code>all</code>, <code>requested</code>, <code>accelerating</code>, <code>mined</code>, <code>completed</code>, <code>failed</code>.<br>Pass <code>true</code> in <code>:details</code> to get a detailed <code>history</code> of the acceleration request.</p>"
default: "<p>Returns the user's past acceleration requests.</p><p>Pass one of the following for <code>:status</code>: <code>all</code>, <code>requested</code>, <code>accelerating</code>, <code>mined</code>, <code>completed</code>, <code>failed</code>. Pass <code>true</code> in <code>:details</code> to get a detailed <code>history</code> of the acceleration request.</p>"
},
urlString: "/v1/services/accelerator/history?status=:status&details=:details",
showConditions: [""],
@@ -9449,36 +9449,6 @@ export const restApiDocsData = [
}
}
},
{
options: { officialOnly: true },
type: "endpoint",
category: "accelerator-private",
httpRequestMethod: "POST",
fragment: "accelerator-cancel",
title: "POST Cancel Acceleration (Pro)",
description: {
default: "<p>Sends a request to cancel an acceleration in the <code>accelerating</code> status.<br>You can retreive eligible acceleration <code>id</code> using the history endpoint GET <code>/api/v1/services/accelerator/history?status=accelerating</code>."
},
urlString: "/v1/services/accelerator/cancel",
showConditions: [""],
showJsExamples: showJsExamplesDefaultFalse,
codeExample: {
default: {
codeTemplate: {
curl: `%{1}" "[[hostname]][[baseNetworkUrl]]/api/v1/services/accelerator/cancel`, //custom interpolation technique handled in replaceCurlPlaceholder()
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: ["id=42"],
headers: "X-Mempool-Auth: stacksats",
response: `HTTP/1.1 200 OK`,
},
}
}
},
];
export const faqData = [
-14
View File
@@ -18,7 +18,6 @@ export class ApiService {
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
public blockSummaryLoaded: { [hash: string]: boolean } = {};
public blockAuditLoaded: { [hash: string]: boolean } = {};
constructor(
@@ -319,14 +318,9 @@ export class ApiService {
}
getStrippedBlockTransactions$(hash: string): Observable<TransactionStripped[]> {
this.setBlockSummaryLoaded(hash);
return this.httpClient.get<TransactionStripped[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary');
}
getStrippedBlockTransaction$(hash: string, txid: string): Observable<TransactionStripped> {
return this.httpClient.get<TransactionStripped>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/tx/' + txid + '/summary');
}
getDifficultyAdjustments$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustments` +
@@ -573,12 +567,4 @@ export class ApiService {
getBlockAuditLoaded(hash) {
return this.blockAuditLoaded[hash];
}
async setBlockSummaryLoaded(hash: string) {
this.blockSummaryLoaded[hash] = true;
}
getBlockSummaryLoaded(hash) {
return this.blockSummaryLoaded[hash];
}
}
@@ -131,20 +131,20 @@ export class ServicesApiServices {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/estimate`, { txInput: txInput }, { observe: 'response' });
}
accelerate$(txInput: string, userBid: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid});
accelerate$(txInput: string, userBid: number, accelerationUUID: string) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID });
}
accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD });
}
accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD });
}
accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD });
}
getAccelerations$(): Observable<Acceleration[]> {
@@ -68,12 +68,7 @@ export interface Env {
AUDIT: boolean;
MAINNET_BLOCK_AUDIT_START_HEIGHT: number;
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
TESTNET4_BLOCK_AUDIT_START_HEIGHT: number;
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
MAINNET_TX_FIRST_SEEN_START_HEIGHT: number;
TESTNET_TX_FIRST_SEEN_START_HEIGHT: number;
TESTNET4_TX_FIRST_SEEN_START_HEIGHT: number;
SIGNET_TX_FIRST_SEEN_START_HEIGHT: number;
HISTORICAL_PRICE: boolean;
ACCELERATOR: boolean;
ACCELERATOR_BUTTON: boolean;
@@ -112,12 +107,7 @@ const defaultEnv: Env = {
'AUDIT': false,
'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0,
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
'TESTNET4_BLOCK_AUDIT_START_HEIGHT': 0,
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
'MAINNET_TX_FIRST_SEEN_START_HEIGHT': 0,
'TESTNET_TX_FIRST_SEEN_START_HEIGHT': 0,
'TESTNET4_TX_FIRST_SEEN_START_HEIGHT': 0,
'SIGNET_TX_FIRST_SEEN_START_HEIGHT': 0,
'HISTORICAL_PRICE': true,
'ACCELERATOR': false,
'ACCELERATOR_BUTTON': true,
@@ -37,7 +37,6 @@ export class WebsocketService {
private isTrackingWallet: boolean = false;
private trackingWalletName: string;
private trackingMempoolBlock: number;
private trackingMempoolBlockNetwork: string;
private stoppingTrackMempoolBlock: any | null = null;
private latestGitCommit = '';
private onlineCheckTimeout: number;
@@ -227,11 +226,10 @@ export class WebsocketService {
clearTimeout(this.stoppingTrackMempoolBlock);
}
// skip duplicate tracking requests
if (force || this.trackingMempoolBlock !== block || this.network !== this.trackingMempoolBlockNetwork) {
if (force || this.trackingMempoolBlock !== block) {
this.websocketSubject.next({ 'track-mempool-block': block });
this.isTrackingMempoolBlock = true;
this.trackingMempoolBlock = block;
this.trackingMempoolBlockNetwork = this.network;
return true;
}
return false;
+13
View File
@@ -214,6 +214,19 @@ export function renderSats(value: number, network: string, mode: 'sats' | 'btc'
}
}
export function insecureRandomUUID(): string {
const hexDigits = '0123456789abcdef';
const uuidLengths = [8, 4, 4, 4, 12];
let uuid = '';
for (const length of uuidLengths) {
for (let i = 0; i < length; i++) {
uuid += hexDigits[Math.floor(Math.random() * 16)];
}
uuid += '-';
}
return uuid.slice(0, -1);
}
export function sleep$(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
@@ -5,10 +5,9 @@ import { Pipe, PipeTransform } from '@angular/core';
})
export class AmountShortenerPipe implements PipeTransform {
transform(num: number, ...args: any[]): unknown {
let digits = args[0] ?? 1;
const digits = args[0] ?? 1;
const unit = args[1] || undefined;
const isMoney = args[2] || false;
const addMoreDigits = args[3] || false;
if (num < 1000) {
return num.toFixed(digits);
@@ -26,12 +25,6 @@ export class AmountShortenerPipe implements PipeTransform {
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
const item = lookup.slice().reverse().find((item) => num >= item.value);
if (addMoreDigits) {
// Add more decimal digits if the integer part is small
const integerPartLength = Math.floor(num / item.value).toString().length;
digits += Math.max(0, 3 - integerPartLength);
}
if (unit !== undefined) {
return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + ' ' + item.symbol + unit : '0';
} else {
-45
View File
@@ -1,45 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Metaplanet Inc</title>
<script src="/resources/config.js"></script>
<script src="/resources/customize.js"></script>
<base href="/">
<meta name="description" content="Secure the Future with Bitcoin." />
<meta property="og:image" content="https://mempool.space/resources/meta/meta-preview.jpg" />
<meta property="og:image:type" content="image/jpeg" />
<meta property="og:image:width" content="2000" />
<meta property="og:image:height" content="1000" />
<meta property="og:description" content="Secure the Future with Bitcoin." />
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@mempool">
<meta name="twitter:creator" content="@mempool">
<meta name="twitter:title" content="Metaplanet Inc">
<meta name="twitter:description" content="Secure the Future with Bitcoin." />
<meta name="twitter:image" content="https://mempool.space/resources/meta/meta-preview.jpg" />
<meta name="twitter:domain" content="metaplanet.mempool.space">
<link rel="apple-touch-icon" sizes="180x180" href="/resources/meta/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/resources/meta/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/resources/meta/favicons/favicon-16x16.png">
<link rel="manifest" href="/resources/meta/favicons/site.webmanifest">
<link rel="shortcut icon" href="/resources/meta/favicons/favicon.ico">
<link id="canonical" rel="canonical" href="https://metaplanet.mempool.space">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-config" content="/resources/favicons/browserconfig.xml">
<meta name="theme-color" content="#1d1f31">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<app-root></app-root>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

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

Before

Width:  |  Height:  |  Size: 2.4 MiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1536" height="269" viewBox="0 0 1536 269" version="1.1"><path d="M 50 134.378 L 50 219 64.990 219 L 79.980 219 80.240 171.850 L 80.500 124.701 106.566 150.850 C 120.902 165.233, 133.274 177, 134.059 177 C 134.844 177, 147.302 165.191, 161.743 150.757 L 188 124.514 188 171.757 L 188 219 203 219 L 218 219 218 134.254 L 218 49.509 176.098 91.402 L 134.195 133.296 93.848 92.866 C 71.656 70.630, 52.712 51.833, 51.750 51.096 C 50.087 49.821, 50 53.941, 50 134.378 M 554.094 52.022 C 553.505 53.385, 550.580 60.575, 547.595 68 C 544.609 75.425, 538.473 90.500, 533.961 101.500 C 529.448 112.500, 524.955 123.525, 523.975 126 C 522.996 128.475, 518.589 139.275, 514.182 150 C 509.776 160.725, 502.075 179.625, 497.068 192 C 492.062 204.375, 487.532 215.516, 487.002 216.757 L 486.037 219.014 554.768 218.757 C 599.944 218.588, 623.422 218.157, 623.271 217.500 C 623.145 216.950, 615.441 197.600, 606.150 174.500 C 596.859 151.400, 586.002 124.400, 582.024 114.500 C 578.046 104.600, 574.206 95.150, 573.490 93.500 C 572.269 90.687, 568.047 80.246, 563.986 70 C 559.552 58.812, 557.185 53.154, 556.214 51.419 C 555.229 49.657, 555.100 49.694, 554.094 52.022 M 987.040 52.723 C 986.321 54.250, 984.723 57.975, 983.487 61 C 982.251 64.025, 978.660 72.800, 975.506 80.500 C 972.352 88.200, 968.966 96.525, 967.983 99 C 966.492 102.751, 957.756 124.075, 947.499 149 C 946.254 152.025, 941.758 163.050, 937.507 173.500 C 925.865 202.117, 925.493 203.024, 922.645 209.720 C 921.190 213.141, 920 216.628, 920 217.470 C 920 218.828, 927.627 219, 988 219 C 1043.191 219, 1056 218.755, 1056 217.698 C 1056 216.981, 1050.997 204.044, 1044.882 188.948 C 1038.767 173.851, 1032.917 159.363, 1031.882 156.750 C 1030.847 154.137, 1029.154 149.863, 1028.120 147.250 C 1015.372 115.043, 989.573 51.321, 989.039 50.723 C 988.658 50.295, 987.758 51.195, 987.040 52.723 M 1072 136.950 L 1072 219 1088.500 219 L 1105 219 1105.035 177.750 L 1105.070 136.500 1113.142 145 C 1117.581 149.675, 1125.333 157.775, 1130.369 163 C 1135.405 168.225, 1148.960 182.400, 1160.491 194.500 C 1172.022 206.600, 1185.966 221.148, 1191.478 226.830 C 1196.990 232.511, 1201.860 237.798, 1202.300 238.580 C 1202.740 239.361, 1203.527 240, 1204.050 240 C 1204.642 240, 1205 208.773, 1205 157 L 1205 74 1188.511 74 L 1172.022 74 1171.761 116.058 L 1171.500 158.117 1155.085 140.808 C 1146.056 131.289, 1133.670 118.325, 1127.560 112 C 1121.451 105.675, 1107.027 90.600, 1095.507 78.500 C 1083.988 66.400, 1073.986 56.140, 1073.282 55.700 C 1072.243 55.053, 1072 70.485, 1072 136.950 M 232.460 75.250 C 232.197 75.938, 232.099 108.450, 232.241 147.500 L 232.500 218.500 295.250 218.758 L 358 219.015 358 202.508 L 358 186 312 186 L 266 186 266 171.500 L 266 157 309.025 157 L 352.050 157 351.775 140.750 L 351.500 124.500 308.750 124.239 L 266 123.979 266 115.489 L 266 107 312 107 L 358 107 358 90.500 L 358 74 295.469 74 C 246.365 74, 232.835 74.268, 232.460 75.250 M 372 90.500 L 372 107 395 107 L 418 107 418 163.024 L 418 219.048 434.750 218.774 L 451.500 218.500 451.758 162.750 L 452.017 107 474.508 107 L 497 107 497 90.500 L 497 74 434.500 74 L 372 74 372 90.500 M 638.460 75.250 C 638.197 75.938, 638.099 108.450, 638.241 147.500 L 638.500 218.500 655.250 218.774 L 672 219.048 672 190.524 L 672 162 697.816 162 C 712.150 162, 726.251 161.516, 729.520 160.912 C 743.231 158.377, 755.459 148.689, 760.510 136.359 C 766.597 121.499, 764.578 102.994, 755.631 91.634 C 748.750 82.898, 742.357 78.800, 731.090 75.904 C 724.374 74.178, 719.711 74, 681.309 74 C 648.333 74, 638.831 74.277, 638.460 75.250 M 779 146.500 L 779 219 841.500 219 L 904 219 904 202.500 L 904 186 858 186 L 812 186 812 130 L 812 74 795.500 74 L 779 74 779 146.500 M 1220 146.500 L 1220 219.015 1282.750 218.758 L 1345.500 218.500 1345.500 202.500 L 1345.500 186.500 1299.750 186.240 L 1254 185.980 1254 171.490 L 1254 157 1297.025 157 L 1340.050 157 1339.775 140.750 L 1339.500 124.500 1296.750 124.239 L 1254 123.979 1254 115.499 L 1254 107.020 1299.750 106.760 L 1345.500 106.500 1345.500 90.500 L 1345.500 74.500 1282.750 74.242 L 1220 73.985 1220 146.500 M 1360.441 75.294 C 1360.168 76.006, 1360.069 83.318, 1360.222 91.544 L 1360.500 106.500 1383.242 106.769 L 1405.983 107.038 1406.242 162.769 L 1406.500 218.500 1423.250 218.774 L 1440 219.048 1440 163.024 L 1440 107 1463 107 L 1486 107 1486 90.500 L 1486 74 1423.469 74 C 1373.234 74, 1360.840 74.254, 1360.441 75.294 M 672 117.939 L 672 129 697.750 128.978 C 720.637 128.958, 723.737 128.763, 725.633 127.228 C 729.850 123.812, 731.223 117.778, 728.929 112.743 C 726.660 107.765, 725.297 107.522, 697.750 107.189 L 672 106.877 672 117.939 M 554.728 139.151 C 554.420 139.893, 550.267 150.175, 545.500 162 C 540.733 173.825, 536.597 184.063, 536.309 184.750 C 535.901 185.725, 539.995 186, 554.893 186 C 565.402 186, 574 185.881, 574 185.736 C 574 185.408, 570.673 176.682, 568 170 C 566.900 167.250, 565.100 162.750, 564 160 C 562.900 157.250, 561.053 152.637,Line truncated

Before

Width:  |  Height:  |  Size: 5.4 KiB

+1 -5
View File
@@ -1403,8 +1403,4 @@ a {
color: var(--fg);
background-color: var(--primary);
border-color: var(--primary);
}
.btn-small-height {
line-height: 1;
}
}
+1
View File
@@ -4,6 +4,7 @@ txindex=1
coinstatsindex=1
listen=1
discover=1
par=16
dbcache=8192
mempoolfullrbf=1
maxconnections=100
+3 -3
View File
@@ -2014,9 +2014,9 @@ case $OS in
moused_nondefault_enable="NO"
nginx_enable="YES"
#nginx_profiles="mempool"
#nginx_mempool_flags="-p /mempool"
#nginx_mempool_configfile="/mempool/mempool.space/nginx/nginx.conf"
nginx_profiles="mempool"
nginx_mempool_flags="-p /mempool"
nginx_mempool_configfile="/mempool/mempool/nginx/nginx.conf"
mysql_enable="YES"
mysql_dbdir="/mysql"
+1 -1
View File
@@ -153,6 +153,6 @@
},
"WALLETS": {
"ENABLED": true,
"WALLETS": ["BITB", "3350"]
"WALLETS": ["BITB"]
}
}
@@ -13,7 +13,6 @@
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 773911,
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 2417829,
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 127609,
"MAINNET_TX_FIRST_SEEN_START_HEIGHT": 838316,
"ITEMS_PER_PAGE": 25,
"LIGHTNING": true,
"ACCELERATOR": true,
+2 -19
View File
@@ -55,10 +55,10 @@ location /api/block/ {
rewrite ^/api/(.*) /$1 break;
try_files /dev/null @esplora-api-cache-forever;
}
# we cache for 1s to mitigate DoS attacks
# other API responses cannot be cached
location /api/ {
rewrite ^/api/(.*) /$1 break;
try_files /dev/null @esplora-api-cache-minimal;
try_files /dev/null @esplora-api-cache-disabled;
}
###########
@@ -171,23 +171,6 @@ location @esplora-api-cache-disabled {
expires -1;
}
location @esplora-api-cache-minimal {
proxy_pass $esploraMainnet;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache apihot;
proxy_cache_valid 200 1s;
proxy_redirect off;
expires 1s;
}
location @esplora-api-cache-forever {
proxy_pass $esploraMainnet;
+2 -19
View File
@@ -47,10 +47,10 @@ location /testnet/api/block/ {
rewrite ^/testnet/api/(.*) /$1 break;
try_files /dev/null @esplora-testnet-api-cache-forever;
}
# we cache for 1s to mitigate DoS attacks
# other API responses cannot be cached
location /testnet/api/ {
rewrite ^/testnet/api/(.*) /$1 break;
try_files /dev/null @esplora-testnet-api-cache-minimal;
try_files /dev/null @esplora-testnet-api-cache-disabled;
}
###########
@@ -160,20 +160,3 @@ location @esplora-testnet-api-cache-forever {
expires 30d;
}
location @esplora-testnet-api-cache-minimal {
proxy_pass $esploraTestnet;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update off;
proxy_cache_use_stale error;
proxy_cache apihot;
proxy_cache_valid 200 1s;
proxy_redirect off;
expires 1s;
}
-6
View File
@@ -42,9 +42,6 @@ http {
#listen [::]:443 ssl http2;
server_name _;
# set cors headers if necessary
set $cors_approved_origin '';
# tor v3
listen 127.0.0.1:81;
set $onion "__NGINX_MEMPOOL_ONION__";
@@ -83,9 +80,6 @@ http {
#listen [::]:443 ssl http2;
server_name _;
# set cors headers if necessary
set $cors_approved_origin '';
# tor v3
listen 127.0.0.1:83;
set $onion "__NGINX_LIQUID_ONION__";