Merge branch 'master' into natsoni/decode-tx

This commit is contained in:
natsoni 2024-12-09 12:11:50 +01:00
commit e848d711fc
No known key found for this signature in database
GPG Key ID: C65917583181743B
39 changed files with 466 additions and 120 deletions

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 83; private static currentVersion = 93;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -710,6 +710,97 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL'); await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
await this.updateToSchemaVersion(83); 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);
}
} }
/** /**

View File

@ -382,7 +382,7 @@ class MempoolBlocks {
const ancestors: Ancestor[] = []; const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = []; const descendants: Ancestor[] = [];
let ancestor: MempoolTransactionExtended let ancestor: MempoolTransactionExtended;
for (const cluster of clusters) { for (const cluster of clusters) {
for (const memberTxid of cluster) { for (const memberTxid of cluster) {
const mempoolTx = mempool[memberTxid]; const mempoolTx = mempool[memberTxid];
@ -462,7 +462,7 @@ class MempoolBlocks {
for (let i = 0; i < block.length; i++) { for (let i = 0; i < block.length; i++) {
const txid = block[i]; const txid = block[i];
if (txid) { if (txid in mempool) {
mempoolTx = mempool[txid]; mempoolTx = mempool[txid];
// save position in projected blocks // save position in projected blocks
mempoolTx.position = { mempoolTx.position = {
@ -481,6 +481,9 @@ class MempoolBlocks {
mempoolTx.acceleratedAt = acceleration?.added; mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta; mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) { for (const ancestor of mempoolTx.ancestors || []) {
if (!(ancestor.txid in mempool)) {
continue;
}
if (!mempool[ancestor.txid].acceleration) { if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true; mempool[ancestor.txid].cpfpDirty = true;
} }
@ -688,7 +691,7 @@ class MempoolBlocks {
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean }; [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) // 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).map(acc => { const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).filter(acc => acc.txid in mempoolCache).map(acc => {
let vsize = mempoolCache[acc.txid].vsize; let vsize = mempoolCache[acc.txid].vsize;
for (const ancestor of mempoolCache[acc.txid].ancestors || []) { for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
vsize += (ancestor.weight / 4); vsize += (ancestor.weight / 4);

View File

@ -1,10 +1,15 @@
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import config from '../../config'; import config from '../../config';
import pricesUpdater from '../../tasks/price-updater'; import pricesUpdater from '../../tasks/price-updater';
import logger from '../../logger';
import PricesRepository from '../../repositories/PricesRepository';
class PricesRoutes { class PricesRoutes {
public initRoutes(app: Application): void { public initRoutes(app: Application): void {
app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this)); app
.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/usd-price-history', this.$getAllPrices.bind(this))
;
} }
private $getCurrentPrices(req: Request, res: Response): void { private $getCurrentPrices(req: Request, res: Response): void {
@ -14,6 +19,23 @@ class PricesRoutes {
res.json(pricesUpdater.getLatestPrices()); res.json(pricesUpdater.getLatestPrices());
} }
private async $getAllPrices(req: Request, res: Response): Promise<void> {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR).toUTCString());
try {
const usdPriceHistory = await PricesRepository.$getPricesTimesAndId();
const responseData = usdPriceHistory.map(p => {
return { time: p.time, USD: p.USD };
});
res.status(200).json(responseData);
} catch (e: any) {
logger.err(`Exception ${e} in PricesRoutes::$getAllPrices. Code: ${e.code}. Message: ${e.message}`);
res.status(403).send();
}
}
} }
export default new PricesRoutes(); export default new PricesRoutes();

View File

@ -0,0 +1,51 @@
{
"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"
}
}
]
}
}

View File

@ -2,7 +2,7 @@
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core'; import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs'; import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
import { ServicesApiServices } from '@app/services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
import { md5, insecureRandomUUID } from '@app/shared/common.utils'; import { md5 } from '@app/shared/common.utils';
import { StateService } from '@app/services/state.service'; import { StateService } from '@app/services/state.service';
import { AudioService } from '@app/services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ETA, EtaService } from '@app/services/eta.service'; import { ETA, EtaService } from '@app/services/eta.service';
@ -94,7 +94,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
auth: IAuth | null = null; auth: IAuth | null = null;
// accelerator stuff // accelerator stuff
accelerationUUID: string;
accelerationSubscription: Subscription; accelerationSubscription: Subscription;
difficultySubscription: Subscription; difficultySubscription: Subscription;
estimateSubscription: Subscription; estimateSubscription: Subscription;
@ -138,7 +137,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private enterpriseService: EnterpriseService, private enterpriseService: EnterpriseService,
) { ) {
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1; this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
this.accelerationUUID = insecureRandomUUID();
// Check if Apple Pay available // 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 // https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
@ -388,7 +386,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationSubscription = this.servicesApiService.accelerate$( this.accelerationSubscription = this.servicesApiService.accelerate$(
this.tx.txid, this.tx.txid,
this.userBid, this.userBid,
this.accelerationUUID
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false; this.processing = false;
@ -522,7 +519,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token, tokenResult.token,
cardTag, cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID,
costUSD costUSD
).subscribe({ ).subscribe({
next: () => { next: () => {
@ -622,7 +618,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token, tokenResult.token,
cardTag, cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID,
costUSD costUSD
).subscribe({ ).subscribe({
next: () => { next: () => {
@ -713,7 +708,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token, tokenResult.token,
tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId, tokenResult.details.cashAppPay.referenceId,
this.accelerationUUID,
costUSD costUSD
).subscribe({ ).subscribe({
next: () => { next: () => {

View File

@ -1,6 +1,6 @@
<div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed"> <div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed">
<div class="timeline-wrapper"> <div class="timeline-wrapper">
@if (!tx.status.confirmed) { @if (!tx.status.confirmed || canceled) {
<div class="timeline"> <div class="timeline">
<div class="intervals"> <div class="intervals">
<div class="node-spacer"></div> <div class="node-spacer"></div>
@ -8,7 +8,7 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
@if (eta) { @if (eta && !canceled) {
~<app-time [time]="eta?.wait / 1000"></app-time> ~<app-time [time]="eta?.wait / 1000"></app-time>
} }
</div> </div>
@ -19,16 +19,20 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval-spacer"></div> <div class="interval-spacer"></div>
<div class="node"> <div class="node">
<div class="acc-to-confirmed right go-faster"></div> <div class="acc-to-confirmed right go-faster" [class.no-animation]="canceled"></div>
</div> </div>
<div class="interval-spacer"> <div class="interval-spacer">
</div> </div>
<div class="node" [id]="'confirmed'"> <div class="node" [id]="'confirmed'">
<div class="acc-to-confirmed left go-faster"></div> <div class="acc-to-confirmed left go-faster" [class.no-animation]="canceled"></div>
<div class="shape-border waiting"> <div class="shape-border waiting">
<div class="shape"></div> <div class="shape"></div>
</div> </div>
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div> @if (canceled) {
<div class="status"><span class="badge badge-danger" i18n="accelerator.canceled">Canceled</span></div>
} @else {
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
}
</div> </div>
</div> </div>
</div> </div>
@ -45,9 +49,9 @@
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
@if (tx.status.confirmed) { @if (tx.status.confirmed) {
<div class="interval-time"> <app-time [time]="acceleratedToMined"></app-time>
<app-time [time]="acceleratedToMined"></app-time> } @else if (eta && canceled) {
</div> ~<app-time [time]="eta?.wait / 1000"></app-time>
} }
</div> </div>
</div> </div>
@ -71,42 +75,42 @@
<div class="interval-spacer"> <div class="interval-spacer">
<div class="seen-to-acc"></div> <div class="seen-to-acc"></div>
</div> </div>
<div class="node" [class.accelerated]="!tx.status.confirmed" [id]="'accelerated'"> <div class="node" [class.accelerated]="!tx.status.confirmed && !canceled" [id]="'accelerated'">
<div class="seen-to-acc left"></div> <div class="seen-to-acc left"></div>
@if (tx.status.confirmed) { @if (tx.status.confirmed && !canceled) {
<div class="acc-to-confirmed right"></div> <div class="acc-to-confirmed right"></div>
} @else { } @else {
<div class="seen-to-acc right"></div> <div class="seen-to-acc right"></div>
} }
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);"> <div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
<div class="shape"></div> <div class="shape"></div>
@if (!tx.status.confirmed) { @if (!tx.status.confirmed || canceled) {
<div class="connector down loading"></div> <div class="connector down" [class.loading]="!canceled"></div>
} }
</div> </div>
@if (tx.status.confirmed) { @if (tx.status.confirmed && !canceled) {
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div> <div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
} }
<div class="time" [class.no-margin]="!tx.status.confirmed" [class.offset-left]="!tx.status.confirmed"> <div class="time" [class.no-margin]="!tx.status.confirmed || canceled" [class.offset-left]="!tx.status.confirmed || canceled">
@if (!tx.status.confirmed) { @if (!tx.status.confirmed) {
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }} <span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
} }
@if (useAbsoluteTime) { @if (useAbsoluteTime) {
<span>{{ acceleratedAt * 1000 | date }}</span> <span>{{ acceleratedAt * 1000 | date }}</span>
} @else { } @else {
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed"></app-time> <app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed || canceled"></app-time>
} }
</div> </div>
</div> </div>
<div class="interval-spacer"> <div class="interval-spacer">
@if (tx.status.confirmed) { @if (tx.status.confirmed && !canceled) {
<div class="acc-to-confirmed"></div> <div class="acc-to-confirmed"></div>
} @else { } @else {
<div class="seen-to-acc"></div> <div class="seen-to-acc"></div>
} }
</div> </div>
<div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'"> <div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'">
@if (tx.status.confirmed) { @if (tx.status.confirmed && !canceled) {
<div class="acc-to-confirmed left"></div> <div class="acc-to-confirmed left"></div>
} @else { } @else {
<div class="seen-to-acc left"></div> <div class="seen-to-acc left"></div>

View File

@ -129,6 +129,9 @@
margin-left: calc(-4em + 5px); margin-left: calc(-4em + 5px);
animation: goFasterLeft 0.8s infinite linear; animation: goFasterLeft 0.8s infinite linear;
} }
&.no-animation {
animation: none;
}
} }
&.left { &.left {

View File

@ -15,6 +15,7 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() tx: Transaction; @Input() tx: Transaction;
@Input() accelerationInfo: Acceleration; @Input() accelerationInfo: Acceleration;
@Input() eta: ETA; @Input() eta: ETA;
@Input() canceled: boolean;
now: number; now: number;
accelerateRatio: number; accelerateRatio: number;

View File

@ -10,7 +10,6 @@ import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pip
import { StateService } from '@app/services/state.service'; import { StateService } from '@app/services/state.service';
import { PriceService } from '@app/services/price.service'; import { PriceService } from '@app/services/price.service';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe'; import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
const periodSeconds = { const periodSeconds = {
'1d': (60 * 60 * 24), '1d': (60 * 60 * 24),
@ -45,6 +44,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() right: number | string = 10; @Input() right: number | string = 10;
@Input() left: number | string = 70; @Input() left: number | string = 70;
@Input() widget: boolean = false; @Input() widget: boolean = false;
@Input() defaultFiat: boolean = false;
data: any[] = []; data: any[] = [];
fiatData: any[] = []; fiatData: any[] = [];
@ -77,7 +77,6 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
private relativeUrlPipe: RelativeUrlPipe, private relativeUrlPipe: RelativeUrlPipe,
private priceService: PriceService, private priceService: PriceService,
private fiatCurrencyPipe: FiatCurrencyPipe, private fiatCurrencyPipe: FiatCurrencyPipe,
private fiatShortenerPipe: FiatShortenerPipe,
private zone: NgZone, private zone: NgZone,
) {} ) {}
@ -86,6 +85,9 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
if (!this.addressSummary$ && (!this.address || !this.stats)) { if (!this.addressSummary$ && (!this.address || !this.stats)) {
return; return;
} }
if (changes.defaultFiat) {
this.selected['Fiat'] = !!this.defaultFiat;
}
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) { if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
if (this.subscription) { if (this.subscription) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
@ -147,7 +149,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
if (!summary) { if (!summary) {
return; return;
} }
const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0); const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
let runningTotal = total; let runningTotal = total;
const processData = summary.map(d => { const processData = summary.map(d => {
@ -161,7 +163,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
d d
}; };
}).reverse(); }).reverse();
this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]); this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]);
this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]); this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]);
@ -179,6 +181,9 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0); const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue); const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue);
this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
this.chartOptions = { this.chartOptions = {
color: [ color: [
new echarts.graphic.LinearGradient(0, 0, 0, 1, [ new echarts.graphic.LinearGradient(0, 0, 0, 1, [
@ -245,21 +250,22 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
let tooltip = '<div>'; let tooltip = '<div>';
const hasTx = data[0].data[2].txid; 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) { if (hasTx) {
const header = data.length === 1 const header = data.length === 1
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}` ? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
: `${data.length} transactions`; : `${data.length} transactions`;
tooltip += `<span><b>${header}</b></span>`; tooltip += `<div><b>${header}</b></div>`;
} }
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 formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal);
const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD'); const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0); const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0);
const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0); const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0);
const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)'); const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)');
@ -291,7 +297,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} }
} }
tooltip += `</div><span>${date}</span></div>`; tooltip += `</div></div>`;
return tooltip; return tooltip;
}.bind(this) }.bind(this)
}, },
@ -311,18 +317,21 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
formatter: (val): string => { formatter: (val): string => {
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue); let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
if (valSpan > 100_000_000_000) { if (valSpan > 100_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0)} BTC`; return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0, undefined, true)} BTC`;
} }
else if (valSpan > 1_000_000_000) { else if (valSpan > 1_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2)} BTC`; return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2, undefined, true)} BTC`;
} else if (valSpan > 100_000_000) { } else if (valSpan > 100_000_000) {
return `${(val / 100_000_000).toFixed(1)} BTC`; return `${(val / 100_000_000).toFixed(1)} BTC`;
} else if (valSpan > 10_000_000) { } else if (valSpan > 10_000_000) {
return `${(val / 100_000_000).toFixed(2)} BTC`; return `${(val / 100_000_000).toFixed(2)} BTC`;
} else if (valSpan > 1_000_000) { } 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)} BTC`;
}
return `${(val / 100_000_000).toFixed(3)} BTC`; return `${(val / 100_000_000).toFixed(3)} BTC`;
} else { } else {
return `${this.amountShortenerPipe.transform(val, 0)} sats`; return `${this.amountShortenerPipe.transform(val, 0, undefined, true)} sats`;
} }
} }
}, },
@ -336,7 +345,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
axisLabel: { axisLabel: {
color: 'rgb(110, 112, 121)', color: 'rgb(110, 112, 121)',
formatter: function(val) { formatter: function(val) {
return this.fiatShortenerPipe.transform(val, null, 'USD'); return `$${this.amountShortenerPipe.transform(val, 0, undefined, true)}`;
}.bind(this) }.bind(this)
}, },
splitLine: { splitLine: {
@ -440,7 +449,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
right: this.right, right: this.right,
}] : undefined }] : undefined
}; };
if (this.chartInstance) { if (this.chartInstance) {
this.chartInstance.setOption(this.chartOptions); this.chartInstance.setOption(this.chartOptions);
} }

View File

@ -1,15 +1,17 @@
<ng-template [ngIf]="button" [ngIfElse]="btnLink"> <ng-template [ngIf]="button" [ngIfElse]="btnLink">
<button #btn [attr.data-clipboard-text]="text" [class]="class" type="button" [disabled]="text === ''"> <button [class]="class" type="button" [disabled]="text === ''" style="box-shadow: none;" (click)="copyText()">
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;top: -2px;left: 1px;"> <span style="position: relative;top: -2px;left: 1px;">
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images> <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> </span>
</button> </button>
</ng-template> </ng-template>
<ng-template #btnLink> <ng-template #btnLink>
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;"> <span style="position: relative;">
<button #btn class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" [attr.data-clipboard-text]="text"> <button class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" style="box-shadow: none;" (click)="copyText()">
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images> <app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
</button> </button>
<span *ngIf="showMessage" class="copied-message" style="top: 29px; left: -23.5px;">{{ copiedMessage }}</span>
</span> </span>
</ng-template> </ng-template>

View File

@ -7,7 +7,19 @@
padding-left: 0.4rem; padding-left: 0.4rem;
} }
img { .copied-message {
position: relative; background: color-mix(in srgb, var(--active-bg) 95%, transparent);
left: -3px; 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;
}

View File

@ -1,6 +1,4 @@
import { Component, ViewChild, ElementRef, AfterViewInit, Input, ChangeDetectionStrategy } from '@angular/core'; import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import * as ClipboardJS from 'clipboard';
import * as tlite from 'tlite';
@Component({ @Component({
selector: 'app-clipboard', selector: 'app-clipboard',
@ -8,15 +6,14 @@ import * as tlite from 'tlite';
styleUrls: ['./clipboard.component.scss'], styleUrls: ['./clipboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ClipboardComponent implements AfterViewInit { export class ClipboardComponent {
@ViewChild('btn') btn: ElementRef;
@ViewChild('buttonWrapper') buttonWrapper: ElementRef;
@Input() button = false; @Input() button = false;
@Input() class = 'btn btn-secondary ml-1'; @Input() class = 'btn btn-secondary ml-1';
@Input() size: 'small' | 'normal' | 'large' = 'normal'; @Input() size: 'small' | 'normal' | 'large' = 'normal';
@Input() text: string; @Input() text: string;
@Input() leftPadding = true; @Input() leftPadding = true;
copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`; copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`;
showMessage = false;
widths = { widths = {
small: '10', small: '10',
@ -24,22 +21,40 @@ export class ClipboardComponent implements AfterViewInit {
large: '18', large: '18',
}; };
clipboard: any; constructor(
private cd: ChangeDetectorRef,
) { }
constructor() { } async copyText() {
if (this.text && !this.showMessage) {
ngAfterViewInit() { try {
this.clipboard = new ClipboardJS(this.btn.nativeElement); await this.copyToClipboard(this.text);
this.clipboard.on('success', () => { this.showMessage = true;
tlite.show(this.buttonWrapper.nativeElement); this.cd.markForCheck();
setTimeout(() => { setTimeout(() => {
tlite.hide(this.buttonWrapper.nativeElement); this.showMessage = false;
}, 1000); this.cd.markForCheck();
}); }, 1000);
} catch (error) {
console.error('Clipboard copy failed:', error);
}
}
} }
onDestroy() { async copyToClipboard(text: string) {
this.clipboard.destroy(); 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();
}
} }
} }

View File

@ -238,7 +238,7 @@
<span>&nbsp;</span> <span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a> </a>
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [height]="graphHeight"></app-address-graph> <app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [defaultFiat]="true" [height]="graphHeight"></app-address-graph>
</div> </div>
</div> </div>
</div> </div>

View File

@ -217,10 +217,10 @@
<tr> <tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td> <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> <td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span>
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) { @if (isAcceleration && 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="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 + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span> <span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + (isAcceleration ? ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0) : 0)"></app-fiat></span>
</td> </td>
</tr> </tr>
} @else { } @else {
@ -247,7 +247,7 @@
<ng-template #effectiveRateRow> <ng-template #effectiveRateRow>
@if (!isLoadingTx) { @if (!isLoadingTx) {
@if ((cpfpInfo && hasEffectiveFeeRate) || accelerationInfo) { @if ((cpfpInfo && hasEffectiveFeeRate) || (accelerationInfo && isAcceleration)) {
<tr> <tr>
@if (isAcceleration) { @if (isAcceleration) {
<td i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td> <td i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>

View File

@ -165,12 +165,12 @@
<br> <br>
</ng-container> </ng-container>
<ng-container *ngIf="transactionTime > 0 && tx.acceleratedAt > 0 && isAcceleration"> <ng-container *ngIf="transactionTime > 0 && tx.acceleratedAt > 0 && (isAcceleration || accelerationCanceled)">
<div class="title float-left"> <div class="title float-left">
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2> <h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
<app-acceleration-timeline [transactionTime]="transactionTime" [acceleratedAt]="tx.acceleratedAt" [tx]="tx" [accelerationInfo]="accelerationInfo" [eta]="(ETA$ | async)"></app-acceleration-timeline> <app-acceleration-timeline [transactionTime]="transactionTime" [acceleratedAt]="tx.acceleratedAt" [tx]="tx" [accelerationInfo]="accelerationInfo" [eta]="(ETA$ | async)" [canceled]="accelerationCanceled"></app-acceleration-timeline>
<br> <br>
</ng-container> </ng-container>

View File

@ -107,6 +107,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
pool: Pool | null; pool: Pool | null;
auditStatus: TxAuditStatus | null; auditStatus: TxAuditStatus | null;
isAcceleration: boolean = false; isAcceleration: boolean = false;
accelerationCanceled: boolean = false;
filters: Filter[] = []; filters: Filter[] = [];
showCpfpDetails = false; showCpfpDetails = false;
miningStats: MiningStats; miningStats: MiningStats;
@ -360,16 +361,17 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
).subscribe((accelerationHistory) => { ).subscribe((accelerationHistory) => {
for (const acceleration of accelerationHistory) { for (const acceleration of accelerationHistory) {
if (acceleration.txid === this.txId) { if (acceleration.txid === this.txId) {
if (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') { if ((acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
if (acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { const boostCost = acceleration.boostCost || acceleration.bidBoost;
const boostCost = acceleration.boostCost || acceleration.bidBoost; acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; acceleration.boost = boostCost;
acceleration.boost = boostCost; this.tx.acceleratedAt = acceleration.added;
this.tx.acceleratedAt = acceleration.added; this.accelerationInfo = acceleration;
this.accelerationInfo = acceleration; }
} else { if (acceleration.status === 'failed' || acceleration.status === 'failed_provisional') {
this.tx.feeDelta = undefined; this.accelerationCanceled = true;
} this.tx.acceleratedAt = acceleration.added;
this.accelerationInfo = acceleration;
} }
this.waitingForAccelerationInfo = false; this.waitingForAccelerationInfo = false;
this.setIsAccelerated(); this.setIsAccelerated();
@ -878,6 +880,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.tx.acceleratedBy = cpfpInfo.acceleratedBy; this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
this.tx.acceleratedAt = cpfpInfo.acceleratedAt; this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
this.tx.feeDelta = cpfpInfo.feeDelta; this.tx.feeDelta = cpfpInfo.feeDelta;
this.accelerationCanceled = false;
this.setIsAccelerated(firstCpfp);
} else if (cpfpInfo.acceleratedAt) { // Acceleration was cancelled: reset acceleration state
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
this.tx.feeDelta = cpfpInfo.feeDelta;
this.accelerationCanceled = true;
this.setIsAccelerated(firstCpfp); this.setIsAccelerated(firstCpfp);
} }
@ -901,7 +910,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} }
setIsAccelerated(initialState: boolean = false) { setIsAccelerated(initialState: boolean = false) {
this.isAcceleration = ((this.tx.acceleration && (!this.tx.status.confirmed || this.waitingForAccelerationInfo)) || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id)))); this.isAcceleration =
(
(this.tx.acceleration && (!this.tx.status.confirmed || this.waitingForAccelerationInfo)) ||
(this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id)))
) &&
!this.accelerationCanceled;
if (this.isAcceleration) { if (this.isAcceleration) {
if (initialState) { if (initialState) {
this.accelerationFlowCompleted = true; this.accelerationFlowCompleted = true;

View File

@ -9339,7 +9339,7 @@ export const restApiDocsData = [
fragment: "accelerator-history", fragment: "accelerator-history",
title: "GET Acceleration History", title: "GET Acceleration History",
description: { description: {
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>" 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>"
}, },
urlString: "/v1/services/accelerator/history?status=:status&details=:details", urlString: "/v1/services/accelerator/history?status=:status&details=:details",
showConditions: [""], showConditions: [""],
@ -9449,6 +9449,36 @@ 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 = [ export const faqData = [

View File

@ -131,20 +131,20 @@ export class ServicesApiServices {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/estimate`, { txInput: txInput }, { observe: 'response' }); return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/estimate`, { txInput: txInput }, { observe: 'response' });
} }
accelerate$(txInput: string, userBid: number, accelerationUUID: string) { accelerate$(txInput: string, userBid: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID }); return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid});
} }
accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) { 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, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD }); return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
} }
accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) { 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, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD }); return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
} }
accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) { 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, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD }); return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
} }
getAccelerations$(): Observable<Acceleration[]> { getAccelerations$(): Observable<Acceleration[]> {

View File

@ -37,6 +37,7 @@ export class WebsocketService {
private isTrackingWallet: boolean = false; private isTrackingWallet: boolean = false;
private trackingWalletName: string; private trackingWalletName: string;
private trackingMempoolBlock: number; private trackingMempoolBlock: number;
private trackingMempoolBlockNetwork: string;
private stoppingTrackMempoolBlock: any | null = null; private stoppingTrackMempoolBlock: any | null = null;
private latestGitCommit = ''; private latestGitCommit = '';
private onlineCheckTimeout: number; private onlineCheckTimeout: number;
@ -226,10 +227,11 @@ export class WebsocketService {
clearTimeout(this.stoppingTrackMempoolBlock); clearTimeout(this.stoppingTrackMempoolBlock);
} }
// skip duplicate tracking requests // skip duplicate tracking requests
if (force || this.trackingMempoolBlock !== block) { if (force || this.trackingMempoolBlock !== block || this.network !== this.trackingMempoolBlockNetwork) {
this.websocketSubject.next({ 'track-mempool-block': block }); this.websocketSubject.next({ 'track-mempool-block': block });
this.isTrackingMempoolBlock = true; this.isTrackingMempoolBlock = true;
this.trackingMempoolBlock = block; this.trackingMempoolBlock = block;
this.trackingMempoolBlockNetwork = this.network;
return true; return true;
} }
return false; return false;

View File

@ -214,19 +214,6 @@ 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> { export function sleep$(ms: number): Promise<void> {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {

View File

@ -5,7 +5,7 @@
<div class="col-md-12 branding mt-2"> <div class="col-md-12 branding mt-2">
<div class="main-logo" [class]="{'services': isServicesPage}"> <div class="main-logo" [class]="{'services': isServicesPage}">
@if (enterpriseInfo?.footer_img) { @if (enterpriseInfo?.footer_img) {
<img [src]="enterpriseInfo?.footer_img" [alt]="enterpriseInfo.title" height="60px" class="mr-3"> <img [src]="enterpriseInfo?.footer_img" [alt]="enterpriseInfo.title" height="60px" class="enterprise-logo">
} @else { } @else {
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images> <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images> <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images>

View File

@ -303,6 +303,10 @@ footer .nowrap {
margin: 0 auto; margin: 0 auto;
} }
.enterprise-logo {
max-width: 100%;
}
footer .site-options { footer .site-options {
float: none; float: none;
margin-top: 15px; margin-top: 15px;

View File

@ -0,0 +1,45 @@
<!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.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -4,7 +4,6 @@ txindex=1
coinstatsindex=1 coinstatsindex=1
listen=1 listen=1
discover=1 discover=1
par=16
dbcache=8192 dbcache=8192
mempoolfullrbf=1 mempoolfullrbf=1
maxconnections=100 maxconnections=100

View File

@ -131,7 +131,7 @@ export NVM_DIR="${HOME}/.nvm"
source "${NVM_DIR}/nvm.sh" source "${NVM_DIR}/nvm.sh"
# what to look for # what to look for
frontends=(mainnet liquid onbtc) frontends=(mainnet liquid onbtc meta)
backends=(mainnet testnet testnet4 signet liquid liquidtestnet onbtc) backends=(mainnet testnet testnet4 signet liquid liquidtestnet onbtc)
frontend_repos=() frontend_repos=()
backend_repos=() backend_repos=()
@ -148,7 +148,7 @@ for repo in $backends;do
done done
# update all repos # update all repos
for repo in $backend_repos;do for repo in $frontend_repos $backend_repos;do
update_repo "${repo}" update_repo "${repo}"
done done

View File

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

View File

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

View File

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

View File

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

View File

@ -281,6 +281,26 @@ export const networks = {
routes: routes.lightning.routes, routes: routes.lightning.routes,
} }
} }
},
meta: {
title: 'Metaplanet Inc.',
description: 'Secure the Future with Bitcoin',
fallbackImg: '/resources/meta/meta-preview.png',
routes: { // only dynamic routes supported
block: routes.block,
address: routes.address,
tx: routes.tx,
mining: {
title: "Mining",
routes: {
pool: routes.mining.routes.pool,
}
},
lightning: {
title: "Lightning",
routes: routes.lightning.routes,
}
}
} }
}; };