mirror of
https://github.com/mempool/mempool.git
synced 2025-06-01 18:49:44 +02:00
Merge branch 'master' into mononaut/unfurler-fixes
This commit is contained in:
commit
b6da116dfc
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -1,6 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
versioning-strategy: increase
|
||||
directory: "/backend"
|
||||
schedule:
|
||||
interval: daily
|
||||
@ -14,6 +15,7 @@ updates:
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: "/frontend"
|
||||
versioning-strategy: increase
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
- name: Unit Tests
|
||||
if: ${{ matrix.flavor == 'dev'}}
|
||||
run: npm run test
|
||||
run: npm run test:ci
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
|
||||
|
||||
- name: Build
|
||||
|
@ -31,6 +31,7 @@
|
||||
"reindex-updated-pools": "npm run start-production --update-pools",
|
||||
"reindex-all-blocks": "npm run start-production --update-pools --reindex-blocks",
|
||||
"test": "./node_modules/.bin/jest --coverage",
|
||||
"test:ci": "CI=true ./node_modules/.bin/jest --coverage",
|
||||
"lint": "./node_modules/.bin/eslint . --ext .ts",
|
||||
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
|
||||
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"",
|
||||
|
@ -186,7 +186,9 @@ describe('Mempool Backend Config', () => {
|
||||
for (const [key, value] of Object.entries(jsonObj)) {
|
||||
// We have a few cases where we can't follow the pattern
|
||||
if (root === 'MEMPOOL' && key === 'HTTP_PORT') {
|
||||
console.log('skipping check for MEMPOOL_HTTP_PORT');
|
||||
if (process.env.CI) {
|
||||
console.log('skipping check for MEMPOOL_HTTP_PORT');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
switch (typeof value) {
|
||||
@ -208,13 +210,17 @@ describe('Mempool Backend Config', () => {
|
||||
//The string used as the default value, to be checked as a regex, i.e, __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=(.*)}
|
||||
const defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}';
|
||||
|
||||
console.log(`looking for ${defaultEntry} in the start.sh script`);
|
||||
if (process.env.CI) {
|
||||
console.log(`looking for ${defaultEntry} in the start.sh script`);
|
||||
}
|
||||
const re = new RegExp(defaultEntry);
|
||||
expect(startSh).toMatch(re);
|
||||
|
||||
//The string that actually replaces the values in the config file
|
||||
const sedStr = 'sed -i "s!' + replaceStr + '!${' + replaceStr + '}!g" mempool-config.json';
|
||||
console.log(`looking for ${sedStr} in the start.sh script`);
|
||||
if (process.env.CI) {
|
||||
console.log(`looking for ${sedStr} in the start.sh script`);
|
||||
}
|
||||
expect(startSh).toContain(sedStr);
|
||||
break;
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import PricesRepository from '../../repositories/PricesRepository';
|
||||
class MiningRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools', this.$listPools)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/:interval', this.$getPools)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', this.$getPoolHistoricalHashrate)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', this.$getPoolBlocks)
|
||||
@ -41,6 +42,10 @@ class MiningRoutes {
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
res.status(400).send('Prices are not available on testnets.');
|
||||
return;
|
||||
}
|
||||
if (req.query.timestamp) {
|
||||
res.status(200).send(await PricesRepository.$getNearestHistoricalPrice(
|
||||
parseInt(<string>req.query.timestamp ?? 0, 10)
|
||||
@ -88,6 +93,29 @@ class MiningRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $listPools(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
|
||||
const pools = await mining.$listPools();
|
||||
if (!pools) {
|
||||
res.status(500).end();
|
||||
return;
|
||||
}
|
||||
|
||||
res.header('X-total-count', pools.length.toString());
|
||||
if (pools.length === 0) {
|
||||
res.status(204).send();
|
||||
} else {
|
||||
res.json(pools);
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getPools(req: Request, res: Response) {
|
||||
try {
|
||||
const stats = await mining.$getPoolsStats(req.params.interval);
|
||||
|
@ -26,7 +26,7 @@ class Mining {
|
||||
/**
|
||||
* Get historical blocks health
|
||||
*/
|
||||
public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> {
|
||||
public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> {
|
||||
return await BlocksAuditsRepository.$getBlocksHealthHistory(
|
||||
this.getTimeRange(interval),
|
||||
Common.getSqlInterval(interval)
|
||||
@ -56,7 +56,7 @@ class Mining {
|
||||
/**
|
||||
* Get historical block fee rates percentiles
|
||||
*/
|
||||
public async $getHistoricalBlockFeeRates(interval: string | null = null): Promise<any> {
|
||||
public async $getHistoricalBlockFeeRates(interval: string | null = null): Promise<any> {
|
||||
return await BlocksRepository.$getHistoricalBlockFeeRates(
|
||||
this.getTimeRange(interval),
|
||||
Common.getSqlInterval(interval)
|
||||
@ -66,7 +66,7 @@ class Mining {
|
||||
/**
|
||||
* Get historical block sizes
|
||||
*/
|
||||
public async $getHistoricalBlockSizes(interval: string | null = null): Promise<any> {
|
||||
public async $getHistoricalBlockSizes(interval: string | null = null): Promise<any> {
|
||||
return await BlocksRepository.$getHistoricalBlockSizes(
|
||||
this.getTimeRange(interval),
|
||||
Common.getSqlInterval(interval)
|
||||
@ -76,7 +76,7 @@ class Mining {
|
||||
/**
|
||||
* Get historical block weights
|
||||
*/
|
||||
public async $getHistoricalBlockWeights(interval: string | null = null): Promise<any> {
|
||||
public async $getHistoricalBlockWeights(interval: string | null = null): Promise<any> {
|
||||
return await BlocksRepository.$getHistoricalBlockWeights(
|
||||
this.getTimeRange(interval),
|
||||
Common.getSqlInterval(interval)
|
||||
@ -595,6 +595,20 @@ class Mining {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List existing mining pools
|
||||
*/
|
||||
public async $listPools(): Promise<{name: string, slug: string, unique_id: number}[] | null> {
|
||||
const [rows] = await database.query(`
|
||||
SELECT
|
||||
name,
|
||||
slug,
|
||||
unique_id
|
||||
FROM pools`
|
||||
);
|
||||
return rows as {name: string, slug: string, unique_id: number}[];
|
||||
}
|
||||
|
||||
private getDateMidnight(date: Date): Date {
|
||||
date.setUTCHours(0);
|
||||
date.setUTCMinutes(0);
|
||||
|
@ -198,18 +198,14 @@ class WebsocketHandler {
|
||||
matchedAddress = matchedAddress.toLowerCase();
|
||||
}
|
||||
if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) {
|
||||
client['track-address'] = null;
|
||||
client['track-scriptpubkey'] = '41' + matchedAddress + 'ac';
|
||||
} else if (/^|(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
|
||||
client['track-address'] = null;
|
||||
client['track-scriptpubkey'] = '21' + matchedAddress + 'ac';
|
||||
client['track-address'] = '41' + matchedAddress + 'ac';
|
||||
} else if (/^(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
|
||||
client['track-address'] = '21' + matchedAddress + 'ac';
|
||||
} else {
|
||||
client['track-address'] = matchedAddress;
|
||||
client['track-scriptpubkey'] = null;
|
||||
}
|
||||
} else {
|
||||
client['track-address'] = null;
|
||||
client['track-scriptpubkey'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -488,6 +484,9 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// pre-compute address transactions
|
||||
const addressCache = this.makeAddressCache(newTransactions);
|
||||
|
||||
this.wss.clients.forEach(async (client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
@ -527,78 +526,13 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (client['track-address']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
const foundTransactions = Array.from(addressCache[client['track-address']]?.values() || []);
|
||||
// txs may be missing prevouts in non-esplora backends
|
||||
// so fetch the full transactions now
|
||||
const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(foundTransactions) : foundTransactions;
|
||||
|
||||
for (const tx of newTransactions) {
|
||||
const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address']);
|
||||
if (someVin) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||
foundTransactions.push(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
} else {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const someVout = tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address']);
|
||||
if (someVout) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||
foundTransactions.push(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
} else {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundTransactions.length) {
|
||||
response['address-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-scriptpubkey']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
for (const tx of newTransactions) {
|
||||
const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey']);
|
||||
if (someVin) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||
foundTransactions.push(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
} else {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey']);
|
||||
if (someVout) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||
foundTransactions.push(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
} else {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundTransactions.length) {
|
||||
response['address-transactions'] = JSON.stringify(foundTransactions);
|
||||
if (fullTransactions.length) {
|
||||
response['address-transactions'] = JSON.stringify(fullTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -606,7 +540,6 @@ class WebsocketHandler {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
newTransactions.forEach((tx) => {
|
||||
|
||||
if (client['track-asset'] === Common.nativeAssetId) {
|
||||
if (tx.vin.some((vin) => !!vin.is_pegin)) {
|
||||
foundTransactions.push(tx);
|
||||
@ -805,6 +738,9 @@ class WebsocketHandler {
|
||||
const fees = feeApi.getRecommendedFee();
|
||||
const mempoolInfo = memPool.getMempoolInfo();
|
||||
|
||||
// pre-compute address transactions
|
||||
const addressCache = this.makeAddressCache(transactions);
|
||||
|
||||
// update init data
|
||||
this.updateSocketDataFields({
|
||||
'mempoolInfo': mempoolInfo,
|
||||
@ -867,44 +803,7 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (client['track-address']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
transactions.forEach((tx) => {
|
||||
if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address'])) {
|
||||
foundTransactions.push(tx);
|
||||
return;
|
||||
}
|
||||
if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address'])) {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
});
|
||||
|
||||
if (foundTransactions.length) {
|
||||
foundTransactions.forEach((tx) => {
|
||||
tx.status = {
|
||||
confirmed: true,
|
||||
block_height: block.height,
|
||||
block_hash: block.id,
|
||||
block_time: block.timestamp,
|
||||
};
|
||||
});
|
||||
|
||||
response['block-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-scriptpubkey']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
transactions.forEach((tx) => {
|
||||
if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey'])) {
|
||||
foundTransactions.push(tx);
|
||||
return;
|
||||
}
|
||||
if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey'])) {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
});
|
||||
const foundTransactions: TransactionExtended[] = Array.from(addressCache[client['track-address']]?.values() || []);
|
||||
|
||||
if (foundTransactions.length) {
|
||||
foundTransactions.forEach((tx) => {
|
||||
@ -982,6 +881,52 @@ class WebsocketHandler {
|
||||
+ '}';
|
||||
}
|
||||
|
||||
private makeAddressCache(transactions: MempoolTransactionExtended[]): { [address: string]: Set<MempoolTransactionExtended> } {
|
||||
const addressCache: { [address: string]: Set<MempoolTransactionExtended> } = {};
|
||||
for (const tx of transactions) {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin?.prevout?.scriptpubkey_address) {
|
||||
if (!addressCache[vin.prevout.scriptpubkey_address]) {
|
||||
addressCache[vin.prevout.scriptpubkey_address] = new Set();
|
||||
}
|
||||
addressCache[vin.prevout.scriptpubkey_address].add(tx);
|
||||
}
|
||||
if (vin?.prevout?.scriptpubkey) {
|
||||
if (!addressCache[vin.prevout.scriptpubkey]) {
|
||||
addressCache[vin.prevout.scriptpubkey] = new Set();
|
||||
}
|
||||
addressCache[vin.prevout.scriptpubkey].add(tx);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
if (vout?.scriptpubkey_address) {
|
||||
if (!addressCache[vout?.scriptpubkey_address]) {
|
||||
addressCache[vout?.scriptpubkey_address] = new Set();
|
||||
}
|
||||
addressCache[vout?.scriptpubkey_address].add(tx);
|
||||
}
|
||||
if (vout?.scriptpubkey) {
|
||||
if (!addressCache[vout.scriptpubkey]) {
|
||||
addressCache[vout.scriptpubkey] = new Set();
|
||||
}
|
||||
addressCache[vout.scriptpubkey].add(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
return addressCache;
|
||||
}
|
||||
|
||||
private async getFullTransactions(transactions: MempoolTransactionExtended[]): Promise<MempoolTransactionExtended[]> {
|
||||
for (let i = 0; i < transactions.length; i++) {
|
||||
try {
|
||||
transactions[i] = await transactionUtils.$getMempoolTransactionExtended(transactions[i].txid, true);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
|
||||
private printLogs(): void {
|
||||
if (this.wss) {
|
||||
const count = this.wss?.clients?.size || 0;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div [formGroup]="fiatForm" class="text-small text-center">
|
||||
<select formControlName="fiat" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 200px;" (change)="changeFiat()">
|
||||
<select formControlName="fiat" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 150px;" (change)="changeFiat()">
|
||||
<option *ngFor="let currency of currencies" [value]="currency[1].code">{{ currency[1].name + " (" + currency[1].code + ")" }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div [formGroup]="languageForm" class="text-small text-center">
|
||||
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 200px;" (change)="changeLanguage()">
|
||||
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 100px;" (change)="changeLanguage()">
|
||||
<option *ngFor="let lang of languages" [value]="lang.code">{{ lang.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div [formGroup]="rateUnitForm" class="text-small text-center">
|
||||
<select formControlName="rateUnits" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 200px;" (change)="changeUnits()">
|
||||
<select formControlName="rateUnits" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 100px;" (change)="changeUnits()">
|
||||
<option *ngFor="let unit of units" [value]="unit.name">{{ unit.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
||||
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit } from '../interfaces/node-api.interface';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { StateService } from './state.service';
|
||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||
import { Outspend, Transaction } from '../interfaces/electrs.interface';
|
||||
@ -312,6 +312,19 @@ export class ApiService {
|
||||
}
|
||||
|
||||
getHistoricalPrice$(timestamp: number | undefined): Observable<Conversion> {
|
||||
if (this.stateService.isAnyTestnet()) {
|
||||
return of({
|
||||
prices: [],
|
||||
exchangeRates: {
|
||||
USDEUR: 0,
|
||||
USDGBP: 0,
|
||||
USDCAD: 0,
|
||||
USDCHF: 0,
|
||||
USDAUD: 0,
|
||||
USDJPY: 0,
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.httpClient.get<Conversion>(
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/historical-price' +
|
||||
(timestamp ? `?timestamp=${timestamp}` : '')
|
||||
|
@ -339,6 +339,10 @@ export class StateService {
|
||||
return this.network === 'liquid' || this.network === 'liquidtestnet';
|
||||
}
|
||||
|
||||
isAnyTestnet(): boolean {
|
||||
return ['testnet', 'signet', 'liquidtestnet'].includes(this.network);
|
||||
}
|
||||
|
||||
resetChainTip() {
|
||||
this.latestBlockHeight = -1;
|
||||
this.chainTip$.next(-1);
|
||||
|
@ -1,20 +1,21 @@
|
||||
<footer>
|
||||
<div class="container-fluid">
|
||||
<div class="row main">
|
||||
<div class="offset-lg-1 col-lg-4 col align-self-center branding mt-2">
|
||||
<div class="col-md-12 branding mt-2">
|
||||
<div class="main-logo">
|
||||
<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>
|
||||
</div>
|
||||
<p><ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></p>
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
</div>
|
||||
<div class="selector">
|
||||
<app-fiat-selector></app-fiat-selector>
|
||||
</div>
|
||||
<div class="selector">
|
||||
<app-rate-unit-selector></app-rate-unit-selector>
|
||||
<div class="site-options">
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
</div>
|
||||
<div class="selector">
|
||||
<app-fiat-selector></app-fiat-selector>
|
||||
</div>
|
||||
<div class="selector">
|
||||
<app-rate-unit-selector></app-rate-unit-selector>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="officialMempoolSpace && stateService.env.ACCELERATOR" class="cta">
|
||||
<a class="btn btn-purple sponsor" [routerLink]="['/login' | relativeUrl]">
|
||||
@ -23,52 +24,50 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-10 offset-md-1 links outer">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<p class="category">Explore</p>
|
||||
<p><a [routerLink]="['/mining' | relativeUrl]">Mining Dashboard</a></p>
|
||||
<p><a *ngIf="env.LIGHTNING" [routerLink]="['/lightning' | relativeUrl]">Lightning Dashboard</a></p>
|
||||
<p><a [routerLink]="['/blocks' | relativeUrl]">Recent Blocks</a></p>
|
||||
<p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
|
||||
<p *ngIf="officialMempoolSpace"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
|
||||
<p><a [routerLink]="['/docs/api' | relativeUrl]">API Documentation</a></p>
|
||||
</div>
|
||||
<div class="col-lg-6 links">
|
||||
<p class="category">Learn</p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-mempool">What is a mempool?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-block-explorer">What is a block explorer?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-mempool-explorer">What is a mempool explorer?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="why-is-transaction-stuck-in-mempool">Why isn't my transaction confirming?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq' | relativeUrl]">More FAQs ›</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row col-md-12 link-tree">
|
||||
<div class="links">
|
||||
<p class="category">Explore</p>
|
||||
<p><a [routerLink]="['/mining' | relativeUrl]">Mining Dashboard</a></p>
|
||||
<p><a *ngIf="env.LIGHTNING" [routerLink]="['/lightning' | relativeUrl]">Lightning Dashboard</a></p>
|
||||
<p><a [routerLink]="['/blocks' | relativeUrl]">Recent Blocks</a></p>
|
||||
<p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
|
||||
<p *ngIf="officialMempoolSpace"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
|
||||
<p><a [routerLink]="['/docs/api' | relativeUrl]">API Documentation</a></p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6 links" *ngIf="officialMempoolSpace || env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED else toolBox" >
|
||||
<p class="category">Networks</p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== '') && (currentNetwork !== 'mainnet')"><a [href]="networkLink('mainnet')">Mainnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'testnet') && env.TESTNET_ENABLED"><a [href]="networkLink('testnet')">Testnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'signet') && env.SIGNET_ENABLED"><a [href]="networkLink('signet')">Signet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquidtestnet')"><a [href]="networkLink('liquidtestnet')">Liquid Testnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquid')"><a [href]="networkLink('liquid')">Liquid Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace && (currentNetwork !== 'bisq'))"><a [href]="networkLink('bisq')">Bisq Explorer</a></p>
|
||||
</div>
|
||||
<ng-template #toolBox>
|
||||
<div class="col-lg-6 links">
|
||||
<p class="category">Tools</p>
|
||||
<p><a [routerLink]="['/clock/mempool/0']">Clock (Mempool)</a></p>
|
||||
<p><a [routerLink]="['/clock/mined/0']">Clock (Mined)</a></p>
|
||||
<p><a [routerLink]="['/tools/calculator']">BTC/Fiat Converter</a></p>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="col-lg-6 links">
|
||||
<p class="category">Legal</p>
|
||||
<p><a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a></p>
|
||||
<p><a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a></p>
|
||||
<p><a [routerLink]="['/trademark-policy']">Trademark Policy</a></p>
|
||||
</div>
|
||||
<div class="links">
|
||||
<p class="category">Learn</p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-mempool">What is a mempool?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-block-explorer">What is a block explorer?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-mempool-explorer">What is a mempool explorer?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="why-is-transaction-stuck-in-mempool">Why isn't my transaction confirming?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq' | relativeUrl]">More FAQs ›</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links" *ngIf="officialMempoolSpace || env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED else toolBox" >
|
||||
<p class="category">Networks</p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== '') && (currentNetwork !== 'mainnet')"><a [href]="networkLink('mainnet')">Mainnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'testnet') && env.TESTNET_ENABLED"><a [href]="networkLink('testnet')">Testnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'signet') && env.SIGNET_ENABLED"><a [href]="networkLink('signet')">Signet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquidtestnet')"><a [href]="networkLink('liquidtestnet')">Liquid Testnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquid')"><a [href]="networkLink('liquid')">Liquid Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace && (currentNetwork !== 'bisq'))"><a [href]="networkLink('bisq')">Bisq Explorer</a></p>
|
||||
</div>
|
||||
<ng-template #toolBox>
|
||||
<div class="links">
|
||||
<p class="category">Tools</p>
|
||||
<p><a [routerLink]="['/clock/mempool/0']">Clock (Mempool)</a></p>
|
||||
<p><a [routerLink]="['/clock/mined/0']">Clock (Mined)</a></p>
|
||||
<p><a [routerLink]="['/tools/calculator']">BTC/Fiat Converter</a></p>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="links">
|
||||
<p class="category">Legal</p>
|
||||
<p><a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a></p>
|
||||
<p><a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a></p>
|
||||
<p><a [routerLink]="['/trademark-policy']">Trademark Policy</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row social-links">
|
||||
<div class="col-sm-12">
|
||||
|
@ -12,15 +12,11 @@ footer p {
|
||||
}
|
||||
|
||||
footer .row.main {
|
||||
padding: 40px 0;
|
||||
max-width: 1200px;
|
||||
padding: 40px 0 24px 0;
|
||||
max-width: 1140px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
footer .row.main .branding {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer .row.main .branding > p {
|
||||
margin-bottom: 45px;
|
||||
}
|
||||
@ -38,16 +34,20 @@ footer .row.main .branding .cta {
|
||||
margin: 25px auto 25px auto;
|
||||
}
|
||||
|
||||
footer .row.main .links.outer {
|
||||
padding-left: 24px;
|
||||
padding-top: 10px;
|
||||
footer .link-tree .links:nth-child(1), footer .link-tree .links:nth-child(4) {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
footer .link-tree .links p {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
footer .row.main .links > div:first-child {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
footer .row.main .links .category {
|
||||
footer .links .category {
|
||||
color: #4a68b9;
|
||||
font-weight: 700;
|
||||
}
|
||||
@ -56,13 +56,27 @@ footer .row.main .links .category:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
footer .site-options {
|
||||
float: right;
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
footer .selector {
|
||||
margin: 20px 0;
|
||||
margin: 20px 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
footer .row.link-tree {
|
||||
max-width: 1140px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
footer .row.social-links {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
footer .row.social-links a {
|
||||
@ -90,22 +104,79 @@ footer .row.version p a {
|
||||
}
|
||||
|
||||
.main-logo {
|
||||
max-width: 220px;
|
||||
margin: 0 auto 20px auto;
|
||||
width: 220px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
@media (max-width: 1200px) {
|
||||
|
||||
footer .row.main .links.outer {
|
||||
.main-logo {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
footer .row.main {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
footer .row.link-tree {
|
||||
max-width: 90%;
|
||||
font-size: 13px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
footer .row.social-links svg {
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
.main-logo {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
footer .row.link-tree {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
footer .row.social-links svg {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
footer .row.link-tree {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer .row.main .links.outer > .row {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
footer .row.main .links.outer > .row > div:first-child {
|
||||
footer .link-tree .links {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
footer .row.main .branding {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main-logo {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
footer .site-options {
|
||||
float: none;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
footer .row.social-links {
|
||||
margin: 48px 0 24px 0;
|
||||
}
|
||||
|
||||
footer .selector {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
footer .selector:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
@reboot screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet
|
||||
@reboot /usr/local/bin/bitcoind -testnet >/dev/null 2>&1
|
||||
@reboot screen -dmS testnet /bitcoin/electrs/electrs-start-testnet
|
||||
@reboot /usr/local/bin/bitcoind -signet >/dev/null 2>&1
|
||||
@reboot screen -dmS signet /bitcoin/electrs/electrs-start-signet
|
||||
@reboot sleep 5 ; /usr/local/bin/bitcoind -testnet >/dev/null 2>&1
|
||||
@reboot sleep 5 ; /usr/local/bin/bitcoind -signet >/dev/null 2>&1
|
||||
@reboot sleep 10 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet
|
||||
@reboot sleep 10 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet
|
||||
@reboot sleep 10 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet
|
||||
|
@ -1,10 +1,10 @@
|
||||
# start elements on reboot
|
||||
@reboot /usr/local/bin/elementsd -chain=liquidv1 >/dev/null 2>&1
|
||||
@reboot /usr/local/bin/elementsd -chain=liquidtestnet >/dev/null 2>&1
|
||||
@reboot sleep 5 ; /usr/local/bin/elementsd -chain=liquidv1 >/dev/null 2>&1
|
||||
@reboot sleep 5 ; /usr/local/bin/elementsd -chain=liquidtestnet >/dev/null 2>&1
|
||||
|
||||
# start electrs on reboot
|
||||
@reboot screen -dmS liquidv1 /elements/electrs/electrs-start-liquid
|
||||
@reboot screen -dmS liquidtestnet /elements/electrs/electrs-start-liquidtestnet
|
||||
@reboot sleep 20 ; screen -dmS liquidv1 /elements/electrs/electrs-start-liquid
|
||||
@reboot sleep 20 ; screen -dmS liquidtestnet /elements/electrs/electrs-start-liquidtestnet
|
||||
|
||||
# hourly asset update and electrs restart
|
||||
6 * * * * cd $HOME/asset_registry_db && git pull --quiet origin master && cd $HOME/asset_registry_testnet_db && git pull --quiet origin master && killall electrs
|
||||
|
@ -1449,7 +1449,7 @@ if [ "${UNFURL_INSTALL}" = ON ];then
|
||||
|
||||
echo "[*] Installing color emoji"
|
||||
osSudo "${ROOT_USER}" curl "https://github.com/samuelngs/apple-emoji-linux/releases/download/ios-15.4/AppleColorEmoji.ttf" -o /usr/local/share/fonts/TTF/AppleColorEmoji.ttf
|
||||
cat >> /usr/local/etc/fonts/conf.d/01-emoji.conf <<EOF
|
||||
cat > /usr/local/etc/fonts/conf.d/01-emoji.conf <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||
<fontconfig>
|
||||
|
Loading…
x
Reference in New Issue
Block a user