Compare commits
81 Commits
nymkappa/h
...
5376-merge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3953a6dca | ||
|
|
78844f5787 | ||
|
|
9714789062 | ||
|
|
13405b4494 | ||
|
|
6d51ce1f38 | ||
|
|
bce9ea3661 | ||
|
|
05a21f3867 | ||
|
|
d50cfe135f | ||
|
|
8ae8430711 | ||
|
|
12daea0f62 | ||
|
|
0d31143fed | ||
|
|
7f3cdbfdb6 | ||
|
|
5be4346dc1 | ||
|
|
a0596cd366 | ||
|
|
0f14aa7ad3 | ||
|
|
44a0f92cc1 | ||
|
|
97a9ea47fc | ||
|
|
d573147ad4 | ||
|
|
679745fb6c | ||
|
|
c089920e4b | ||
|
|
62c96272d8 | ||
|
|
d87b668353 | ||
|
|
ebd4170da1 | ||
|
|
0310452dfb | ||
|
|
969687ef39 | ||
|
|
0831256cce | ||
|
|
af40cac284 | ||
|
|
e4868b70c1 | ||
|
|
5f45ce80f1 | ||
|
|
14e49126c3 | ||
|
|
a4d73130b7 | ||
|
|
cd702955fc | ||
|
|
4c66bf61f0 | ||
|
|
ffa582558b | ||
|
|
423b41939e | ||
|
|
9a81db8e6c | ||
|
|
cb3326d691 | ||
|
|
535e5313ef | ||
|
|
8bd6d40ed2 | ||
|
|
7516db0c71 | ||
|
|
abe9aa1fdc | ||
|
|
60b3f9ace6 | ||
|
|
073fe8e8cd | ||
|
|
bfe7b996a4 | ||
|
|
bfd771056d | ||
|
|
e05f5ee751 | ||
|
|
d9f3611da3 | ||
|
|
7c7419ab1c | ||
|
|
96afbca029 | ||
|
|
8c80358e71 | ||
|
|
a5fbc94182 | ||
|
|
fd7f340854 | ||
|
|
7f784944af | ||
|
|
cab01f7f26 | ||
|
|
3b4eda432f | ||
|
|
8b699da721 | ||
|
|
5b2f613856 | ||
|
|
f1e2c893cc | ||
|
|
7d3d59c348 | ||
|
|
8719b424e5 | ||
|
|
ef498b55ed | ||
|
|
9718610104 | ||
|
|
937e82bb89 | ||
|
|
91bf35bb65 | ||
|
|
f0f6ee1919 | ||
|
|
7b837b96da | ||
|
|
c417470be2 | ||
|
|
dfd7877f82 | ||
|
|
136e80e5cf | ||
|
|
b3aed2f58b | ||
|
|
e75f913af3 | ||
|
|
0a9703f164 | ||
|
|
db321c3fa5 | ||
|
|
b6aeb5661f | ||
|
|
f08fa034cc | ||
|
|
aa9888a2fe | ||
|
|
3909148d6e | ||
|
|
79e2883ebe | ||
|
|
fdbca80920 | ||
|
|
64baade3b3 | ||
|
|
4d06636d83 |
@@ -42,6 +42,7 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
||||
@@ -321,6 +322,20 @@ class BitcoinRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async getStrippedBlockTransaction(req: Request, res: Response) {
|
||||
try {
|
||||
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
|
||||
if (!transaction) {
|
||||
handleError(req, res, 404, `transaction not found in summary`);
|
||||
return;
|
||||
}
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||
res.json(transaction);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getBlock(req: Request, res: Response) {
|
||||
try {
|
||||
const block = await blocks.$getBlock(req.params.hash);
|
||||
|
||||
@@ -1224,6 +1224,11 @@ class Blocks {
|
||||
return summary.transactions;
|
||||
}
|
||||
|
||||
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
|
||||
const txs = await this.$getStrippedBlockTransactions(hash);
|
||||
return txs.find(tx => tx.txid === txid) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 15 blocks
|
||||
*
|
||||
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 83;
|
||||
private static currentVersion = 93;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -710,6 +710,97 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
|
||||
await this.updateToSchemaVersion(83);
|
||||
}
|
||||
|
||||
// add new pools indexes
|
||||
if (databaseSchemaVersion < 84 && isBitcoin === true) {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`pools\`
|
||||
ADD INDEX \`slug\` (\`slug\`),
|
||||
ADD INDEX \`unique_id\` (\`unique_id\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(84);
|
||||
}
|
||||
|
||||
// lightning channels indexes
|
||||
if (databaseSchemaVersion < 85 && isBitcoin === true) {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`channels\`
|
||||
ADD INDEX \`created\` (\`created\`),
|
||||
ADD INDEX \`capacity\` (\`capacity\`),
|
||||
ADD INDEX \`closing_reason\` (\`closing_reason\`),
|
||||
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(85);
|
||||
}
|
||||
|
||||
// lightning nodes indexes
|
||||
if (databaseSchemaVersion < 86 && isBitcoin === true) {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`nodes\`
|
||||
ADD INDEX \`status\` (\`status\`),
|
||||
ADD INDEX \`channels\` (\`channels\`),
|
||||
ADD INDEX \`country_id\` (\`country_id\`),
|
||||
ADD INDEX \`as_number\` (\`as_number\`),
|
||||
ADD INDEX \`first_seen\` (\`first_seen\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(86);
|
||||
}
|
||||
|
||||
// lightning node sockets indexes
|
||||
if (databaseSchemaVersion < 87 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
|
||||
await this.updateToSchemaVersion(87);
|
||||
}
|
||||
|
||||
// lightning stats indexes
|
||||
if (databaseSchemaVersion < 88 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
|
||||
await this.updateToSchemaVersion(88);
|
||||
}
|
||||
|
||||
// geo names indexes
|
||||
if (databaseSchemaVersion < 89 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
|
||||
await this.updateToSchemaVersion(89);
|
||||
}
|
||||
|
||||
// hashrates indexes
|
||||
if (databaseSchemaVersion < 90 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
|
||||
await this.updateToSchemaVersion(90);
|
||||
}
|
||||
|
||||
// block audits indexes
|
||||
if (databaseSchemaVersion < 91 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
|
||||
await this.updateToSchemaVersion(91);
|
||||
}
|
||||
|
||||
// elements_pegs indexes
|
||||
if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`elements_pegs\`
|
||||
ADD INDEX \`block\` (\`block\`),
|
||||
ADD INDEX \`datetime\` (\`datetime\`),
|
||||
ADD INDEX \`amount\` (\`amount\`),
|
||||
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
|
||||
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(92);
|
||||
}
|
||||
|
||||
// federation_txos indexes
|
||||
if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`federation_txos\`
|
||||
ADD INDEX \`unspent\` (\`unspent\`),
|
||||
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
|
||||
ADD INDEX \`blocktime\` (\`blocktime\`),
|
||||
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
|
||||
ADD INDEX \`expiredAt\` (\`expiredAt\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(93);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -46,8 +46,11 @@ class AccelerationApi {
|
||||
private websocketConnected: boolean = false;
|
||||
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
|
||||
private websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/';
|
||||
private _accelerations: Record<string, Acceleration> = {};
|
||||
private lastPoll = 0;
|
||||
private lastPing = Date.now();
|
||||
private lastPong = Date.now();
|
||||
private forcePoll = false;
|
||||
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
|
||||
|
||||
@@ -242,18 +245,23 @@ class AccelerationApi {
|
||||
while (this.useWebsocket) {
|
||||
this.startedWebsocketLoop = true;
|
||||
if (!this.ws) {
|
||||
this.ws = new WebSocket(`${config.MEMPOOL_SERVICES.API.replace('https://', 'ws://').replace('http://', 'ws://')}/accelerator/ws`);
|
||||
this.websocketConnected = true;
|
||||
this.ws = new WebSocket(this.websocketPath);
|
||||
this.lastPing = 0;
|
||||
|
||||
this.ws.on('open', () => {
|
||||
logger.info('Acceleration websocket opened');
|
||||
logger.info(`Acceleration websocket opened to ${this.websocketPath}`);
|
||||
this.websocketConnected = true;
|
||||
this.ws?.send(JSON.stringify({
|
||||
'watch-accelerations': true
|
||||
}));
|
||||
});
|
||||
|
||||
this.ws.on('error', (error) => {
|
||||
logger.err('Acceleration websocket error: ' + error);
|
||||
let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`;
|
||||
if (error['errors']) {
|
||||
errMsg += ' - ' + error['errors'].join(' - ');
|
||||
}
|
||||
logger.err(errMsg);
|
||||
this.ws = null;
|
||||
this.websocketConnected = false;
|
||||
});
|
||||
@@ -266,12 +274,45 @@ class AccelerationApi {
|
||||
|
||||
this.ws.on('message', (data, isBinary) => {
|
||||
try {
|
||||
const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string);
|
||||
const msg = (isBinary ? data : data.toString()) as string;
|
||||
const parsedMsg = msg?.length ? JSON.parse(msg) : null;
|
||||
this.handleWebsocketMessage(parsedMsg);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('ping', () => {
|
||||
logger.debug('received ping from acceleration websocket server');
|
||||
});
|
||||
|
||||
this.ws.on('pong', () => {
|
||||
logger.debug('received pong from acceleration websocket server');
|
||||
this.lastPong = Date.now();
|
||||
});
|
||||
} else if (this.websocketConnected) {
|
||||
if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) {
|
||||
logger.warn('No pong received within 10 seconds, terminating connection');
|
||||
try {
|
||||
this.ws?.terminate();
|
||||
} catch (e) {
|
||||
logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e));
|
||||
} finally {
|
||||
this.ws = null;
|
||||
this.websocketConnected = false;
|
||||
this.lastPing = 0;
|
||||
}
|
||||
} else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) {
|
||||
logger.debug('sending ping to acceleration websocket server');
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws?.ping();
|
||||
this.lastPing = Date.now();
|
||||
} catch (e) {
|
||||
logger.warn('failed to send ping to acceleration websocket server: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -164,7 +164,11 @@ interface IConfig {
|
||||
},
|
||||
WALLETS: {
|
||||
ENABLED: boolean;
|
||||
WALLETS: string[];
|
||||
WALLETS: {
|
||||
url: string;
|
||||
name: string;
|
||||
apiKey: string;
|
||||
}[];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
51
frontend/custom-meta-config.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -172,10 +172,6 @@
|
||||
background-color: var(--tertiary);
|
||||
}
|
||||
|
||||
.btn-small-height {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
|
||||
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { md5, insecureRandomUUID } from '@app/shared/common.utils';
|
||||
import { md5 } from '@app/shared/common.utils';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { ETA, EtaService } from '@app/services/eta.service';
|
||||
@@ -94,7 +94,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
auth: IAuth | null = null;
|
||||
|
||||
// accelerator stuff
|
||||
accelerationUUID: string;
|
||||
accelerationSubscription: Subscription;
|
||||
difficultySubscription: Subscription;
|
||||
estimateSubscription: Subscription;
|
||||
@@ -138,7 +137,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
private enterpriseService: EnterpriseService,
|
||||
) {
|
||||
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
|
||||
this.accelerationUUID = insecureRandomUUID();
|
||||
|
||||
// Check if Apple Pay available
|
||||
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
|
||||
@@ -202,6 +200,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
moveToStep(step: CheckoutStep): void {
|
||||
this.processing = false;
|
||||
this._step = step;
|
||||
if (this.timeoutTimer) {
|
||||
clearTimeout(this.timeoutTimer);
|
||||
@@ -387,7 +386,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.accelerationSubscription = this.servicesApiService.accelerate$(
|
||||
this.tx.txid,
|
||||
this.userBid,
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
@@ -521,7 +519,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
@@ -621,7 +618,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
@@ -712,7 +708,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
tokenResult.details.cashAppPay.cashtag,
|
||||
tokenResult.details.cashAppPay.referenceId,
|
||||
this.accelerationUUID,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -129,6 +129,9 @@
|
||||
margin-left: calc(-4em + 5px);
|
||||
animation: goFasterLeft 0.8s infinite linear;
|
||||
}
|
||||
&.no-animation {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="acceleration-list">
|
||||
<div class="acceleration-list" *ngIf="{ accelerations: accelerationList$ | async } as state">
|
||||
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
|
||||
<thead>
|
||||
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
@@ -21,8 +21,8 @@
|
||||
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
|
||||
</ng-container>
|
||||
</thead>
|
||||
<tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let acceleration of accelerations; let i= index;">
|
||||
<tbody *ngIf="state.accelerations && nonEmptyAccelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let acceleration of state.accelerations; let i= index;">
|
||||
<td class="txid text-left">
|
||||
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
|
||||
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
@if (hasCpfp) {
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
|
||||
}
|
||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@
|
||||
<tr>
|
||||
<td colspan="3" class="pt-0">
|
||||
<div class="d-flex justify-content-end align-items-start">
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blocks-list',
|
||||
@@ -49,6 +50,7 @@ export class BlocksList implements OnInit {
|
||||
private ogService: OpenGraphService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) {
|
||||
this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool';
|
||||
@@ -182,7 +184,7 @@ export class BlocksList implements OnInit {
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.router.navigate(['blocks', page]);
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/blocks/'), page]);
|
||||
}
|
||||
|
||||
trackByBlock(index: number, block: BlockExtended): number {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
<span> </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>
|
||||
|
||||
@@ -267,7 +267,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
if (event.key === prevKey) {
|
||||
if (this.mempoolBlocks[this.markIndex - 1]) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/mempool-block/'), this.markIndex - 1]);
|
||||
} else {
|
||||
const blocks = this.stateService.blocksSubject$.getValue();
|
||||
for (const block of (blocks || [])) {
|
||||
|
||||
@@ -217,10 +217,10 @@
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||
@if (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>
|
||||
@@ -267,7 +267,7 @@
|
||||
}
|
||||
</div>
|
||||
@if (hasCpfp) {
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="toggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="toggleCpfp()">CPFP</button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -280,7 +280,7 @@
|
||||
<ng-template #acceleratingRow>
|
||||
<tr>
|
||||
<td rowspan="2" colspan="2" style="padding: 0;">
|
||||
<app-active-acceleration-box [acceleratedBy]="tx.acceleratedBy" [effectiveFeeRate]="tx.effectiveFeePerVsize" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="showCpfpDetails = !showCpfpDetails" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
|
||||
<app-active-acceleration-box [acceleratedBy]="tx.acceleratedBy" [effectiveFeeRate]="tx.effectiveFeePerVsize" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="toggleCpfp()" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
|
||||
@@ -29,7 +29,6 @@ export class TransactionDetailsComponent implements OnInit {
|
||||
@Input() hasEffectiveFeeRate: boolean;
|
||||
@Input() cpfpInfo: CpfpInfo;
|
||||
@Input() hasCpfp: boolean;
|
||||
@Input() showCpfpDetails: boolean;
|
||||
@Input() accelerationInfo: Acceleration;
|
||||
@Input() acceleratorAvailable: boolean;
|
||||
@Input() accelerateCtaType: string;
|
||||
@@ -51,7 +50,7 @@ export class TransactionDetailsComponent implements OnInit {
|
||||
this.accelerateClicked.emit(true);
|
||||
}
|
||||
|
||||
toggleCpfp(): void {
|
||||
toggleCpfp(): void {
|
||||
this.toggleCpfp$.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
[hasEffectiveFeeRate]="hasEffectiveFeeRate"
|
||||
[cpfpInfo]="cpfpInfo"
|
||||
[hasCpfp]="hasCpfp"
|
||||
[showCpfpDetails]="showCpfpDetails"
|
||||
[accelerationInfo]="accelerationInfo"
|
||||
[replaced]="replaced"
|
||||
[isCached]="isCached"
|
||||
@@ -69,7 +68,9 @@
|
||||
<!-- CPFP Details -->
|
||||
<ng-template [ngIf]="showCpfpDetails">
|
||||
<br>
|
||||
<h2 class="text-left">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="xs"></fa-icon></h2>
|
||||
<div class="title">
|
||||
<h2 class="text-left" i18n="transaction.related-transactions|CPFP List">Related Transactions</h2>
|
||||
</div>
|
||||
<div class="box cpfp-details">
|
||||
<table class="table table-fixed table-borderless table-striped">
|
||||
<thead>
|
||||
@@ -164,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>
|
||||
|
||||
|
||||
@@ -66,10 +66,6 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-small-height {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.arrow-green {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -406,6 +408,30 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const auditAvailable = this.isAuditAvailable(height);
|
||||
const isCoinbase = this.tx.vin.some(v => v.is_coinbase);
|
||||
const fetchAudit = auditAvailable && !isCoinbase;
|
||||
|
||||
const addFirstSeen = (audit: TxAuditStatus | null, hash: string, height: number, txid: string, useFullSummary: boolean) => {
|
||||
if (
|
||||
this.isFirstSeenAvailable(height)
|
||||
&& !audit?.firstSeen // firstSeen is not already in audit
|
||||
&& (!audit || audit?.seen) // audit is disabled or tx is already seen (meaning 'firstSeen' is in block summary)
|
||||
) {
|
||||
return useFullSummary ?
|
||||
this.apiService.getStrippedBlockTransactions$(hash).pipe(
|
||||
map(strippedTxs => {
|
||||
return { audit, firstSeen: strippedTxs.find(tx => tx.txid === txid)?.time };
|
||||
}),
|
||||
catchError(() => of({ audit }))
|
||||
) :
|
||||
this.apiService.getStrippedBlockTransaction$(hash, txid).pipe(
|
||||
map(strippedTx => {
|
||||
return { audit, firstSeen: strippedTx?.time };
|
||||
}),
|
||||
catchError(() => of({ audit }))
|
||||
);
|
||||
}
|
||||
return of({ audit });
|
||||
};
|
||||
|
||||
if (fetchAudit) {
|
||||
// If block audit is already cached, use it to get transaction audit
|
||||
const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash);
|
||||
@@ -428,24 +454,31 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
accelerated: isAccelerated,
|
||||
firstSeen,
|
||||
};
|
||||
}),
|
||||
switchMap(audit => addFirstSeen(audit, hash, height, txid, true)),
|
||||
catchError(() => {
|
||||
return of({ audit: null });
|
||||
})
|
||||
)
|
||||
} else {
|
||||
return this.apiService.getBlockTxAudit$(hash, txid).pipe(
|
||||
retry({ count: 3, delay: 2000 }),
|
||||
switchMap(audit => addFirstSeen(audit, hash, height, txid, false)),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
return of({ audit: null });
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return of(isCoinbase ? { coinbase: true } : null);
|
||||
const audit = isCoinbase ? { coinbase: true } : null;
|
||||
return addFirstSeen(audit, hash, height, txid, this.apiService.getBlockSummaryLoaded(hash));
|
||||
}
|
||||
}),
|
||||
).subscribe(auditStatus => {
|
||||
this.auditStatus = auditStatus;
|
||||
if (this.auditStatus?.firstSeen) {
|
||||
this.transactionTime = this.auditStatus.firstSeen;
|
||||
this.auditStatus = auditStatus?.audit;
|
||||
const firstSeen = this.auditStatus?.firstSeen || auditStatus['firstSeen'];
|
||||
if (firstSeen) {
|
||||
this.transactionTime = firstSeen;
|
||||
}
|
||||
this.setIsAccelerated();
|
||||
});
|
||||
@@ -847,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);
|
||||
}
|
||||
|
||||
@@ -870,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;
|
||||
@@ -922,6 +967,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'testnet4':
|
||||
if (blockHeight < this.stateService.env.TESTNET4_BLOCK_AUDIT_START_HEIGHT) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'signet':
|
||||
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||
return false;
|
||||
@@ -935,6 +985,34 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return true;
|
||||
}
|
||||
|
||||
isFirstSeenAvailable(blockHeight: number): boolean {
|
||||
if (this.stateService.env.BASE_MODULE !== 'mempool') {
|
||||
return false;
|
||||
}
|
||||
switch (this.stateService.network) {
|
||||
case 'testnet':
|
||||
if (this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'testnet4':
|
||||
if (this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'signet':
|
||||
if (this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
resetTransaction() {
|
||||
this.firstLoad = false;
|
||||
this.gotInitialPosition = false;
|
||||
|
||||
@@ -9339,7 +9339,7 @@ export const restApiDocsData = [
|
||||
fragment: "accelerator-history",
|
||||
title: "GET Acceleration History",
|
||||
description: {
|
||||
default: "<p>Returns the user's past acceleration requests.</p><p>Pass one of the following for <code>:status</code>: <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",
|
||||
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 = [
|
||||
|
||||
@@ -18,6 +18,7 @@ export class ApiService {
|
||||
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
|
||||
|
||||
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
|
||||
public blockSummaryLoaded: { [hash: string]: boolean } = {};
|
||||
public blockAuditLoaded: { [hash: string]: boolean } = {};
|
||||
|
||||
constructor(
|
||||
@@ -318,9 +319,14 @@ export class ApiService {
|
||||
}
|
||||
|
||||
getStrippedBlockTransactions$(hash: string): Observable<TransactionStripped[]> {
|
||||
this.setBlockSummaryLoaded(hash);
|
||||
return this.httpClient.get<TransactionStripped[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary');
|
||||
}
|
||||
|
||||
getStrippedBlockTransaction$(hash: string, txid: string): Observable<TransactionStripped> {
|
||||
return this.httpClient.get<TransactionStripped>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/tx/' + txid + '/summary');
|
||||
}
|
||||
|
||||
getDifficultyAdjustments$(interval: string | undefined): Observable<any> {
|
||||
return this.httpClient.get<any[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustments` +
|
||||
@@ -567,4 +573,12 @@ export class ApiService {
|
||||
getBlockAuditLoaded(hash) {
|
||||
return this.blockAuditLoaded[hash];
|
||||
}
|
||||
|
||||
async setBlockSummaryLoaded(hash: string) {
|
||||
this.blockSummaryLoaded[hash] = true;
|
||||
}
|
||||
|
||||
getBlockSummaryLoaded(hash) {
|
||||
return this.blockSummaryLoaded[hash];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,20 +131,20 @@ export class ServicesApiServices {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/estimate`, { txInput: txInput }, { observe: 'response' });
|
||||
}
|
||||
|
||||
accelerate$(txInput: string, userBid: number, accelerationUUID: string) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID });
|
||||
accelerate$(txInput: string, userBid: number) {
|
||||
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) {
|
||||
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 });
|
||||
accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
|
||||
}
|
||||
|
||||
accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD });
|
||||
accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
|
||||
}
|
||||
|
||||
accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD });
|
||||
accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
|
||||
}
|
||||
|
||||
getAccelerations$(): Observable<Acceleration[]> {
|
||||
|
||||
@@ -68,7 +68,12 @@ export interface Env {
|
||||
AUDIT: boolean;
|
||||
MAINNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
TESTNET4_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
MAINNET_TX_FIRST_SEEN_START_HEIGHT: number;
|
||||
TESTNET_TX_FIRST_SEEN_START_HEIGHT: number;
|
||||
TESTNET4_TX_FIRST_SEEN_START_HEIGHT: number;
|
||||
SIGNET_TX_FIRST_SEEN_START_HEIGHT: number;
|
||||
HISTORICAL_PRICE: boolean;
|
||||
ACCELERATOR: boolean;
|
||||
ACCELERATOR_BUTTON: boolean;
|
||||
@@ -107,7 +112,12 @@ const defaultEnv: Env = {
|
||||
'AUDIT': false,
|
||||
'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'TESTNET4_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'MAINNET_TX_FIRST_SEEN_START_HEIGHT': 0,
|
||||
'TESTNET_TX_FIRST_SEEN_START_HEIGHT': 0,
|
||||
'TESTNET4_TX_FIRST_SEEN_START_HEIGHT': 0,
|
||||
'SIGNET_TX_FIRST_SEEN_START_HEIGHT': 0,
|
||||
'HISTORICAL_PRICE': true,
|
||||
'ACCELERATOR': false,
|
||||
'ACCELERATOR_BUTTON': true,
|
||||
|
||||
@@ -37,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;
|
||||
|
||||
@@ -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> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -303,6 +303,10 @@ footer .nowrap {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.enterprise-logo {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
footer .site-options {
|
||||
float: none;
|
||||
margin-top: 15px;
|
||||
|
||||
45
frontend/src/index.mempool.meta.html
Normal 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>
|
||||
BIN
frontend/src/resources/meta/favicons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
frontend/src/resources/meta/favicons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
frontend/src/resources/meta/favicons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
frontend/src/resources/meta/favicons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 698 B |
BIN
frontend/src/resources/meta/favicons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/src/resources/meta/favicons/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
frontend/src/resources/meta/favicons/site.webmanifest
Normal 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"}
|
||||
BIN
frontend/src/resources/meta/meta-preview.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
1
frontend/src/resources/metalogo.svg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
@@ -1403,4 +1403,8 @@ a {
|
||||
color: var(--fg);
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-small-height {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ txindex=1
|
||||
coinstatsindex=1
|
||||
listen=1
|
||||
discover=1
|
||||
par=16
|
||||
dbcache=8192
|
||||
mempoolfullrbf=1
|
||||
maxconnections=100
|
||||
|
||||
@@ -2014,9 +2014,9 @@ case $OS in
|
||||
moused_nondefault_enable="NO"
|
||||
|
||||
nginx_enable="YES"
|
||||
nginx_profiles="mempool"
|
||||
nginx_mempool_flags="-p /mempool"
|
||||
nginx_mempool_configfile="/mempool/mempool/nginx/nginx.conf"
|
||||
#nginx_profiles="mempool"
|
||||
#nginx_mempool_flags="-p /mempool"
|
||||
#nginx_mempool_configfile="/mempool/mempool.space/nginx/nginx.conf"
|
||||
|
||||
mysql_enable="YES"
|
||||
mysql_dbdir="/mysql"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -153,6 +153,6 @@
|
||||
},
|
||||
"WALLETS": {
|
||||
"ENABLED": true,
|
||||
"WALLETS": ["BITB"]
|
||||
"WALLETS": ["BITB", "3350"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 773911,
|
||||
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 2417829,
|
||||
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 127609,
|
||||
"MAINNET_TX_FIRST_SEEN_START_HEIGHT": 838316,
|
||||
"ITEMS_PER_PAGE": 25,
|
||||
"LIGHTNING": true,
|
||||
"ACCELERATOR": true,
|
||||
|
||||
19
production/mempool-frontend-config.meta.json
Normal 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"
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -55,10 +55,10 @@ location /api/block/ {
|
||||
rewrite ^/api/(.*) /$1 break;
|
||||
try_files /dev/null @esplora-api-cache-forever;
|
||||
}
|
||||
# other API responses cannot be cached
|
||||
# we cache for 1s to mitigate DoS attacks
|
||||
location /api/ {
|
||||
rewrite ^/api/(.*) /$1 break;
|
||||
try_files /dev/null @esplora-api-cache-disabled;
|
||||
try_files /dev/null @esplora-api-cache-minimal;
|
||||
}
|
||||
|
||||
###########
|
||||
@@ -171,6 +171,23 @@ location @esplora-api-cache-disabled {
|
||||
expires -1;
|
||||
}
|
||||
|
||||
location @esplora-api-cache-minimal {
|
||||
proxy_pass $esploraMainnet;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_cache_background_update on;
|
||||
proxy_cache_use_stale updating;
|
||||
proxy_cache apihot;
|
||||
proxy_cache_valid 200 1s;
|
||||
proxy_redirect off;
|
||||
|
||||
expires 1s;
|
||||
}
|
||||
|
||||
location @esplora-api-cache-forever {
|
||||
proxy_pass $esploraMainnet;
|
||||
|
||||
|
||||
@@ -47,10 +47,10 @@ location /testnet/api/block/ {
|
||||
rewrite ^/testnet/api/(.*) /$1 break;
|
||||
try_files /dev/null @esplora-testnet-api-cache-forever;
|
||||
}
|
||||
# other API responses cannot be cached
|
||||
# we cache for 1s to mitigate DoS attacks
|
||||
location /testnet/api/ {
|
||||
rewrite ^/testnet/api/(.*) /$1 break;
|
||||
try_files /dev/null @esplora-testnet-api-cache-disabled;
|
||||
try_files /dev/null @esplora-testnet-api-cache-minimal;
|
||||
}
|
||||
|
||||
###########
|
||||
@@ -160,3 +160,20 @@ location @esplora-testnet-api-cache-forever {
|
||||
|
||||
expires 30d;
|
||||
}
|
||||
|
||||
location @esplora-testnet-api-cache-minimal {
|
||||
proxy_pass $esploraTestnet;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_cache_background_update off;
|
||||
proxy_cache_use_stale error;
|
||||
proxy_cache apihot;
|
||||
proxy_cache_valid 200 1s;
|
||||
proxy_redirect off;
|
||||
|
||||
expires 1s;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ http {
|
||||
#listen [::]:443 ssl http2;
|
||||
server_name _;
|
||||
|
||||
# set cors headers if necessary
|
||||
set $cors_approved_origin '';
|
||||
|
||||
# tor v3
|
||||
listen 127.0.0.1:81;
|
||||
set $onion "__NGINX_MEMPOOL_ONION__";
|
||||
@@ -80,6 +83,9 @@ http {
|
||||
#listen [::]:443 ssl http2;
|
||||
server_name _;
|
||||
|
||||
# set cors headers if necessary
|
||||
set $cors_approved_origin '';
|
||||
|
||||
# tor v3
|
||||
listen 127.0.0.1:83;
|
||||
set $onion "__NGINX_LIQUID_ONION__";
|
||||
|
||||
17
production/unfurler-config.meta.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||