Compare commits
24 Commits
5376-merge
...
orangesurf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00cb2ffc75 | ||
|
|
160b9217b9 | ||
|
|
37acc4493a | ||
|
|
6d4f03e5f2 | ||
|
|
2d2f3ad4c4 | ||
|
|
70036e4a7e | ||
|
|
4daa997e58 | ||
|
|
15dd4cd633 | ||
|
|
8b73bdfba9 | ||
|
|
4fe246ecf1 | ||
|
|
90bb5304ef | ||
|
|
ded47eb309 | ||
|
|
aef361b01a | ||
|
|
392f6a01c4 | ||
|
|
6112c7f8ee | ||
|
|
58b4c07924 | ||
|
|
e58579ed8a | ||
|
|
f57eb047f6 | ||
|
|
dcae94ba66 | ||
|
|
1d2a5e9c94 | ||
|
|
522b4d914f | ||
|
|
2372d8cff3 | ||
|
|
59cefc2b4b | ||
|
|
526625fc56 |
@@ -1,12 +1,12 @@
|
||||
import config from '../../config';
|
||||
import axios, { AxiosResponse, isAxiosError } from 'axios';
|
||||
import axios, { isAxiosError } from 'axios';
|
||||
import http from 'http';
|
||||
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import logger from '../../logger';
|
||||
import { Common } from '../common';
|
||||
import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
|
||||
import os from 'os';
|
||||
interface FailoverHost {
|
||||
host: string,
|
||||
rtts: number[],
|
||||
@@ -20,6 +20,13 @@ interface FailoverHost {
|
||||
preferred?: boolean,
|
||||
checked: boolean,
|
||||
lastChecked?: number,
|
||||
publicDomain: string,
|
||||
hashes: {
|
||||
frontend?: string,
|
||||
backend?: string,
|
||||
electrs?: string,
|
||||
lastUpdated: number,
|
||||
}
|
||||
}
|
||||
|
||||
class FailoverRouter {
|
||||
@@ -29,14 +36,21 @@ class FailoverRouter {
|
||||
maxHeight: number = 0;
|
||||
hosts: FailoverHost[];
|
||||
multihost: boolean;
|
||||
pollInterval: number = 60000;
|
||||
gitHashInterval: number = 600000; // 10 minutes
|
||||
pollInterval: number = 60000; // 1 minute
|
||||
pollTimer: NodeJS.Timeout | null = null;
|
||||
pollConnection = axios.create();
|
||||
localHostname: string = 'localhost';
|
||||
requestConnection = axios.create({
|
||||
httpAgent: new http.Agent({ keepAlive: true })
|
||||
});
|
||||
|
||||
constructor() {
|
||||
try {
|
||||
this.localHostname = os.hostname();
|
||||
} catch (e) {
|
||||
logger.warn('Failed to set local hostname, using "localhost"');
|
||||
}
|
||||
// setup list of hosts
|
||||
this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
|
||||
return {
|
||||
@@ -45,6 +59,10 @@ class FailoverRouter {
|
||||
rtts: [],
|
||||
rtt: Infinity,
|
||||
failures: 0,
|
||||
publicDomain: 'https://' + this.extractPublicDomain(domain),
|
||||
hashes: {
|
||||
lastUpdated: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
this.activeHost = {
|
||||
@@ -55,6 +73,10 @@ class FailoverRouter {
|
||||
socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
|
||||
preferred: true,
|
||||
checked: false,
|
||||
publicDomain: `http://${this.localHostname}`,
|
||||
hashes: {
|
||||
lastUpdated: 0,
|
||||
},
|
||||
};
|
||||
this.fallbackHost = this.activeHost;
|
||||
this.hosts.unshift(this.activeHost);
|
||||
@@ -106,6 +128,24 @@ class FailoverRouter {
|
||||
host.outOfSync = false;
|
||||
}
|
||||
host.unreachable = false;
|
||||
|
||||
// update esplora git hash using the x-powered-by header from the height check
|
||||
const poweredBy = result.headers['x-powered-by'];
|
||||
if (poweredBy) {
|
||||
const match = poweredBy.match(/([a-fA-F0-9]{5,40})/);
|
||||
if (match && match[1]?.length) {
|
||||
host.hashes.electrs = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Check front and backend git hashes less often
|
||||
if (Date.now() - host.hashes.lastUpdated > this.gitHashInterval) {
|
||||
await Promise.all([
|
||||
this.$updateFrontendGitHash(host),
|
||||
this.$updateBackendGitHash(host)
|
||||
]);
|
||||
host.hashes.lastUpdated = Date.now();
|
||||
}
|
||||
} else {
|
||||
host.outOfSync = true;
|
||||
host.unreachable = true;
|
||||
@@ -202,6 +242,47 @@ class FailoverRouter {
|
||||
}
|
||||
}
|
||||
|
||||
// methods for retrieving git hashes by host
|
||||
private async $updateFrontendGitHash(host: FailoverHost): Promise<void> {
|
||||
try {
|
||||
const url = `${host.publicDomain}/resources/config.js`;
|
||||
const response = await this.pollConnection.get<string>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
|
||||
const match = response.data.match(/GIT_COMMIT_HASH\s*=\s*['"](.*?)['"]/);
|
||||
if (match && match[1]?.length) {
|
||||
host.hashes.frontend = match[1];
|
||||
}
|
||||
} catch (e) {
|
||||
// failed to get frontend build hash - do nothing
|
||||
}
|
||||
}
|
||||
|
||||
private async $updateBackendGitHash(host: FailoverHost): Promise<void> {
|
||||
try {
|
||||
const url = `${host.publicDomain}/api/v1/backend-info`;
|
||||
const response = await this.pollConnection.get<any>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
|
||||
if (response.data?.gitCommit) {
|
||||
host.hashes.backend = response.data.gitCommit;
|
||||
}
|
||||
} catch (e) {
|
||||
// failed to get backend build hash - do nothing
|
||||
}
|
||||
}
|
||||
|
||||
// returns the public mempool domain corresponding to an esplora server url
|
||||
// (a bit of a hack to avoid manually specifying frontend & backend URLs for each esplora server)
|
||||
private extractPublicDomain(url: string): string {
|
||||
// force the url to start with a valid protocol
|
||||
const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;
|
||||
// parse as URL and extract the hostname
|
||||
try {
|
||||
const parsed = new URL(urlWithProtocol);
|
||||
return parsed.hostname;
|
||||
} catch (e) {
|
||||
// fallback to the original url
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> {
|
||||
let axiosConfig;
|
||||
let url;
|
||||
@@ -381,6 +462,7 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
unreachable: !!host.unreachable,
|
||||
checked: !!host.checked,
|
||||
lastChecked: host.lastChecked || 0,
|
||||
hashes: host.hashes,
|
||||
}));
|
||||
} else {
|
||||
return [];
|
||||
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 93;
|
||||
private static currentVersion = 94;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -801,6 +801,323 @@ class DatabaseMigration {
|
||||
`);
|
||||
await this.updateToSchemaVersion(93);
|
||||
}
|
||||
|
||||
// Unify database schema for all mempool netwoks
|
||||
// versions above 94 should not use network-specific flags
|
||||
if (databaseSchemaVersion < 94) {
|
||||
|
||||
if (!isBitcoin) {
|
||||
// Apply all the bitcoin specific migrations to non-bitcoin networks: liquid, liquidtestnet and testnet4 (!)
|
||||
// Version 5
|
||||
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
|
||||
|
||||
// Version 6
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`');
|
||||
await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT');
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL');
|
||||
await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)');
|
||||
await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
|
||||
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
|
||||
|
||||
// Version 7
|
||||
await this.$executeQuery('DROP table IF EXISTS hashrates;');
|
||||
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
|
||||
|
||||
// Version 8
|
||||
await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
|
||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
|
||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
|
||||
|
||||
// Version 9
|
||||
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
|
||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
|
||||
|
||||
// Version 10
|
||||
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
|
||||
|
||||
// Version 11
|
||||
await this.$executeQuery(`ALTER TABLE blocks
|
||||
ADD avg_fee INT UNSIGNED NULL,
|
||||
ADD avg_fee_rate INT UNSIGNED NULL
|
||||
`);
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
|
||||
|
||||
// Version 12
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||
|
||||
// Version 13
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||
|
||||
// Version 14
|
||||
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
|
||||
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||
|
||||
// Version 17
|
||||
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
|
||||
|
||||
// Version 18
|
||||
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
|
||||
|
||||
// Version 20
|
||||
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
|
||||
|
||||
// Version 22
|
||||
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
|
||||
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
|
||||
|
||||
// Version 24
|
||||
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
|
||||
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
|
||||
|
||||
// Version 25
|
||||
await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats'));
|
||||
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
|
||||
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
|
||||
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
|
||||
|
||||
// Version 26
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
|
||||
|
||||
// Version 27
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
|
||||
|
||||
// Version 28
|
||||
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
|
||||
|
||||
// Version 29
|
||||
await this.$executeQuery(this.getCreateGeoNamesTableQuery(), await this.$checkIfTableExists('geo_names'));
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD as_number int(11) unsigned NULL DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD city_id int(11) unsigned NULL DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD country_id int(11) unsigned NULL DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD accuracy_radius int(11) unsigned NULL DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
|
||||
|
||||
// Version 30
|
||||
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
|
||||
|
||||
// Version 31
|
||||
await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
|
||||
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
|
||||
await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
|
||||
|
||||
// Version 32
|
||||
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
|
||||
|
||||
// Version 33
|
||||
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
|
||||
|
||||
// Version 34
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
|
||||
|
||||
// Version 35
|
||||
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
|
||||
|
||||
// Version 36
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
|
||||
|
||||
// Version 37
|
||||
await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
|
||||
|
||||
// Version 38
|
||||
await this.$executeQuery(`TRUNCATE lightning_stats`);
|
||||
await this.$executeQuery(`TRUNCATE node_stats`);
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
|
||||
await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
|
||||
await this.updateToSchemaVersion(38);
|
||||
|
||||
// Version 39
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
|
||||
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
|
||||
|
||||
// Version 40
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
|
||||
|
||||
// Version 41
|
||||
await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
|
||||
|
||||
// Version 42
|
||||
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
|
||||
|
||||
// Version 43
|
||||
await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
|
||||
|
||||
// Version 44
|
||||
await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
|
||||
|
||||
// Version 45
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"');
|
||||
|
||||
// Version 48
|
||||
await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"');
|
||||
|
||||
// Version 57
|
||||
await this.$executeQuery(`ALTER TABLE nodes MODIFY updated_at datetime NULL`);
|
||||
|
||||
// Version 60
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"');
|
||||
|
||||
// Version 61
|
||||
if (! await this.$checkIfTableExists('blocks_templates')) {
|
||||
await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"');
|
||||
}
|
||||
await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"');
|
||||
await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)');
|
||||
await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template');
|
||||
|
||||
// Version 62
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_fees BIGINT UNSIGNED DEFAULT NULL');
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_weight BIGINT UNSIGNED DEFAULT NULL');
|
||||
|
||||
// Version 63
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
|
||||
|
||||
// Version 64
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
|
||||
|
||||
// Version 65
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
|
||||
|
||||
// Version 67
|
||||
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD version INT NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD INDEX `version` (`version`)');
|
||||
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD version INT NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)');
|
||||
|
||||
// Version 76
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
|
||||
|
||||
// Version 81
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
|
||||
|
||||
// Version 83
|
||||
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
|
||||
|
||||
// Version 84
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`pools\`
|
||||
ADD INDEX \`slug\` (\`slug\`),
|
||||
ADD INDEX \`unique_id\` (\`unique_id\`)
|
||||
`);
|
||||
|
||||
// Version 85
|
||||
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\`)
|
||||
`);
|
||||
|
||||
// Version 86
|
||||
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\`)
|
||||
`);
|
||||
|
||||
// Version 87
|
||||
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
|
||||
await this.updateToSchemaVersion(87);
|
||||
|
||||
// Version 88
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
|
||||
|
||||
// Version 89
|
||||
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
|
||||
|
||||
// Version 90
|
||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
|
||||
|
||||
// Version 91
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.NETWORK !== 'liquid') {
|
||||
// Apply all the liquid specific migrations to all other networks
|
||||
// Version 68
|
||||
await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);');
|
||||
await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses'));
|
||||
await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos'));
|
||||
|
||||
// Version 71
|
||||
await this.$executeQuery('ALTER TABLE `federation_txos` ADD timelock INT NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `federation_txos` ADD expiredAt INT NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `federation_txos` ADD emergencyKey TINYINT NOT NULL DEFAULT 0');
|
||||
|
||||
// Version 92
|
||||
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\`)
|
||||
`);
|
||||
|
||||
// Version 93
|
||||
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\`)
|
||||
`);
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.NETWORK !== 'mainnet') {
|
||||
// Apply all the mainnet specific migrations to all other networks
|
||||
// Version 69
|
||||
await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations'));
|
||||
|
||||
// Version 70
|
||||
await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;');
|
||||
|
||||
// Version 77
|
||||
await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL');
|
||||
}
|
||||
await this.updateToSchemaVersion(94);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
51
frontend/custom-river-config.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"theme": "wiz",
|
||||
"enterprise": "river",
|
||||
"branding": {
|
||||
"name": "river",
|
||||
"title": "river",
|
||||
"site_id": 22,
|
||||
"header_img": "/resources/riverlogo.svg",
|
||||
"footer_img": "/resources/riverlogo.svg"
|
||||
},
|
||||
"dashboard": {
|
||||
"widgets": [
|
||||
{
|
||||
"component": "fees",
|
||||
"mobileOrder": 4
|
||||
},
|
||||
{
|
||||
"component": "walletBalance",
|
||||
"mobileOrder": 1,
|
||||
"props": {
|
||||
"wallet": "RIVER"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "twitter",
|
||||
"mobileOrder": 5,
|
||||
"props": {
|
||||
"handle": "River"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "wallet",
|
||||
"mobileOrder": 2,
|
||||
"props": {
|
||||
"wallet": "RIVER",
|
||||
"period": "all"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "blocks"
|
||||
},
|
||||
{
|
||||
"component": "walletTransactions",
|
||||
"mobileOrder": 3,
|
||||
"props": {
|
||||
"wallet": "RIVER"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -45,14 +45,17 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
@Input() left: number | string = 70;
|
||||
@Input() widget: boolean = false;
|
||||
@Input() defaultFiat: boolean = false;
|
||||
@Input() showLegend: boolean = true;
|
||||
@Input() showYAxis: boolean = true;
|
||||
|
||||
adjustedLeft: number;
|
||||
adjustedRight: number;
|
||||
data: any[] = [];
|
||||
fiatData: any[] = [];
|
||||
hoverData: any[] = [];
|
||||
conversions: any;
|
||||
allowZoom: boolean = false;
|
||||
initialRight = this.right;
|
||||
initialLeft = this.left;
|
||||
|
||||
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
|
||||
|
||||
subscription: Subscription;
|
||||
@@ -120,7 +123,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
} else if (this.conversions && this.conversions['USD']) {
|
||||
price = this.conversions['USD'];
|
||||
}
|
||||
return { ...item, price: price }
|
||||
return { ...item, price: price };
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -181,8 +184,8 @@ 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.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right;
|
||||
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40;
|
||||
|
||||
this.chartOptions = {
|
||||
color: [
|
||||
@@ -199,10 +202,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
grid: {
|
||||
top: 20,
|
||||
bottom: this.allowZoom ? 65 : 20,
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
right: this.adjustedRight,
|
||||
left: this.adjustedLeft,
|
||||
},
|
||||
legend: !this.stateService.isAnyTestnet() ? {
|
||||
legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? {
|
||||
data: [
|
||||
{
|
||||
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
|
||||
@@ -313,6 +316,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
type: 'value',
|
||||
position: 'left',
|
||||
axisLabel: {
|
||||
show: this.showYAxis,
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val): string => {
|
||||
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
|
||||
@@ -343,9 +347,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
{
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
show: this.showYAxis,
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: function(val) {
|
||||
return `$${this.amountShortenerPipe.transform(val, 0, undefined, true)}`;
|
||||
return `$${this.amountShortenerPipe.transform(val, 3, undefined, true, true)}`;
|
||||
}.bind(this)
|
||||
},
|
||||
splitLine: {
|
||||
@@ -399,8 +404,8 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
type: 'slider',
|
||||
brushSelect: false,
|
||||
realtime: true,
|
||||
left: this.left,
|
||||
right: this.right,
|
||||
left: this.adjustedLeft,
|
||||
right: this.adjustedRight,
|
||||
selectedDataBackground: {
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
@@ -413,7 +418,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
|
||||
onChartClick(e) {
|
||||
if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) {
|
||||
this.zone.run(() => {
|
||||
this.zone.run(() => {
|
||||
const url = this.relativeUrlPipe.transform(`/tx/${this.hoverData[0][2].txid}`);
|
||||
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
|
||||
window.open(url);
|
||||
@@ -430,23 +435,23 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
|
||||
onLegendSelectChanged(e) {
|
||||
this.selected = e.selected;
|
||||
this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
|
||||
this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
|
||||
this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right;
|
||||
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40;
|
||||
|
||||
this.chartOptions = {
|
||||
grid: {
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
right: this.adjustedRight,
|
||||
left: this.adjustedLeft,
|
||||
},
|
||||
legend: {
|
||||
selected: this.selected,
|
||||
},
|
||||
dataZoom: this.allowZoom ? [{
|
||||
left: this.left,
|
||||
right: this.right,
|
||||
left: this.adjustedLeft,
|
||||
right: this.adjustedRight,
|
||||
}, {
|
||||
left: this.left,
|
||||
right: this.right,
|
||||
left: this.adjustedLeft,
|
||||
right: this.adjustedRight,
|
||||
}] : undefined
|
||||
};
|
||||
|
||||
@@ -478,7 +483,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
// Add a point at today's date to make the graph end at the current time
|
||||
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
|
||||
extendedSummary.reverse();
|
||||
|
||||
|
||||
let oneHour = 60 * 60;
|
||||
// Fill gaps longer than interval
|
||||
for (let i = 0; i < extendedSummary.length - 1; i++) {
|
||||
@@ -491,7 +496,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
i += hours - 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return extendedSummary.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
.accept-results {
|
||||
td, th {
|
||||
&.allowed {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
&.txid {
|
||||
width: 50%;
|
||||
}
|
||||
&.rate {
|
||||
width: 20%;
|
||||
text-align: right;
|
||||
white-space: wrap;
|
||||
}
|
||||
&.reason {
|
||||
width: 20%;
|
||||
text-align: right;
|
||||
white-space: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 950px) {
|
||||
table-layout: auto;
|
||||
|
||||
td, th {
|
||||
&.allowed {
|
||||
width: 100px;
|
||||
}
|
||||
&.txid {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@
|
||||
<th class="rtt only-small">RTT</th>
|
||||
<th class="rtt only-large">RTT</th>
|
||||
<th class="height">Height</th>
|
||||
<th class="frontend only-large">Front</th>
|
||||
<th class="backend only-large">Back</th>
|
||||
<th class="electrs only-large">Electrs</th>
|
||||
</tr>
|
||||
<tr *ngFor="let host of hosts; let i = index; trackBy: trackByFn">
|
||||
<td class="rank">{{ i + 1 }}</td>
|
||||
@@ -28,6 +31,15 @@
|
||||
<td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
|
||||
<td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
|
||||
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅')) }}</td>
|
||||
<ng-container *ngFor="let type of ['frontend', 'backend', 'electrs']">
|
||||
<td class="{{type}} only-large" [style.background-color]="host.hashes?.[type] ? '#' + host.hashes[type].slice(0, 6) : ''">
|
||||
@if (host.hashes?.[type]) {
|
||||
<a [style.color]="'white'" href="https://github.com/mempool/{{type === 'electrs' ? 'electrs' : 'mempool'}}/commit/{{ host.hashes[type] }}" target="_blank">{{ host.hashes[type].slice(0, 8) || '?' }}</a>
|
||||
} @else {
|
||||
<span>?</span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
max-width: 720px;
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
background: var(--box-bg);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="box preview-box" *ngIf="(walletAddresses$ | async) as walletAddresses">
|
||||
<app-preview-title>
|
||||
<span i18n="shared.wallet">Wallet</span>
|
||||
</app-preview-title>
|
||||
<div>
|
||||
<div class="table-col">
|
||||
<table class="table table-borderless dual-col-striped table-fixed wallet-table" *ngIf="(walletStats$ | async) as walletStats">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="address.number-addresses">Addresses</td>
|
||||
<td class="wrap-cell">{{ addressStrings.length }}</td>
|
||||
<td class="spacer"></td>
|
||||
<td i18n="address.utxos">UTXOs</td>
|
||||
<td class="wrap-cell">{{ walletStats.utxos }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="wallet.balance-btc">Balance (BTC)</td>
|
||||
<td class="wrap-cell"><app-amount [satoshis]="walletStats.balance" [noFiat]="true" [digitsInfo]="walletStats.balance > 1_000_000_000 ? '1.4-4' : '1.8-8'"></app-amount></td>
|
||||
<td class="spacer"></td>
|
||||
<td i18n="wallet.balance-usd">Balance (USD)</td>
|
||||
<td class="wrap-cell"><span class="fiat"><app-fiat [value]="walletStats.balance"></app-fiat></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col-md graph-col">
|
||||
<app-address-graph [addressSummary$]="walletSummary$" period="all" [widget]="true" [defaultFiat]="true" [height]="330" [left]="-40" [right]="-40" [showLegend]="false" [showYAxis]="false"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
.title-wrapper {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.graph-col {
|
||||
height: 350px;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
margin-left: 2px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.table-col {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table {
|
||||
font-size: 32px;
|
||||
|
||||
::ng-deep .symbol {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fiat {
|
||||
display: block;
|
||||
}
|
||||
245
frontend/src/app/components/wallet/wallet-preview.component.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { switchMap, catchError, map, tap, shareReplay, startWith, scan } from 'rxjs/operators';
|
||||
import { Address, AddressTxSummary, ChainStats, Transaction } from '@interfaces/electrs.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { of, Observable, Subscription } from 'rxjs';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||
import { WalletAddress } from '@interfaces/node-api.interface';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
|
||||
class WalletStats implements ChainStats {
|
||||
addresses: string[];
|
||||
funded_txo_count: number;
|
||||
funded_txo_sum: number;
|
||||
spent_txo_count: number;
|
||||
spent_txo_sum: number;
|
||||
tx_count: number;
|
||||
|
||||
constructor (stats: ChainStats[], addresses: string[]) {
|
||||
Object.assign(this, stats.reduce((acc, stat) => {
|
||||
acc.funded_txo_count += stat.funded_txo_count;
|
||||
acc.funded_txo_sum += stat.funded_txo_sum;
|
||||
acc.spent_txo_count += stat.spent_txo_count;
|
||||
acc.spent_txo_sum += stat.spent_txo_sum;
|
||||
return acc;
|
||||
}, {
|
||||
funded_txo_count: 0,
|
||||
funded_txo_sum: 0,
|
||||
spent_txo_count: 0,
|
||||
spent_txo_sum: 0,
|
||||
tx_count: 0,
|
||||
})
|
||||
);
|
||||
this.addresses = addresses;
|
||||
}
|
||||
|
||||
public addTx(tx: Transaction): void {
|
||||
for (const vin of tx.vin) {
|
||||
if (this.addresses.includes(vin.prevout?.scriptpubkey_address)) {
|
||||
this.spendTxo(vin.prevout.value);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
if (this.addresses.includes(vout.scriptpubkey_address)) {
|
||||
this.fundTxo(vout.value);
|
||||
}
|
||||
}
|
||||
this.tx_count++;
|
||||
}
|
||||
|
||||
public removeTx(tx: Transaction): void {
|
||||
for (const vin of tx.vin) {
|
||||
if (this.addresses.includes(vin.prevout?.scriptpubkey_address)) {
|
||||
this.unspendTxo(vin.prevout.value);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
if (this.addresses.includes(vout.scriptpubkey_address)) {
|
||||
this.unfundTxo(vout.value);
|
||||
}
|
||||
}
|
||||
this.tx_count--;
|
||||
}
|
||||
|
||||
private fundTxo(value: number): void {
|
||||
this.funded_txo_sum += value;
|
||||
this.funded_txo_count++;
|
||||
}
|
||||
|
||||
private unfundTxo(value: number): void {
|
||||
this.funded_txo_sum -= value;
|
||||
this.funded_txo_count--;
|
||||
}
|
||||
|
||||
private spendTxo(value: number): void {
|
||||
this.spent_txo_sum += value;
|
||||
this.spent_txo_count++;
|
||||
}
|
||||
|
||||
private unspendTxo(value: number): void {
|
||||
this.spent_txo_sum -= value;
|
||||
this.spent_txo_count--;
|
||||
}
|
||||
|
||||
get balance(): number {
|
||||
return this.funded_txo_sum - this.spent_txo_sum;
|
||||
}
|
||||
|
||||
get totalReceived(): number {
|
||||
return this.funded_txo_sum;
|
||||
}
|
||||
|
||||
get utxos(): number {
|
||||
return this.funded_txo_count - this.spent_txo_count;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-wallet-preview',
|
||||
templateUrl: './wallet-preview.component.html',
|
||||
styleUrls: ['./wallet-preview.component.scss']
|
||||
})
|
||||
export class WalletPreviewComponent implements OnInit, OnDestroy {
|
||||
network = '';
|
||||
|
||||
addresses: Address[] = [];
|
||||
addressStrings: string[] = [];
|
||||
walletName: string;
|
||||
isLoadingWallet = true;
|
||||
wallet$: Observable<Record<string, WalletAddress>>;
|
||||
walletAddresses$: Observable<Record<string, Address>>;
|
||||
walletSummary$: Observable<AddressTxSummary[]>;
|
||||
walletStats$: Observable<WalletStats>;
|
||||
error: any;
|
||||
walletSubscription: Subscription;
|
||||
|
||||
collapseAddresses: boolean = true;
|
||||
|
||||
fullyLoaded = false;
|
||||
txCount = 0;
|
||||
received = 0;
|
||||
sent = 0;
|
||||
chainBalance = 0;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
private openGraphService: OpenGraphService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks', 'stats']);
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
this.wallet$ = this.route.paramMap.pipe(
|
||||
map((params: ParamMap) => params.get('wallet') as string),
|
||||
tap((walletName: string) => {
|
||||
this.walletName = walletName;
|
||||
this.openGraphService.waitFor('wallet-addresses-' + this.walletName);
|
||||
this.openGraphService.waitFor('wallet-data-' + this.walletName);
|
||||
this.openGraphService.waitFor('wallet-txs-' + this.walletName);
|
||||
this.seoService.setTitle($localize`:@@wallet.component.browser-title:Wallet: ${walletName}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.wallet:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} wallet ${walletName}:INTERPOLATION:.`);
|
||||
}),
|
||||
switchMap((walletName: string) => this.apiService.getWallet$(walletName).pipe(
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
this.seoService.logSoft404();
|
||||
console.log(err);
|
||||
this.openGraphService.fail('wallet-addresses-' + this.walletName);
|
||||
this.openGraphService.fail('wallet-data-' + this.walletName);
|
||||
this.openGraphService.fail('wallet-txs-' + this.walletName);
|
||||
return of({});
|
||||
})
|
||||
)),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
this.walletAddresses$ = this.wallet$.pipe(
|
||||
map(wallet => {
|
||||
const walletInfo: Record<string, Address> = {};
|
||||
for (const address of Object.keys(wallet)) {
|
||||
walletInfo[address] = {
|
||||
address,
|
||||
chain_stats: wallet[address].stats,
|
||||
mempool_stats: {
|
||||
funded_txo_count: 0,
|
||||
funded_txo_sum: 0,
|
||||
spent_txo_count: 0, spent_txo_sum: 0, tx_count: 0
|
||||
},
|
||||
};
|
||||
}
|
||||
return walletInfo;
|
||||
}),
|
||||
tap(() => {
|
||||
this.isLoadingWallet = false;
|
||||
})
|
||||
);
|
||||
|
||||
this.walletSubscription = this.walletAddresses$.subscribe(wallet => {
|
||||
this.addressStrings = Object.keys(wallet);
|
||||
this.addresses = Object.values(wallet);
|
||||
this.openGraphService.waitOver('wallet-addresses-' + this.walletName);
|
||||
});
|
||||
|
||||
this.walletSummary$ = this.wallet$.pipe(
|
||||
map(wallet => this.deduplicateWalletTransactions(Object.values(wallet).flatMap(address => address.transactions))),
|
||||
tap(() => {
|
||||
this.openGraphService.waitOver('wallet-txs-' + this.walletName);
|
||||
})
|
||||
);
|
||||
|
||||
this.walletStats$ = this.wallet$.pipe(
|
||||
switchMap(wallet => {
|
||||
const walletStats = new WalletStats(Object.values(wallet).map(w => w.stats), Object.keys(wallet));
|
||||
return this.stateService.walletTransactions$.pipe(
|
||||
startWith([]),
|
||||
scan((stats, newTransactions) => {
|
||||
for (const tx of newTransactions) {
|
||||
stats.addTx(tx);
|
||||
}
|
||||
return stats;
|
||||
}, walletStats),
|
||||
);
|
||||
}),
|
||||
tap(() => {
|
||||
this.openGraphService.waitOver('wallet-data-' + this.walletName);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
|
||||
const transactions = new Map<string, AddressTxSummary>();
|
||||
for (const tx of walletTransactions) {
|
||||
if (transactions.has(tx.txid)) {
|
||||
transactions.get(tx.txid).value += tx.value;
|
||||
} else {
|
||||
transactions.set(tx.txid, tx);
|
||||
}
|
||||
}
|
||||
return Array.from(transactions.values()).sort((a, b) => {
|
||||
if (a.height === b.height) {
|
||||
return b.tx_position - a.tx_position;
|
||||
}
|
||||
return b.height - a.height;
|
||||
});
|
||||
}
|
||||
|
||||
normalizeAddress(address: string): string {
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) {
|
||||
return address.toLowerCase();
|
||||
} else {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.walletSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import { HashrateChartPoolsComponent } from '@components/hashrates-chart-pools/h
|
||||
import { BlockHealthGraphComponent } from '@components/block-health-graph/block-health-graph.component';
|
||||
import { AddressComponent } from '@components/address/address.component';
|
||||
import { WalletComponent } from '@components/wallet/wallet.component';
|
||||
import { WalletPreviewComponent } from '@components/wallet/wallet-preview.component';
|
||||
import { AddressGraphComponent } from '@components/address-graph/address-graph.component';
|
||||
import { UtxoGraphComponent } from '@components/utxo-graph/utxo-graph.component';
|
||||
import { ActiveAccelerationBox } from '@components/acceleration/active-acceleration-box/active-acceleration-box.component';
|
||||
@@ -49,6 +50,7 @@ import { CommonModule } from '@angular/common';
|
||||
MempoolBlockComponent,
|
||||
AddressComponent,
|
||||
WalletComponent,
|
||||
WalletPreviewComponent,
|
||||
|
||||
MiningDashboardComponent,
|
||||
AcceleratorDashboardComponent,
|
||||
|
||||
@@ -144,4 +144,9 @@ export interface HealthCheckHost {
|
||||
link?: string;
|
||||
statusPage?: SafeResourceUrl;
|
||||
flag?: string;
|
||||
hashes?: {
|
||||
frontend?: string;
|
||||
backend?: string;
|
||||
electrs?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,12 +142,12 @@ const routes: Routes = [
|
||||
|
||||
if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
|
||||
routes[0].children.push({
|
||||
path: 'nodes',
|
||||
path: 'monitoring',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: ServerHealthComponent
|
||||
});
|
||||
routes[0].children.push({
|
||||
path: 'network',
|
||||
path: 'nodes',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: ServerStatusComponent
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
|
||||
import { TransactionPreviewComponent } from '@components/transaction/transaction-preview.component';
|
||||
import { BlockPreviewComponent } from '@components/block/block-preview.component';
|
||||
import { AddressPreviewComponent } from '@components/address/address-preview.component';
|
||||
import { WalletPreviewComponent } from '@components/wallet/wallet-preview.component';
|
||||
import { PoolPreviewComponent } from '@components/pool/pool-preview.component';
|
||||
import { MasterPagePreviewComponent } from '@components/master-page-preview/master-page-preview.component';
|
||||
|
||||
@@ -20,6 +21,11 @@ const routes: Routes = [
|
||||
children: [],
|
||||
component: AddressPreviewComponent
|
||||
},
|
||||
{
|
||||
path: 'wallet/:wallet',
|
||||
children: [],
|
||||
component: WalletPreviewComponent
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
children: [],
|
||||
|
||||
@@ -8,8 +8,12 @@ export class AmountShortenerPipe implements PipeTransform {
|
||||
const digits = args[0] ?? 1;
|
||||
const unit = args[1] || undefined;
|
||||
const isMoney = args[2] || false;
|
||||
const sigfigs = args[3] || false; // if true, "digits" is the number of significant digits, not the number of decimal places
|
||||
|
||||
if (num < 1000) {
|
||||
if (sigfigs) {
|
||||
return Number(num.toPrecision(digits));
|
||||
}
|
||||
return num.toFixed(digits);
|
||||
}
|
||||
|
||||
@@ -25,10 +29,15 @@ export class AmountShortenerPipe implements PipeTransform {
|
||||
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
|
||||
const item = lookup.slice().reverse().find((item) => num >= item.value);
|
||||
|
||||
if (unit !== undefined) {
|
||||
return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + ' ' + item.symbol + unit : '0';
|
||||
} else {
|
||||
return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0';
|
||||
if (!item) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const scaledNum = num / item.value;
|
||||
const formattedNum = Number(sigfigs ? scaledNum.toPrecision(digits) : scaledNum.toFixed(digits)).toString();
|
||||
|
||||
return unit !== undefined
|
||||
? formattedNum + ' ' + item.symbol + unit
|
||||
: formattedNum + item.symbol;
|
||||
}
|
||||
}
|
||||
45
frontend/src/index.mempool.river.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>River | Invest in Bitcoin with confidence</title>
|
||||
<script src="/resources/config.js"></script>
|
||||
<script src="/resources/customize.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="Easily buy Bitcoin in minutes. Zero fees on recurring buys. Invest in Bitcoin with confidence with world-class security." />
|
||||
<meta property="og:image" content="https://mempool.space/resources/river/river-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="Easily buy Bitcoin in minutes. Zero fees on recurring buys. Invest in Bitcoin with confidence with world-class security." />
|
||||
<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="River | Invest in Bitcoin with confidence">
|
||||
<meta name="twitter:description" content="Easily buy Bitcoin in minutes. Zero fees on recurring buys. Invest in Bitcoin with confidence with world-class security." />
|
||||
<meta name="twitter:image" content="https://mempool.space/resources/river/river-preview.jpg" />
|
||||
<meta name="twitter:domain" content="river.mempool.space">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/resources/river/favicons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/resources/river/favicons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/resources/river/favicons/favicon-16x16.png">
|
||||
<link rel="manifest" href="/resources/river/favicons/site.webmanifest">
|
||||
<link rel="shortcut icon" href="/resources/river/favicons/favicon.ico">
|
||||
<link id="canonical" rel="canonical" href="https://river.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/river/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
frontend/src/resources/river/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/src/resources/river/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
frontend/src/resources/river/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 378 B |
BIN
frontend/src/resources/river/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 772 B |
BIN
frontend/src/resources/river/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
frontend/src/resources/river/river-preview.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
1
frontend/src/resources/river/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"}
|
||||
4
frontend/src/resources/riverlogo.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="160" height="40" viewBox="0 0 160 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M75.8865 31.2087H70.2185L66.2029 23.3666H59.531V31.2087H54.5116V8.52042H66.8515C69.4627 8.52042 71.4874 9.22873 72.9256 10.6425C74.3609 12.0591 75.08 13.8144 75.08 15.9082C75.08 17.3135 74.7388 18.5975 74.0592 19.7658C73.3796 20.9341 72.4011 21.8286 71.1293 22.4551L75.8893 31.2059L75.8865 31.2087ZM59.531 18.3407H67.1109C68.0612 18.3407 68.7888 18.1291 69.2964 17.7086C69.8039 17.2881 70.0577 16.7096 70.0577 15.9759C70.0577 15.2422 69.8039 14.6525 69.2964 14.2094C68.7888 13.7664 68.0612 13.5463 67.1109 13.5463H59.531V18.3435V18.3407ZM84.2954 31.2087H79.276V8.52042H84.2954V31.2087ZM99.709 31.0083L106.192 8.52042H111.699L105.135 31.2087H94.0862L87.7949 8.52042H93.3022L99.7119 31.0083H99.709ZM134.718 26.1857V31.2087H114.638V8.52042H134.557V13.5434H119.66V17.3671H134.397V22.3591H119.66V26.1828H134.721L134.718 26.1857ZM159.914 31.2087H154.246L150.23 23.3666H143.559V31.2087H138.539V8.52042H150.879C153.49 8.52042 155.515 9.22873 156.953 10.6425C158.388 12.0591 159.108 13.8144 159.108 15.9082C159.108 17.3135 158.766 18.5975 158.087 19.7658C157.407 20.9341 156.429 21.8286 155.157 22.4551L159.917 31.2059L159.914 31.2087ZM143.559 18.3407H151.138C152.089 18.3407 152.816 18.1291 153.324 17.7086C153.831 17.2881 154.085 16.7096 154.085 15.9759C154.085 15.2422 153.831 14.6525 153.324 14.2094C152.816 13.7664 152.089 13.5463 151.138 13.5463H143.559V18.3435V18.3407Z" fill="#F9F9F9"/>
|
||||
<path d="M6.50834 35.4049L19.1838 7.60612C19.2346 7.49324 19.4038 7.56097 19.3643 7.67949L10.2673 34.0024C11.6265 33.5734 13.0195 33.2207 14.4407 32.9442L20.481 8.00683C20.5092 7.88549 20.6896 7.92218 20.6727 8.04352L17.1591 32.5124C18.3181 32.3713 19.4968 32.281 20.684 32.2415L21.8289 8.15357C21.8345 8.02941 22.0178 8.02941 22.0235 8.15357L23.1683 32.2415C24.3583 32.2782 25.5342 32.3685 26.6932 32.5124L23.1796 8.04352C23.1627 7.92218 23.3404 7.88549 23.3714 8.00683L29.4116 32.9442C30.8328 33.2207 32.2259 33.5763 33.5851 34.0024L24.4881 7.67949C24.4486 7.56379 24.6178 7.49324 24.6685 7.60612L37.344 35.4049C39.6338 36.3982 41.8107 37.6031 43.8495 39L21.9248 1L0 39C2.03597 37.6031 4.21294 36.3982 6.50552 35.4049" fill="#C5A063"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -131,8 +131,8 @@ export NVM_DIR="${HOME}/.nvm"
|
||||
source "${NVM_DIR}/nvm.sh"
|
||||
|
||||
# what to look for
|
||||
frontends=(mainnet liquid onbtc meta)
|
||||
backends=(mainnet testnet testnet4 signet liquid liquidtestnet onbtc)
|
||||
frontends=(mainnet liquid onbtc bitb meta river)
|
||||
backends=(mainnet testnet testnet4 signet liquid liquidtestnet onbtc bitb)
|
||||
frontend_repos=()
|
||||
backend_repos=()
|
||||
|
||||
|
||||
@@ -153,6 +153,6 @@
|
||||
},
|
||||
"WALLETS": {
|
||||
"ENABLED": true,
|
||||
"WALLETS": ["BITB", "3350"]
|
||||
"WALLETS": ["BITB", "3350", "RIVER"]
|
||||
}
|
||||
}
|
||||
|
||||
19
production/mempool-frontend-config.river.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-river-config.json"
|
||||
}
|
||||
@@ -5,6 +5,7 @@ nvm use v20.12.0
|
||||
|
||||
# start all mempool backends that exist
|
||||
for site in mainnet mainnet-lightning testnet testnet-lightning testnet4 signet signet-lightning liquid liquidtestnet;do
|
||||
[ ! -e "${HOME}/${site}/backend/" ] && continue
|
||||
cd "${HOME}/${site}/backend/" && \
|
||||
echo "starting mempool backend: ${site}" && \
|
||||
screen -dmS "${site}" sh -c 'while true;do npm run start-production;sleep 1;done'
|
||||
@@ -15,7 +16,7 @@ screen -dmS x startx
|
||||
sleep 3
|
||||
|
||||
# start unfurlers for each frontend
|
||||
for site in mainnet liquid onbtc meta;do
|
||||
for site in mainnet liquid onbtc bitb meta river;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'
|
||||
|
||||
17
production/unfurler-config.bitb.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"SERVER": {
|
||||
"HOST": "https://bitb.tk7.mempool.space",
|
||||
"HTTP_PORT": 8006
|
||||
},
|
||||
"MEMPOOL": {
|
||||
"HTTP_HOST": "http://127.0.0.1",
|
||||
"HTTP_PORT": 86,
|
||||
"NETWORK": "bitb"
|
||||
},
|
||||
"PUPPETEER": {
|
||||
"CLUSTER_SIZE": 8,
|
||||
"EXEC_PATH": "/usr/local/bin/chrome",
|
||||
"MAX_PAGE_AGE": 86400,
|
||||
"RENDER_TIMEOUT": 3000
|
||||
}
|
||||
}
|
||||
17
production/unfurler-config.river.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"SERVER": {
|
||||
"HOST": "https://river.tk7.mempool.space",
|
||||
"HTTP_PORT": 8007
|
||||
},
|
||||
"MEMPOOL": {
|
||||
"HTTP_HOST": "http://127.0.0.1",
|
||||
"HTTP_PORT": 7,
|
||||
"NETWORK": "river"
|
||||
},
|
||||
"PUPPETEER": {
|
||||
"CLUSTER_SIZE": 8,
|
||||
"EXEC_PATH": "/usr/local/bin/chrome",
|
||||
"MAX_PAGE_AGE": 86400,
|
||||
"RENDER_TIMEOUT": 3000
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ class Server {
|
||||
secureHost = true;
|
||||
secureMempoolHost = true;
|
||||
canonicalHost: string;
|
||||
networkName: string;
|
||||
|
||||
seoQueueLength: number = 0;
|
||||
unfurlQueueLength: number = 0;
|
||||
@@ -41,6 +42,7 @@ class Server {
|
||||
this.secureHost = config.SERVER.HOST.startsWith('https');
|
||||
this.secureMempoolHost = config.MEMPOOL.HTTP_HOST.startsWith('https');
|
||||
this.network = config.MEMPOOL.NETWORK || 'bitcoin';
|
||||
this.networkName = networks[this.network].networkName || capitalize(this.network);
|
||||
|
||||
let canonical;
|
||||
switch(config.MEMPOOL.NETWORK) {
|
||||
@@ -339,7 +341,7 @@ class Server {
|
||||
|
||||
if (matchedRoute.render) {
|
||||
ogImageUrl = `${config.SERVER.HOST}/render/${lang || 'en'}/preview${path}`;
|
||||
ogTitle = `${this.network ? capitalize(this.network) + ' ' : ''}${matchedRoute.networkMode !== 'mainnet' ? capitalize(matchedRoute.networkMode) + ' ' : ''}${matchedRoute.title}`;
|
||||
ogTitle = `${this.networkName} ${matchedRoute.networkMode !== 'mainnet' ? capitalize(matchedRoute.networkMode) + ' ' : ''}${matchedRoute.title}`;
|
||||
} else {
|
||||
ogTitle = networks[this.network].title;
|
||||
}
|
||||
@@ -394,7 +396,7 @@ class Server {
|
||||
|
||||
if (matchedRoute.render) {
|
||||
ogImageUrl = `${config.SERVER.HOST}/render/${lang || 'en'}/preview${path}`;
|
||||
ogTitle = `${this.network ? capitalize(this.network) + ' ' : ''}${matchedRoute.networkMode !== 'mainnet' ? capitalize(matchedRoute.networkMode) + ' ' : ''}${matchedRoute.title}`;
|
||||
ogTitle = `${this.networkName} ${matchedRoute.networkMode !== 'mainnet' ? capitalize(matchedRoute.networkMode) + ' ' : ''}${matchedRoute.title}`;
|
||||
}
|
||||
|
||||
if (matchedRoute.sip) {
|
||||
|
||||
@@ -85,6 +85,13 @@ const routes = {
|
||||
return `Address: ${path[0]}`;
|
||||
}
|
||||
},
|
||||
wallet: {
|
||||
render: true,
|
||||
params: 1,
|
||||
getTitle(path) {
|
||||
return `Wallet: ${path[0]}`;
|
||||
}
|
||||
},
|
||||
blocks: {
|
||||
title: "Blocks",
|
||||
fallbackImg: '/resources/previews/blocks.jpg',
|
||||
@@ -263,6 +270,7 @@ export const networks = {
|
||||
routes: {} // no routes supported
|
||||
},
|
||||
onbtc: {
|
||||
networkName: 'ONBTC',
|
||||
title: 'National Bitcoin Office of El Salvador',
|
||||
description: 'The National Bitcoin Office (ONBTC) of El Salvador under President @nayibbukele',
|
||||
fallbackImg: '/resources/onbtc/onbtc-preview.jpg',
|
||||
@@ -282,13 +290,59 @@ export const networks = {
|
||||
}
|
||||
}
|
||||
},
|
||||
bitb: {
|
||||
networkName: '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,
|
||||
wallet: routes.wallet,
|
||||
tx: routes.tx,
|
||||
mining: {
|
||||
title: "Mining",
|
||||
routes: {
|
||||
pool: routes.mining.routes.pool,
|
||||
}
|
||||
},
|
||||
lightning: {
|
||||
title: "Lightning",
|
||||
routes: routes.lightning.routes,
|
||||
}
|
||||
}
|
||||
},
|
||||
meta: {
|
||||
networkName: 'Metaplanet',
|
||||
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,
|
||||
wallet: routes.wallet,
|
||||
tx: routes.tx,
|
||||
mining: {
|
||||
title: "Mining",
|
||||
routes: {
|
||||
pool: routes.mining.routes.pool,
|
||||
}
|
||||
},
|
||||
lightning: {
|
||||
title: "Lightning",
|
||||
routes: routes.lightning.routes,
|
||||
}
|
||||
}
|
||||
},
|
||||
river: {
|
||||
networkName: 'River',
|
||||
title: 'River | Invest in Bitcoin with confidence',
|
||||
description: 'Easily buy Bitcoin in minutes. Zero fees on recurring buys. Invest in Bitcoin with confidence with world-class security.',
|
||||
fallbackImg: '/resources/river/river-preview.jpg',
|
||||
routes: { // only dynamic routes supported
|
||||
block: routes.block,
|
||||
address: routes.address,
|
||||
wallet: routes.wallet,
|
||||
tx: routes.tx,
|
||||
mining: {
|
||||
title: "Mining",
|
||||
|
||||