Compare commits

..

36 Commits

Author SHA1 Message Date
nymkappa
e3953a6dca Merge branch 'master' into 5376-merge-attempt 2024-12-09 08:56:55 +01:00
nymkappa
78844f5787 Attempt to merge master into #5376 2024-12-09 08:54:26 +01:00
wiz
9714789062 Add metaplanet routes to unfurler 2024-12-09 15:53:38 +09:00
wiz
13405b4494 ops: Start metaplanet unfurler process 2024-12-09 15:30:37 +09:00
wiz
6d51ce1f38 ops: Fix metaplanet unfurler config 2024-12-09 15:22:37 +09:00
wiz
bce9ea3661 Merge pull request #5677 from mempool/mononaut/enterprise-logo-center
center enterprise footer logo
2024-12-09 13:54:31 +09:00
Mononaut
05a21f3867 center enterprise footer logo 2024-12-09 03:30:06 +00:00
wiz
d50cfe135f Merge pull request #5674 from mempool/mononaut/balance-widget-usd
show USD series by default in address balance widget
2024-12-09 12:11:22 +09:00
wiz
8ae8430711 Merge pull request #5659 from mempool/nymkappa/internal-price-rest-api
[internal] provide internal rest api to retreive btcusd price history
2024-12-09 10:32:11 +09:00
wiz
12daea0f62 ops: Add metaplanet related configs 2024-12-09 10:22:46 +09:00
wiz
0d31143fed Merge pull request #5671 from mempool/natsoni/cancelled-accel-on-timeline
Canceled acceleration on timeline
2024-12-08 13:12:27 +09:00
Mononaut
7f3cdbfdb6 show USD series by default in address balance widget 2024-12-07 16:01:26 +00:00
nymkappa
5be4346dc1 Merge branch 'master' into nymkappa/internal-price-rest-api 2024-12-06 10:17:52 +01:00
wiz
a0596cd366 Merge pull request #5654 from mempool/natsoni/address-graph-fix
Fix address balance graph
2024-12-06 17:17:08 +09:00
wiz
0f14aa7ad3 Merge pull request #5635 from mempool/natsoni/refactor-clipboard
Refactor clipboard component
2024-12-06 17:16:26 +09:00
wiz
44a0f92cc1 Merge pull request #5627 from mempool/mononaut/custom-dash-assets
update custom dashboard assets
2024-12-06 17:15:47 +09:00
wiz
97a9ea47fc Merge pull request #5641 from mempool/mononaut/fix-mempool-network-change
fix stuck mempool block on network change
2024-12-06 17:14:35 +09:00
wiz
d573147ad4 Remove unnecessary par=16 from bitcoin.conf 2024-12-06 15:14:26 +09:00
wiz
679745fb6c Merge pull request #5668 from mempool/natsoni/fix-accel-cancellation
Fix tx frontend issues after acceleration cancellation
2024-12-06 14:30:04 +09:00
wiz
c089920e4b Merge pull request #5666 from mempool/mononaut/fix-undefined-mempooltxs
fix undefined mempool tx errors
2024-12-06 14:29:47 +09:00
natsoni
d87b668353 Show timeline on canceled accelerations 2024-12-05 12:36:17 +01:00
natsoni
0310452dfb Fix tx frontend issues after acceleration cancellation 2024-12-04 15:45:18 +01:00
Mononaut
e4868b70c1 more processBlockTemplates null checks 2024-12-02 22:51:24 +00:00
Mononaut
5f45ce80f1 filter accelerations before calculating pool positions 2024-12-02 22:51:23 +00:00
nymkappa
423b41939e [internal] provide internal rest api to retreive btcusd price history 2024-11-21 15:20:24 +01:00
natsoni
cb3326d691 Wrap large amounts in power of ten in address graph 2024-11-19 19:32:28 +01:00
natsoni
535e5313ef Polish address balance graph tooltip 2024-11-19 18:11:02 +01:00
natsoni
8bd6d40ed2 Don't use SI units in address balance graph axis 2024-11-19 18:00:00 +01:00
natsoni
7516db0c71 Fix USD y axis overflow in address graph 2024-11-19 17:45:09 +01:00
Mononaut
60b3f9ace6 update custom dashboard assets 2024-11-17 16:05:48 +00:00
Mononaut
96afbca029 fix stuck mempool block on network change 2024-11-15 01:34:06 +00:00
natsoni
cab01f7f26 Refactor clipboard to use native clipboard API 2024-11-12 17:09:58 +01:00
Mononaut
79e2883ebe update unfurler and build config 2024-07-26 14:17:55 +00:00
Mononaut
fdbca80920 check in new resources 2024-07-26 14:17:12 +00:00
Mononaut
64baade3b3 custom dashboard wallet widgets 2024-07-26 14:17:12 +00:00
Mononaut
4d06636d83 wallet tracking backend support 2024-07-26 14:17:07 +00:00
37 changed files with 405 additions and 123 deletions

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) {
if (txid in mempool) {
mempoolTx = mempool[txid];
// save position in projected blocks
mempoolTx.position = {
@@ -481,6 +481,9 @@ 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;
}
@@ -688,7 +691,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).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;
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
vsize += (ancestor.weight / 4);

View File

@@ -1,10 +1,15 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import pricesUpdater from '../../tasks/price-updater';
import logger from '../../logger';
import PricesRepository from '../../repositories/PricesRepository';
class PricesRoutes {
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 {
@@ -14,6 +19,23 @@ class PricesRoutes {
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();

View File

@@ -19,8 +19,13 @@ interface WalletAddress {
lastSync: number;
}
interface Wallet {
interface WalletConfig {
url: string;
name: string;
apiKey: string;
}
interface Wallet extends WalletConfig {
addresses: Record<string, WalletAddress>;
lastPoll: number;
}
@@ -32,10 +37,10 @@ class WalletApi {
private syncing = false;
constructor() {
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => {
acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 };
this.wallets = (config.WALLETS.WALLETS as WalletConfig[]).reduce((acc, wallet) => {
acc[wallet.name] = { ...wallet, addresses: {}, lastPoll: 0 };
return acc;
}, {} as Record<string, Wallet>) : {};
}, {} as Record<string, Wallet>);
}
public getWallet(wallet: string): Record<string, WalletAddress> {
@@ -52,16 +57,18 @@ class WalletApi {
const wallet = this.wallets[walletKey];
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
try {
const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets/${wallet.name}`);
const addresses: Record<string, WalletAddress> = response.data;
const addressList: WalletAddress[] = Object.values(addresses);
const response = await axios.get(`${wallet.url}/${wallet.name}`, { headers: { 'Authorization': `${wallet.apiKey}` } });
const data: { walletBalances: WalletAddress[] } = response.data;
const addresses = data.walletBalances;
const newAddresses: Record<string, boolean> = {};
// sync all current addresses
for (const address of addressList) {
for (const address of addresses) {
await this.$syncWalletAddress(wallet, address);
newAddresses[address.address] = true;
}
// remove old addresses
for (const address of Object.keys(wallet.addresses)) {
if (!addresses[address]) {
if (!newAddresses[address]) {
delete wallet.addresses[address];
}
}
@@ -86,10 +93,11 @@ class WalletApi {
const walletAddress: WalletAddress = {
address: address.address,
active: address.active,
transactions: summary,
transactions: await bitcoinApi.$getAddressTransactionSummary(address.address),
stats: addressInfo.chain_stats,
lastSync: Date.now(),
};
logger.debug(`Synced ${walletAddress.transactions?.length || 0} transactions for wallet ${wallet.name} address ${address.address}`);
wallet.addresses[address.address] = walletAddress;
} catch (e) {
logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`);
@@ -142,7 +150,17 @@ class WalletApi {
wallet.addresses[address].transactions?.push(txSummary);
}
if (anyMatch) {
walletTransactions[walletKey].push(tx);
for (const address of Object.keys({ ...funded, ...spent })) {
if (!walletTransactions[walletKey][address]) {
walletTransactions[walletKey][address] = [];
}
walletTransactions[walletKey][address].push({
txid: tx.txid,
value: (funded[address] ?? 0) - (spent[address] ?? 0),
height: block.height,
time: block.timestamp,
});
}
}
}
}
@@ -150,4 +168,4 @@ class WalletApi {
}
}
export default new WalletApi();
export default new WalletApi();

View File

@@ -164,7 +164,11 @@ interface IConfig {
},
WALLETS: {
ENABLED: boolean;
WALLETS: string[];
WALLETS: {
url: string;
name: string;
apiKey: string;
}[];
}
}

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

@@ -9732,9 +9732,9 @@
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -9755,7 +9755,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -9770,10 +9770,6 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/debug": {
@@ -14059,9 +14055,9 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
},
"node_modules/path-type": {
"version": "4.0.0",
@@ -25291,9 +25287,9 @@
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="
},
"express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -25314,7 +25310,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -28493,9 +28489,9 @@
}
},
"path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
},
"path-type": {
"version": "4.0.0",

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() tx: Transaction;
@Input() accelerationInfo: Acceleration;
@Input() eta: ETA;
@Input() canceled: boolean;
now: 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 { 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),
@@ -45,6 +44,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() right: number | string = 10;
@Input() left: number | string = 70;
@Input() widget: boolean = false;
@Input() defaultFiat: boolean = false;
data: any[] = [];
fiatData: any[] = [];
@@ -77,7 +77,6 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
private relativeUrlPipe: RelativeUrlPipe,
private priceService: PriceService,
private fiatCurrencyPipe: FiatCurrencyPipe,
private fiatShortenerPipe: FiatShortenerPipe,
private zone: NgZone,
) {}
@@ -86,6 +85,9 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
if (!this.addressSummary$ && (!this.address || !this.stats)) {
return;
}
if (changes.defaultFiat) {
this.selected['Fiat'] = !!this.defaultFiat;
}
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
if (this.subscription) {
this.subscription.unsubscribe();
@@ -147,7 +149,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
if (!summary) {
return;
}
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;
const processData = summary.map(d => {
@@ -161,7 +163,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
d
};
}).reverse();
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]);
@@ -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 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 = {
color: [
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
@@ -245,21 +250,22 @@ 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 += `<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 formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
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 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;
}.bind(this)
},
@@ -311,18 +317,21 @@ 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)} BTC`;
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0, undefined, true)} BTC`;
}
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) {
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)} BTC`;
}
return `${(val / 100_000_000).toFixed(3)} BTC`;
} 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: {
color: 'rgb(110, 112, 121)',
formatter: function(val) {
return this.fiatShortenerPipe.transform(val, null, 'USD');
return `$${this.amountShortenerPipe.transform(val, 0, undefined, true)}`;
}.bind(this)
},
splitLine: {
@@ -440,7 +449,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
right: this.right,
}] : undefined
};
if (this.chartInstance) {
this.chartInstance.setOption(this.chartOptions);
}

View File

@@ -1,15 +1,17 @@
<ng-template [ngIf]="button" [ngIfElse]="btnLink">
<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;">
<button [class]="class" type="button" [disabled]="text === ''" style="box-shadow: none;" (click)="copyText()">
<span 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 #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">
<span style="position: relative;">
<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>
</button>
<span *ngIf="showMessage" class="copied-message" style="top: 29px; left: -23.5px;">{{ copiedMessage }}</span>
</span>
</ng-template>

View File

@@ -7,7 +7,19 @@
padding-left: 0.4rem;
}
img {
position: relative;
left: -3px;
}
.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;
}

View File

@@ -1,6 +1,4 @@
import { Component, ViewChild, ElementRef, AfterViewInit, Input, ChangeDetectionStrategy } from '@angular/core';
import * as ClipboardJS from 'clipboard';
import * as tlite from 'tlite';
import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-clipboard',
@@ -8,15 +6,14 @@ import * as tlite from 'tlite';
styleUrls: ['./clipboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClipboardComponent implements AfterViewInit {
@ViewChild('btn') btn: ElementRef;
@ViewChild('buttonWrapper') buttonWrapper: ElementRef;
export class ClipboardComponent {
@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',
@@ -24,22 +21,40 @@ export class ClipboardComponent implements AfterViewInit {
large: '18',
};
clipboard: any;
constructor(
private cd: ChangeDetectorRef,
) { }
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 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);
}
}
}
onDestroy() {
this.clipboard.destroy();
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();
}
}
}

View File

@@ -238,7 +238,7 @@
<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>
</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>
@@ -315,4 +315,4 @@
</ng-template>
<ng-template #loadingbig>
<span class="skeleton-loader skeleton-loader-big" ></span>
</ng-template>
</ng-template>

View File

@@ -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 (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="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>
</tr>
} @else {
@@ -247,7 +247,7 @@
<ng-template #effectiveRateRow>
@if (!isLoadingTx) {
@if ((cpfpInfo && hasEffectiveFeeRate) || accelerationInfo) {
@if ((cpfpInfo && hasEffectiveFeeRate) || (accelerationInfo && isAcceleration)) {
<tr>
@if (isAcceleration) {
<td i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>

View File

@@ -165,12 +165,12 @@
<br>
</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">
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
</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>
</ng-container>

View File

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

View File

@@ -37,6 +37,7 @@ 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;
@@ -226,10 +227,11 @@ export class WebsocketService {
clearTimeout(this.stoppingTrackMempoolBlock);
}
// 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.isTrackingMempoolBlock = true;
this.trackingMempoolBlock = block;
this.trackingMempoolBlockNetwork = this.network;
return true;
}
return false;

View File

@@ -5,7 +5,7 @@
<div class="col-md-12 branding mt-2">
<div class="main-logo" [class]="{'services': isServicesPage}">
@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 {
<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>

View File

@@ -303,6 +303,10 @@ footer .nowrap {
margin: 0 auto;
}
.enterprise-logo {
max-width: 100%;
}
footer .site-options {
float: none;
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
listen=1
discover=1
par=16
dbcache=8192
mempoolfullrbf=1
maxconnections=100

View File

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

View File

@@ -153,6 +153,6 @@
},
"WALLETS": {
"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
# start unfurlers for each frontend
for site in mainnet liquid onbtc;do
for site in mainnet liquid onbtc bitb meta;do
cd "$HOME/${site}/unfurler" && \
echo "starting mempool unfurler: ${site}" && \
screen -dmS "unfurler-${site}" sh -c 'while true;do npm run unfurler;sleep 2;done'

View File

@@ -0,0 +1,17 @@
{
"SERVER": {
"HOST": "https://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,46 @@ export const networks = {
routes: routes.lightning.routes,
}
}
},
bitb: {
title: 'BITB | Bitwise Bitcoin ETF',
description: 'BITB provides low-cost access to bitcoin through a professionally managed fund',
fallbackImg: '/resources/bitb/bitb-preview.jpg',
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,
}
}
},
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,
}
}
}
};