Merge branch 'master' into nymkappa/bugfix/bisq-dump-file

This commit is contained in:
wiz 2022-06-01 00:14:43 +09:00 committed by GitHub
commit 72c3eea863
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
150 changed files with 50932 additions and 50074 deletions

View File

@ -21,7 +21,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 16.10.0
node-version: 16.15.0
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: ${{ matrix.browser }} browser tests (Mempool)

2
.nvmrc
View File

@ -1 +1 @@
v16.10.0
v16.15.0

176
README.md
View File

@ -10,14 +10,7 @@ It is an open-source project developed and operated for the benefit of the Bitco
Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi full-node distro all the way to a robust production instance on a powerful FreeBSD server.
We support the following installation methods, ranked in order from simple to advanced:
1) [One-click installation on full-node distros](#one-click-installation)
2) [Docker installation on Linux using docker-compose](./docker)
3) [Manual installation on Linux or FreeBSD](#manual-installation)
4) [Production installation on a powerful FreeBSD server](./production)
This doc offers install notes on the one-click method and manual install method. Follow the links above for install notes on Docker and production installations.
**Most people should use a one-click install method.** Other install methods are meant for developers and others with experience managing servers.
<a id="one-click-installation"></a>
## One-Click Installation
@ -29,167 +22,12 @@ Mempool can be conveniently installed on the following full-node distros:
- [myNode](https://github.com/mynodebtc/mynode)
- [Start9](https://github.com/Start9Labs/embassy-os)
<a id="manual-installation"></a>
## Manual Installation
**We highly recommend you deploy your own Mempool instance this way.** No matter which option you pick, you'll be able to get your own fully-sovereign instance of Mempool up quickly without needing to fiddle with any settings.
The following instructions are for a manual installation on Linux or FreeBSD. You may need to change file and directory paths to match your OS.
## Advanced Installation Methods
You will need [Bitcoin Core](https://github.com/bitcoin/bitcoin), [Electrum Server](https://github.com/romanz/electrs), [Node.js](https://github.com/nodejs/node), [MariaDB](https://github.com/mariadb/server), and [Nginx](https://github.com/nginx/nginx). Below, we walk through how to configure each of these.
Mempool can be installed in other ways too, but we only recommend doing so if you're a developer, have experience managing servers, or otherwise know what you're doing.
### 1. Get Latest Mempool Release
Clone the Mempool repo, and checkout the latest release tag:
```bash
git clone https://github.com/mempool/mempool
cd mempool
latestrelease=$(curl -s https://api.github.com/repos/mempool/mempool/releases/latest|grep tag_name|head -1|cut -d '"' -f4)
git checkout $latestrelease
```
### 2. Configure Bitcoin Core
Enable RPC and txindex in `bitcoin.conf`:
```bash
rpcuser=mempool
rpcpassword=mempool
txindex=1
```
### 3. Get & Configure MySQL
Install MariaDB from your OS package manager:
```bash
# Debian, Ubuntu, etc.
apt-get install mariadb-server mariadb-client
# macOS
brew install mariadb
mysql.server start
```
Create a database and grant privileges:
```bash
MariaDB [(none)]> drop database mempool;
Query OK, 0 rows affected (0.00 sec)
MariaDB [(none)]> create database mempool;
Query OK, 1 row affected (0.00 sec)
MariaDB [(none)]> grant all privileges on mempool.* to 'mempool'@'%' identified by 'mempool';
Query OK, 0 rows affected (0.00 sec)
```
### 4. Build Mempool Backend
Install Mempool dependencies with npm and build the backend:
```bash
cd backend
npm install --prod
npm run build
```
In the `backend` folder, make a copy of the sample config:
```bash
cp mempool-config.sample.json mempool-config.json
```
Edit `mempool-config.json` with your Bitcoin Core node RPC credentials:
```bash
{
"MEMPOOL": {
"NETWORK": "mainnet",
"BACKEND": "electrum",
"HTTP_PORT": 8999
},
"CORE_RPC": {
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool"
},
"ELECTRUM": {
"HOST": "127.0.0.1",
"PORT": 50002,
"TLS_ENABLED": true
},
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"PORT": 3306,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"DATABASE": "mempool"
}
}
```
Start the backend:
```bash
npm run start
```
When it's running, you should see output like this:
```bash
Mempool updated in 0.189 seconds
Updating mempool
Mempool updated in 0.096 seconds
Updating mempool
Mempool updated in 0.099 seconds
Updating mempool
Calculated fee for transaction 1 / 10
Calculated fee for transaction 2 / 10
Calculated fee for transaction 3 / 10
Calculated fee for transaction 4 / 10
Calculated fee for transaction 5 / 10
Calculated fee for transaction 6 / 10
Calculated fee for transaction 7 / 10
Calculated fee for transaction 8 / 10
Calculated fee for transaction 9 / 10
Calculated fee for transaction 10 / 10
Mempool updated in 0.243 seconds
Updating mempool
```
### 5. Build Mempool Frontend
Install the Mempool dependencies with npm and build the frontend:
```bash
cd frontend
npm install --prod
npm run build
```
Install the output into the nginx webroot folder:
```bash
sudo rsync -av --delete dist/ /var/www/
```
### 6. `nginx` + `certbot`
Install the supplied `nginx.conf` and `nginx-mempool.conf` in `/etc/nginx`:
```bash
# install nginx and certbot
apt-get install -y nginx python3-certbot-nginx
# install the mempool configuration for nginx
cp nginx.conf nginx-mempool.conf /etc/nginx/
# replace example.com with your domain name
certbot --nginx -d example.com
```
If everything went well, you should see the beautiful mempool :grin:
If you get stuck on "loading blocks", this means the websocket can't connect. Check your nginx proxy setup, firewalls, etc. and open an issue if you need help.
- See the [`docker/`](./docker/) directory for instructions on deploying Mempool with Docker.
- See the [`backend/`](./backend/) and [`frontend/`](./frontend/) directories for manual install instructions oriented for developers and small-scale deployments.
- See the [`production/`](./production/) directory for guidance on setting up a more serious Mempool instance designed for high performance at scale.

View File

@ -1,22 +1,161 @@
# Setup backend watchers
# Mempool Backend
The backend is static. Typescript scripts are compiled into the `dist` folder and served through a node web server.
These instructions are mostly intended for developers, but can be used as a basis for personal or small-scale production setups.
You can avoid the manual shutdown/recompile/restart command line cycle by using a watcher.
If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool does not provide support for custom setups.
Make sure you are in the `backend` directory `cd backend`.
See other ways to set up Mempool on [the main README](/../../#installation-methods).
1. Install nodemon and ts-node
Jump to a section in this doc:
- [Set Up the Backend](#setup)
- [Development Tips](#development-tips)
## Setup
### 1. Clone Mempool Repository
Get the latest Mempool code:
```
sudo npm install -g ts-node nodemon
git clone https://github.com/mempool/mempool
cd mempool
```
2. Run the watcher
Check out the latest release:
> Note: You can find your npm global binary folder using `npm -g bin`, where nodemon will be installed.
```
latestrelease=$(curl -s https://api.github.com/repos/mempool/mempool/releases/latest|grep tag_name|head -1|cut -d '"' -f4)
git checkout $latestrelease
```
### 2. Configure Bitcoin Core
Turn on `txindex`, enable RPC, and set RPC credentials in `bitcoin.conf`:
```
txindex=1
server=1
rpcuser=mempool
rpcpassword=mempool
```
### 3. Configure Electrum Server
[Pick an Electrum Server implementation](https://mempool.space/docs/faq#address-lookup-issues), configure it, and make sure it's synced.
**This step is optional.** You can run Mempool without configuring an Electrum Server for it, but address lookups will be disabled.
### 4. Configure MariaDB
_Mempool needs MariaDB v10.5 or later. If you already have MySQL installed, make sure to migrate any existing databases **before** installing MariaDB._
Get MariaDB from your operating system's package manager:
```
# Debian, Ubuntu, etc.
apt-get install mariadb-server mariadb-client
# macOS
brew install mariadb
mysql.server start
```
Create a database and grant privileges:
```
MariaDB [(none)]> drop database mempool;
Query OK, 0 rows affected (0.00 sec)
MariaDB [(none)]> create database mempool;
Query OK, 1 row affected (0.00 sec)
MariaDB [(none)]> grant all privileges on mempool.* to 'mempool'@'%' identified by 'mempool';
Query OK, 0 rows affected (0.00 sec)
```
### 5. Prepare Mempool Backend
#### Build
_Node.js 16 and npm 7 are recommended._
Install dependencies with `npm` and build the backend:
```
cd backend
npm install # add --prod for production
npm run build
```
#### Configure
In the backend folder, make a copy of the sample config file:
```
cp mempool-config.sample.json mempool-config.json
```
Edit `mempool-config.json` as needed.
In particular, make sure:
- the correct Bitcoin Core RPC credentials are specified in `CORE_RPC`
- the correct `BACKEND` is specified in `MEMPOOL`:
- "electrum" if you're using [romanz/electrs](https://github.com/romanz/electrs) or [cculianu/Fulcrum](https://github.com/cculianu/Fulcrum)
- "esplora" if you're using [Blockstream/electrs](https://github.com/Blockstream/electrs)
- "none" if you're not using any Electrum Server
### 6. Run Mempool Backend
Run the Mempool backend:
```
npm run start
```
When it's running, you should see output like this:
```
Mempool updated in 0.189 seconds
Updating mempool
Mempool updated in 0.096 seconds
Updating mempool
Mempool updated in 0.099 seconds
Updating mempool
Calculated fee for transaction 1 / 10
Calculated fee for transaction 2 / 10
Calculated fee for transaction 3 / 10
Calculated fee for transaction 4 / 10
Calculated fee for transaction 5 / 10
Calculated fee for transaction 6 / 10
Calculated fee for transaction 7 / 10
Calculated fee for transaction 8 / 10
Calculated fee for transaction 9 / 10
Calculated fee for transaction 10 / 10
Mempool updated in 0.243 seconds
Updating mempool
```
### 7. Set Up Mempool Frontend
With the backend configured and running, proceed to set up the [Mempool frontend](../frontend#manual-setup).
## Development Tips
### Set Up Backend Watchers
The Mempool backend is static. TypeScript scripts are compiled into the `dist` folder and served through a Node.js web server.
As a result, for development purposes, you may find it helpful to set up backend watchers to avoid the manual shutdown/recompile/restart command-line cycle.
First, install `nodemon` and `ts-node`:
```
npm install -g ts-node nodemon
```
Then, run the watcher:
```
nodemon src/index.ts --ignore cache/ --ignore pools.json
```
`nodemon` should be in npm's global binary folder. If needed, you can determine where that is with `npm -g bin`.

View File

@ -17,8 +17,8 @@
"mysql2": "2.3.3",
"node-worker-threads-pool": "^1.5.1",
"socks-proxy-agent": "^6.2.0",
"typescript": "~4.6.3",
"ws": "~8.6.0"
"typescript": "~4.7.2",
"ws": "~8.7.0"
},
"devDependencies": {
"@types/compression": "^1.7.2",
@ -1469,9 +1469,9 @@
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
},
"node_modules/typescript": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz",
"integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -1532,9 +1532,9 @@
"dev": true
},
"node_modules/ws": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz",
"integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz",
"integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==",
"engines": {
"node": ">=10.0.0"
},
@ -2708,9 +2708,9 @@
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
},
"typescript": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg=="
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz",
"integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A=="
},
"unpipe": {
"version": "1.0.0",
@ -2755,9 +2755,9 @@
"dev": true
},
"ws": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz",
"integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz",
"integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==",
"requires": {}
},
"yallist": {

View File

@ -36,8 +36,8 @@
"mysql2": "2.3.3",
"node-worker-threads-pool": "^1.5.1",
"socks-proxy-agent": "^6.2.0",
"typescript": "~4.6.3",
"ws": "~8.6.0"
"typescript": "~4.7.2",
"ws": "~8.7.0"
},
"devDependencies": {
"@types/compression": "^1.7.2",

View File

@ -2,7 +2,7 @@ import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, blockHash?: string): Promise<IEsploraApi.Transaction>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getBlockHeightTip(): Promise<number>;
$getTxIdsForBlock(hash: string): Promise<string[]>;
$getBlockHash(height: number): Promise<string>;

View File

@ -14,14 +14,32 @@ class BitcoinApi implements AbstractBitcoinApi {
this.bitcoindClient = bitcoinClient;
}
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false, blockHash?: string): Promise<IEsploraApi.Transaction> {
static convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block {
return {
id: block.hash,
height: block.height,
version: block.version,
timestamp: block.time,
bits: parseInt(block.bits, 16),
nonce: block.nonce,
difficulty: block.difficulty,
merkle_root: block.merkleroot,
tx_count: block.nTx,
size: block.size,
weight: block.weight,
previousblockhash: block.previousblockhash,
};
}
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
// If the transaction is in the mempool we already converted and fetched the fee. Only prevouts are missing
const txInMempool = mempool.getMempool()[txId];
if (txInMempool && addPrevout) {
return this.$addPrevouts(txInMempool);
}
return this.bitcoindClient.getRawTransaction(txId, true, blockHash)
return this.bitcoindClient.getRawTransaction(txId, true)
.then((transaction: IBitcoinApi.Transaction) => {
if (skipConversion) {
transaction.vout.forEach((vout) => {
@ -29,7 +47,7 @@ class BitcoinApi implements AbstractBitcoinApi {
});
return transaction;
}
return this.$convertTransaction(transaction, addPrevout);
return this.$convertTransaction(transaction, addPrevout, lazyPrevouts);
})
.catch((e: Error) => {
if (e.message.startsWith('The genesis block coinbase')) {
@ -109,7 +127,7 @@ class BitcoinApi implements AbstractBitcoinApi {
const outSpends: IEsploraApi.Outspend[] = [];
const tx = await this.$getRawTransaction(txId, true, false);
for (let i = 0; i < tx.vout.length; i++) {
if (tx.status && tx.status.block_height == 0) {
if (tx.status && tx.status.block_height === 0) {
outSpends.push({
spent: false
});
@ -128,7 +146,7 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
}
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
let esploraTransaction: IEsploraApi.Transaction = {
txid: transaction.txid,
version: transaction.version,
@ -174,35 +192,15 @@ class BitcoinApi implements AbstractBitcoinApi {
};
}
if (transaction.confirmations) {
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
} else {
if (addPrevout) {
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, false, lazyPrevouts);
} else if (!transaction.confirmations) {
esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
if (addPrevout) {
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
}
}
return esploraTransaction;
}
static convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block {
return {
id: block.hash,
height: block.height,
version: block.version,
timestamp: block.time,
bits: parseInt(block.bits, 16),
nonce: block.nonce,
difficulty: block.difficulty,
merkle_root: block.merkleroot,
tx_count: block.nTx,
size: block.size,
weight: block.weight,
previousblockhash: block.previousblockhash,
};
}
private translateScriptPubKeyType(outputType: string): string {
const map = {
'pubkey': 'p2pk',
@ -245,7 +243,7 @@ class BitcoinApi implements AbstractBitcoinApi {
if (vin.prevout) {
continue;
}
const innerTx = await this.$getRawTransaction(vin.txid, false);
const innerTx = await this.$getRawTransaction(vin.txid, false, false);
vin.prevout = innerTx.vout[vin.vout];
this.addInnerScriptsToVin(vin);
}
@ -271,22 +269,30 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.bitcoindClient.getRawMemPool(true);
}
private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean, lazyPrevouts: boolean): Promise<IEsploraApi.Transaction> {
if (transaction.vin[0].is_coinbase) {
transaction.fee = 0;
return transaction;
}
let totalIn = 0;
for (const vin of transaction.vin) {
const innerTx = await this.$getRawTransaction(vin.txid, !addPrevout);
if (addPrevout) {
vin.prevout = innerTx.vout[vin.vout];
this.addInnerScriptsToVin(vin);
for (let i = 0; i < transaction.vin.length; i++) {
if (lazyPrevouts && i > 12) {
transaction.vin[i].lazy = true;
continue;
}
totalIn += innerTx.vout[vin.vout].value;
const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
this.addInnerScriptsToVin(transaction.vin[i]);
totalIn += innerTx.vout[transaction.vin[i].vout].value;
}
if (lazyPrevouts && transaction.vin.length > 12) {
transaction.fee = -1;
} else {
const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0);
transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
}
const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0);
transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
return transaction;
}

View File

@ -33,6 +33,8 @@ export namespace IEsploraApi {
// Elements
is_pegin?: boolean;
issuance?: Issuance;
// Custom
lazy?: boolean;
}
interface Issuance {

View File

@ -15,6 +15,9 @@ import BitcoinApi from './bitcoin/bitcoin-api';
import { prepareBlock } from '../utils/blocks-utils';
import BlocksRepository from '../repositories/BlocksRepository';
import HashratesRepository from '../repositories/HashratesRepository';
import indexer from '../indexer';
import fiatConversion from './fiat-conversion';
import RatesRepository from '../repositories/RatesRepository';
class Blocks {
private blocks: BlockExtended[] = [];
@ -23,9 +26,6 @@ class Blocks {
private lastDifficultyAdjustmentTime = 0;
private previousDifficultyRetarget = 0;
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
private blockIndexingStarted = false;
public blockIndexingCompleted = false;
public reindexFlag = false;
constructor() { }
@ -134,7 +134,7 @@ class Blocks {
blockExtended.extras.avgFeeRate = stats.avgfeerate;
}
if (Common.indexingEnabled()) {
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
let pool: PoolTag;
if (blockExtended.extras?.coinbaseTx !== undefined) {
pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
@ -143,7 +143,8 @@ class Blocks {
}
if (!pool) { // We should never have this situation in practise
logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. Check your "pools" table entries`);
logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. ` +
`Check your "pools" table entries`);
return blockExtended;
}
@ -196,24 +197,15 @@ class Blocks {
* [INDEXING] Index all blocks metadata for the mining dashboard
*/
public async $generateBlockDatabase() {
if (this.blockIndexingStarted && !this.reindexFlag) {
return;
}
this.reindexFlag = false;
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync
return;
}
this.blockIndexingStarted = true;
this.blockIndexingCompleted = false;
try {
let currentBlockHeight = blockchainInfo.blocks;
let indexingBlockAmount = config.MEMPOOL.INDEXING_BLOCKS_AMOUNT;
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, blockchainInfo.blocks);
if (indexingBlockAmount <= -1) {
indexingBlockAmount = currentBlockHeight + 1;
}
@ -274,14 +266,14 @@ class Blocks {
loadingIndicators.setProgress('block-indexing', 100);
} catch (e) {
logger.err('Block indexing failed. Trying again later. Reason: ' + (e instanceof Error ? e.message : e));
this.blockIndexingStarted = false;
loadingIndicators.setProgress('block-indexing', 100);
return;
}
const chainValid = await BlocksRepository.$validateChain();
this.reindexFlag = !chainValid;
this.blockIndexingCompleted = chainValid;
if (!chainValid) {
indexer.reindex();
}
}
public async $updateBlocks() {
@ -298,6 +290,8 @@ class Blocks {
logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${config.MEMPOOL.INITIAL_BLOCKS_AMOUNT} recent blocks`);
this.currentBlockHeight = blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT;
fastForwarded = true;
logger.info(`Re-indexing skipped blocks and corresponding hashrates data`);
indexer.reindex(); // Make sure to index the skipped blocks #1619
}
if (!this.lastDifficultyAdjustmentTime) {
@ -349,6 +343,9 @@ class Blocks {
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
}
if (fiatConversion.ratesInitialized === true) {
await RatesRepository.$saveRate(blockExtended.height, fiatConversion.getConversionRates());
}
if (block.height % 2016 === 0) {
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
@ -389,10 +386,43 @@ class Blocks {
return prepareBlock(blockExtended);
}
public async $getBlocksExtras(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
// Note - This API is breaking if indexing is not available. For now it is okay because we only
// use it for the mining pages, and mining pages should not be available if indexing is turned off.
// I'll need to fix it before we refactor the block(s) related pages
/**
* Index a block by hash if it's missing from the database. Returns the block after indexing
*/
public async $getBlock(hash: string): Promise<BlockExtended | IEsploraApi.Block> {
// Check the memory cache
const blockByHash = this.getBlocks().find((b) => b.id === hash);
if (blockByHash) {
return blockByHash;
}
// Block has already been indexed
if (Common.indexingEnabled()) {
const dbBlock = await blocksRepository.$getBlockByHash(hash);
if (dbBlock != null) {
return prepareBlock(dbBlock);
}
}
const block = await bitcoinApi.$getBlock(hash);
// Not Bitcoin network, return the block as it
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return block;
}
// Bitcoin network, add our custom data on top
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
if (Common.indexingEnabled()) {
delete(blockExtended['coinbaseTx']);
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
return blockExtended;
}
public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
try {
let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight();
const returnBlocks: BlockExtended[] = [];
@ -401,25 +431,32 @@ class Blocks {
return returnBlocks;
}
if (currentHeight === 0 && Common.indexingEnabled()) {
currentHeight = await blocksRepository.$mostRecentBlockHeight();
}
// Check if block height exist in local cache to skip the hash lookup
const blockByHeight = this.getBlocks().find((b) => b.height === currentHeight);
let startFromHash: string | null = null;
if (blockByHeight) {
startFromHash = blockByHeight.id;
} else {
} else if (!Common.indexingEnabled()) {
startFromHash = await bitcoinApi.$getBlockHash(currentHeight);
}
let nextHash = startFromHash;
for (let i = 0; i < limit && currentHeight >= 0; i++) {
let block = this.getBlocks().find((b) => b.height === currentHeight);
if (!block && Common.indexingEnabled()) {
if (block) {
returnBlocks.push(block);
} else if (Common.indexingEnabled()) {
block = await this.$indexBlock(currentHeight);
} else if (!block) {
returnBlocks.push(block);
} else if (nextHash != null) {
block = prepareBlock(await bitcoinApi.$getBlock(nextHash));
nextHash = block.previousblockhash;
returnBlocks.push(block);
}
returnBlocks.push(block);
nextHash = block.previousblockhash;
currentHeight--;
}

View File

@ -169,12 +169,12 @@ export class Common {
default: return null;
}
}
static indexingEnabled(): boolean {
return (
['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) &&
['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK) &&
config.DATABASE.ENABLED === true &&
config.MEMPOOL.INDEXING_BLOCKS_AMOUNT != 0
config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== 0
);
}
}

View File

@ -4,7 +4,7 @@ import logger from '../logger';
import { Common } from './common';
class DatabaseMigration {
private static currentVersion = 17;
private static currentVersion = 19;
private queryTimeout = 120000;
private statisticsAddedIndexed = false;
@ -180,6 +180,14 @@ class DatabaseMigration {
if (databaseSchemaVersion < 17 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
}
if (databaseSchemaVersion < 18 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
}
if (databaseSchemaVersion < 19) {
await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates'));
}
} catch (e) {
throw e;
}
@ -462,6 +470,14 @@ class DatabaseMigration {
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateRatesTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS rates (
height int(10) unsigned NOT NULL,
bisq_rates JSON NOT NULL,
PRIMARY KEY (height)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
public async $truncateIndexedData(tables: string[]) {
const allowedTables = ['blocks', 'hashrates'];

View File

@ -32,7 +32,7 @@ class DifficultyAdjustmentApi {
}
}
let timeAvgMins = blocksInEpoch ? diff / blocksInEpoch / 60 : 10;
let timeAvgMins = blocksInEpoch && blocksInEpoch > 146 ? diff / blocksInEpoch / 60 : 10;
// Testnet difficulty is set to 1 after 20 minutes of no blocks,
// therefore the time between blocks will always be below 20 minutes (1200s).

View File

@ -1,4 +1,3 @@
import config from '../config';
import { MempoolBlock } from '../mempool.interfaces';
import { Common } from './common';
import mempool from './mempool';
@ -19,6 +18,7 @@ class FeeApi {
'fastestFee': this.defaultFee,
'halfHourFee': this.defaultFee,
'hourFee': this.defaultFee,
'economyFee': minimumFee,
'minimumFee': minimumFee,
};
}
@ -31,6 +31,7 @@ class FeeApi {
'fastestFee': firstMedianFee,
'halfHourFee': secondMedianFee,
'hourFee': thirdMedianFee,
'economyFee': Math.min(2 * minimumFee, thirdMedianFee),
'minimumFee': minimumFee,
};
}

View File

@ -6,12 +6,19 @@ import backendInfo from './backend-info';
import { SocksProxyAgent } from 'socks-proxy-agent';
class FiatConversion {
private conversionRates: IConversionRates = {
'USD': 0
};
private debasingFiatCurrencies = ['AED', 'AUD', 'BDT', 'BHD', 'BMD', 'BRL', 'CAD', 'CHF', 'CLP',
'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HUF', 'IDR', 'ILS', 'INR', 'JPY', 'KRW', 'KWD',
'LKR', 'MMK', 'MXN', 'MYR', 'NGN', 'NOK', 'NZD', 'PHP', 'PKR', 'PLN', 'RUB', 'SAR', 'SEK',
'SGD', 'THB', 'TRY', 'TWD', 'UAH', 'USD', 'VND', 'ZAR'];
private conversionRates: IConversionRates = {};
private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined;
public ratesInitialized = false; // If true, it means rates are ready for use
constructor() { }
constructor() {
for (const fiat of this.debasingFiatCurrencies) {
this.conversionRates[fiat] = 0;
}
}
public setProgressChangedCallback(fn: (rates: IConversionRates) => void) {
this.ratesChangedCallback = fn;
@ -62,13 +69,14 @@ class FiatConversion {
response = await axios.get(fiatConversionUrl, { headers: headers, timeout: 10000 });
}
const usd = response.data.data.find((item: any) => item.currencyCode === 'USD');
for (const rate of response.data.data) {
if (this.debasingFiatCurrencies.includes(rate.currencyCode) && rate.provider === 'Bisq-Aggregate') {
this.conversionRates[rate.currencyCode] = Math.round(100 * rate.price) / 100;
}
}
this.conversionRates = {
'USD': usd.price,
};
logger.debug(`USD Conversion Rate: ${usd.price}`);
this.ratesInitialized = true;
logger.debug(`USD Conversion Rate: ${this.conversionRates.USD}`);
if (this.ratesChangedCallback) {
this.ratesChangedCallback(this.conversionRates);

View File

@ -4,14 +4,11 @@ import PoolsRepository from '../repositories/PoolsRepository';
import HashratesRepository from '../repositories/HashratesRepository';
import bitcoinClient from './bitcoin/bitcoin-client';
import logger from '../logger';
import blocks from './blocks';
import { Common } from './common';
import loadingIndicators from './loading-indicators';
import { escape } from 'mysql2';
class Mining {
hashrateIndexingStarted = false;
weeklyHashrateIndexingStarted = false;
constructor() {
}
@ -92,14 +89,18 @@ class Mining {
});
poolsStatistics['pools'] = poolsStats;
poolsStatistics['oldestIndexedBlockTimestamp'] = await BlocksRepository.$oldestBlockTimestamp();
const blockCount: number = await BlocksRepository.$blockCount(null, interval);
poolsStatistics['blockCount'] = blockCount;
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h);
poolsStatistics['lastEstimatedHashrate'] = lastBlockHashrate;
try {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate');
}
return poolsStatistics;
}
@ -110,7 +111,7 @@ class Mining {
public async $getPoolStat(slug: string): Promise<object> {
const pool = await PoolsRepository.$getPool(slug);
if (!pool) {
throw new Error(`This mining pool does not exist`);
throw new Error('This mining pool does not exist ' + escape(slug));
}
const blockCount: number = await BlocksRepository.$blockCount(pool.id);
@ -122,7 +123,12 @@ class Mining {
const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w');
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
const currentEstimatedkHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h);
let currentEstimatedHashrate = 0;
try {
currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate');
}
return {
pool: pool,
@ -136,7 +142,7 @@ class Mining {
'24h': blockCount24h / totalBlock24h,
'1w': blockCount1w / totalBlock1w,
},
estimatedHashrate: currentEstimatedkHashrate * (blockCount24h / totalBlock24h),
estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h),
reportedHashrate: null,
};
}
@ -152,14 +158,9 @@ class Mining {
* [INDEXING] Generate weekly mining pool hashrate history
*/
public async $generatePoolHashrateHistory(): Promise<void> {
if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted || this.weeklyHashrateIndexingStarted) {
return;
}
const now = new Date();
try {
this.weeklyHashrateIndexingStarted = true;
const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing');
// Run only if:
@ -167,11 +168,9 @@ class Mining {
// * we started a new week (around Monday midnight)
const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate();
if (!runIndexing) {
this.weeklyHashrateIndexingStarted = false;
return;
}
} catch (e) {
this.weeklyHashrateIndexingStarted = false;
throw e;
}
@ -191,6 +190,7 @@ class Mining {
const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000;
logger.debug(`Indexing weekly mining pool hashrate`);
loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp) {
@ -255,7 +255,6 @@ class Mining {
++indexedThisRun;
++totalIndexed;
}
this.weeklyHashrateIndexingStarted = false;
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate());
if (newlyIndexed > 0) {
logger.info(`Indexed ${newlyIndexed} pools weekly hashrate`);
@ -263,7 +262,6 @@ class Mining {
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
} catch (e) {
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
this.weeklyHashrateIndexingStarted = false;
throw e;
}
}
@ -272,22 +270,14 @@ class Mining {
* [INDEXING] Generate daily hashrate data
*/
public async $generateNetworkHashrateHistory(): Promise<void> {
if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) {
return;
}
try {
this.hashrateIndexingStarted = true;
// We only run this once a day around midnight
const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing');
const now = new Date().getUTCDate();
if (now === latestRunDate) {
this.hashrateIndexingStarted = false;
return;
}
} catch (e) {
this.hashrateIndexingStarted = false;
throw e;
}
@ -305,6 +295,7 @@ class Mining {
const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000;
logger.debug(`Indexing daily network hashrate`);
loadingIndicators.setProgress('daily-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp) {
@ -376,14 +367,12 @@ class Mining {
await HashratesRepository.$saveHashrates(hashrates);
await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate());
this.hashrateIndexingStarted = false;
if (newlyIndexed > 0) {
logger.info(`Indexed ${newlyIndexed} day of network hashrate`);
}
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
} catch (e) {
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
this.hashrateIndexingStarted = false;
throw e;
}
}

View File

@ -21,8 +21,8 @@ class TransactionUtils {
};
}
public async $getTransactionExtended(txId: string, addPrevouts = false): Promise<TransactionExtended> {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts);
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false): Promise<TransactionExtended> {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
return this.extendTransaction(transaction);
}

View File

@ -25,10 +25,8 @@ import databaseMigration from './api/database-migration';
import syncAssets from './sync-assets';
import icons from './api/liquid/icons';
import { Common } from './api/common';
import mining from './api/mining';
import HashratesRepository from './repositories/HashratesRepository';
import BlocksRepository from './repositories/BlocksRepository';
import poolsUpdater from './tasks/pools-updater';
import indexer from './indexer';
class Server {
private wss: WebSocket.Server | undefined;
@ -99,7 +97,7 @@ class Server {
}
await databaseMigration.$initializeOrMigrateDatabase();
if (Common.indexingEnabled()) {
await this.$resetHashratesIndexingState();
await indexer.$resetHashratesIndexingState();
}
} catch (e) {
throw new Error(e instanceof Error ? e.message : 'Error');
@ -154,7 +152,7 @@ class Server {
await poolsUpdater.updatePoolsJson();
await blocks.$updateBlocks();
await memPool.$updateMempool();
this.$runIndexingWhenReady();
indexer.$run();
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
this.currentBackendRetryInterval = 5;
@ -173,29 +171,6 @@ class Server {
}
}
async $resetHashratesIndexingState() {
try {
await HashratesRepository.$setLatestRun('last_hashrates_indexing', 0);
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0);
} catch (e) {
logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e));
}
}
async $runIndexingWhenReady() {
if (!Common.indexingEnabled() || mempool.hasPriority()) {
return;
}
try {
await blocks.$generateBlockDatabase();
await mining.$generateNetworkHashrateHistory();
await mining.$generatePoolHashrateHistory();
} catch (e) {
logger.err(`Indexing failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e));
}
}
setUpWebsocketHandling() {
if (this.wss) {
websocketHandler.setWebsocketServer(this.wss);
@ -300,24 +275,12 @@ class Server {
if (Common.indexingEnabled()) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/24h', routes.$getPools.bind(routes, '24h'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3d', routes.$getPools.bind(routes, '3d'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/1w', routes.$getPools.bind(routes, '1w'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/1m', routes.$getPools.bind(routes, '1m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3m', routes.$getPools.bind(routes, '3m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/6m', routes.$getPools.bind(routes, '6m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/1y', routes.$getPools.bind(routes, '1y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/2y', routes.$getPools.bind(routes, '2y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3y', routes.$getPools.bind(routes, '3y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/all', routes.$getPools.bind(routes, 'all'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/:interval', routes.$getPools)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', routes.$getPoolHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', routes.$getPoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks/:height', routes.$getPoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', routes.$getPool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/:interval', routes.$getPool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools', routes.$getPoolsHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate', routes.$getHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees)
@ -349,8 +312,9 @@ class Server {
}
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras', routes.getBlocksExtras)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras/:height', routes.getBlocksExtras);
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock);
if (config.MEMPOOL.BACKEND !== 'esplora') {
this.app
@ -362,10 +326,7 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', routes.getRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', routes.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', routes.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', routes.getBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', routes.getBlockTransactions)

52
backend/src/indexer.ts Normal file
View File

@ -0,0 +1,52 @@
import { Common } from './api/common';
import blocks from './api/blocks';
import mempool from './api/mempool';
import mining from './api/mining';
import logger from './logger';
import HashratesRepository from './repositories/HashratesRepository';
class Indexer {
runIndexer = true;
indexerRunning = false;
constructor() {
}
public reindex() {
this.runIndexer = true;
}
public async $run() {
if (!Common.indexingEnabled() || this.runIndexer === false ||
this.indexerRunning === true || mempool.hasPriority()
) {
return;
}
this.runIndexer = false;
this.indexerRunning = true;
try {
await blocks.$generateBlockDatabase();
await this.$resetHashratesIndexingState();
await mining.$generateNetworkHashrateHistory();
await mining.$generatePoolHashrateHistory();
} catch (e) {
this.reindex();
logger.err(`Indexer failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e));
}
this.indexerRunning = false;
}
async $resetHashratesIndexingState() {
try {
await HashratesRepository.$setLatestRun('last_hashrates_indexing', 0);
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0);
} catch (e) {
logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e));
}
}
}
export default new Indexer();

View File

@ -81,7 +81,7 @@ export interface TransactionStripped {
export interface BlockExtension {
totalFees?: number;
medianFee?: number; // Actually the median fee rate that we compute ourself
medianFee?: number;
feeRange?: number[];
reward?: number;
coinbaseTx?: TransactionMinerInfo;

View File

@ -5,6 +5,7 @@ import { Common } from '../api/common';
import { prepareBlock } from '../utils/blocks-utils';
import PoolsRepository from './PoolsRepository';
import HashratesRepository from './HashratesRepository';
import { escape } from 'mysql2';
class BlocksRepository {
/**
@ -120,6 +121,19 @@ class BlocksRepository {
}
}
/**
* Return most recent block height
*/
public async $mostRecentBlockHeight(): Promise<number> {
try {
const [row] = await DB.query('SELECT MAX(height) as maxHeight from blocks');
return row[0]['maxHeight'];
} catch (e) {
logger.err(`Cannot count blocks for this pool (using offset). Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get blocks count for a period
*/
@ -235,12 +249,30 @@ class BlocksRepository {
public async $getBlocksByPool(slug: string, startHeight?: number): Promise<object[]> {
const pool = await PoolsRepository.$getPool(slug);
if (!pool) {
throw new Error(`This mining pool does not exist`);
throw new Error('This mining pool does not exist ' + escape(slug));
}
const params: any[] = [];
let query = ` SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
previous_block_hash as previousblockhash
let query = ` SELECT
height,
hash as id,
UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
size,
weight,
tx_count,
coinbase_raw,
difficulty,
fees,
fee_span,
median_fee,
reward,
version,
bits,
nonce,
merkle_root,
previous_block_hash as previousblockhash,
avg_fee,
avg_fee_rate
FROM blocks
WHERE pool_id = ?`;
params.push(pool.id);
@ -273,11 +305,32 @@ class BlocksRepository {
*/
public async $getBlockByHeight(height: number): Promise<object | null> {
try {
const [rows]: any[] = await DB.query(`
SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.slug as pool_slug,
pools.addresses as pool_addresses, pools.regexes as pool_regexes,
previous_block_hash as previousblockhash
const [rows]: any[] = await DB.query(`SELECT
height,
hash as id,
UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
size,
weight,
tx_count,
coinbase_raw,
difficulty,
pools.id as pool_id,
pools.name as pool_name,
pools.link as pool_link,
pools.slug as pool_slug,
pools.addresses as pool_addresses,
pools.regexes as pool_regexes,
fees,
fee_span,
median_fee,
reward,
version,
bits,
nonce,
merkle_root,
previous_block_hash as previousblockhash,
avg_fee,
avg_fee_rate
FROM blocks
JOIN pools ON blocks.pool_id = pools.id
WHERE height = ${height};
@ -287,6 +340,7 @@ class BlocksRepository {
return null;
}
rows[0].fee_span = JSON.parse(rows[0].fee_span);
return rows[0];
} catch (e) {
logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
@ -294,6 +348,34 @@ class BlocksRepository {
}
}
/**
* Get one block by hash
*/
public async $getBlockByHash(hash: string): Promise<object | null> {
try {
const query = `
SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, hash as id,
pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.slug as pool_slug,
pools.addresses as pool_addresses, pools.regexes as pool_regexes,
previous_block_hash as previousblockhash
FROM blocks
JOIN pools ON blocks.pool_id = pools.id
WHERE hash = '${hash}';
`;
const [rows]: any[] = await DB.query(query);
if (rows.length <= 0) {
return null;
}
rows[0].fee_span = JSON.parse(rows[0].fee_span);
return rows[0];
} catch (e) {
logger.err(`Cannot get indexed block ${hash}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Return blocks difficulty
*/
@ -397,18 +479,28 @@ class BlocksRepository {
const [blocks]: any[] = await DB.query(`SELECT height, hash, previous_block_hash,
UNIX_TIMESTAMP(blockTimestamp) as timestamp FROM blocks ORDER BY height`);
let currentHeight = 1;
while (currentHeight < blocks.length) {
if (blocks[currentHeight].previous_block_hash !== blocks[currentHeight - 1].hash) {
logger.warn(`Chain divergence detected at block ${blocks[currentHeight - 1].height}, re-indexing newer blocks and hashrates`);
await this.$deleteBlocksFrom(blocks[currentHeight - 1].height);
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[currentHeight - 1].timestamp - 604800);
let partialMsg = false;
let idx = 1;
while (idx < blocks.length) {
if (blocks[idx].height - 1 !== blocks[idx - 1].height) {
if (partialMsg === false) {
logger.info('Some blocks are not indexed, skipping missing blocks during chain validation');
partialMsg = true;
}
++idx;
continue;
}
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}, re-indexing newer blocks and hashrates`);
await this.$deleteBlocksFrom(blocks[idx - 1].height);
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
return false;
}
++currentHeight;
++idx;
}
logger.info(`${currentHeight} blocks hash validated in ${new Date().getTime() - start} ms`);
logger.info(`${idx} blocks hash validated in ${new Date().getTime() - start} ms`);
return true;
} catch (e) {
logger.err('Cannot validate chain of block hash. Reason: ' + (e instanceof Error ? e.message : e));
@ -435,9 +527,9 @@ class BlocksRepository {
public async $getHistoricalBlockFees(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avg_height,
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(fees) as INT) as avg_fees
CAST(AVG(fees) as INT) as avgFees
FROM blocks`;
if (interval !== null) {
@ -457,12 +549,12 @@ class BlocksRepository {
/**
* Get the historical averaged block rewards
*/
public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise<any> {
public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avg_height,
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(reward) as INT) as avg_rewards
CAST(AVG(reward) as INT) as avgRewards
FROM blocks`;
if (interval !== null) {
@ -485,15 +577,15 @@ class BlocksRepository {
public async $getHistoricalBlockFeeRates(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avg_height,
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(JSON_EXTRACT(fee_span, '$[0]')) as INT) as avg_fee_0,
CAST(AVG(JSON_EXTRACT(fee_span, '$[1]')) as INT) as avg_fee_10,
CAST(AVG(JSON_EXTRACT(fee_span, '$[2]')) as INT) as avg_fee_25,
CAST(AVG(JSON_EXTRACT(fee_span, '$[3]')) as INT) as avg_fee_50,
CAST(AVG(JSON_EXTRACT(fee_span, '$[4]')) as INT) as avg_fee_75,
CAST(AVG(JSON_EXTRACT(fee_span, '$[5]')) as INT) as avg_fee_90,
CAST(AVG(JSON_EXTRACT(fee_span, '$[6]')) as INT) as avg_fee_100
CAST(AVG(JSON_EXTRACT(fee_span, '$[0]')) as INT) as avgFee_0,
CAST(AVG(JSON_EXTRACT(fee_span, '$[1]')) as INT) as avgFee_10,
CAST(AVG(JSON_EXTRACT(fee_span, '$[2]')) as INT) as avgFee_25,
CAST(AVG(JSON_EXTRACT(fee_span, '$[3]')) as INT) as avgFee_50,
CAST(AVG(JSON_EXTRACT(fee_span, '$[4]')) as INT) as avgFee_75,
CAST(AVG(JSON_EXTRACT(fee_span, '$[5]')) as INT) as avgFee_90,
CAST(AVG(JSON_EXTRACT(fee_span, '$[6]')) as INT) as avgFee_100
FROM blocks`;
if (interval !== null) {
@ -516,9 +608,9 @@ class BlocksRepository {
public async $getHistoricalBlockSizes(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avg_height,
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(size) as INT) as avg_size
CAST(AVG(size) as INT) as avgSize
FROM blocks`;
if (interval !== null) {
@ -541,9 +633,9 @@ class BlocksRepository {
public async $getHistoricalBlockWeights(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avg_height,
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(weight) as INT) as avg_weight
CAST(AVG(weight) as INT) as avgWeight
FROM blocks`;
if (interval !== null) {

View File

@ -1,3 +1,4 @@
import { escape } from 'mysql2';
import { Common } from '../api/common';
import DB from '../database';
import logger from '../logger';
@ -105,7 +106,7 @@ class HashratesRepository {
public async $getPoolWeeklyHashrate(slug: string): Promise<any[]> {
const pool = await PoolsRepository.$getPool(slug);
if (!pool) {
throw new Error(`This mining pool does not exist`);
throw new Error('This mining pool does not exist ' + escape(slug));
}
// Find hashrate boundaries

View File

@ -78,7 +78,6 @@ class PoolsRepository {
const [rows]: any[] = await DB.query(query, [slug]);
if (rows.length < 1) {
logger.debug(`This slug does not match any known pool`);
return null;
}

View File

@ -0,0 +1,21 @@
import DB from '../database';
import logger from '../logger';
import { IConversionRates } from '../mempool.interfaces';
class RatesRepository {
public async $saveRate(height: number, rates: IConversionRates) {
try {
await DB.query(`INSERT INTO rates(height, bisq_rates) VALUE (?, ?)`, [height, JSON.stringify(rates)]);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Rate already exists for block ${height}, ignoring`);
} else {
logger.err(`Cannot save exchange rate into db for block ${height} Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
}
}
export default new RatesRepository();

View File

@ -572,9 +572,9 @@ class Routes {
}
}
public async $getPools(interval: string, req: Request, res: Response) {
public async $getPools(req: Request, res: Response) {
try {
const stats = await miningStats.$getPoolsStats(interval);
const stats = await miningStats.$getPoolsStats(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
@ -588,7 +588,7 @@ class Routes {
public async $getPoolsHistoricalHashrate(req: Request, res: Response) {
try {
const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval ?? null);
const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
@ -619,9 +619,17 @@ class Routes {
}
public async $getHistoricalHashrate(req: Request, res: Response) {
let currentHashrate = 0, currentDifficulty = 0;
try {
const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval ?? null);
const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval ?? null);
currentHashrate = await bitcoinClient.getNetworkHashPs();
currentDifficulty = await bitcoinClient.getDifficulty();
} catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
}
try {
const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval);
const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
@ -630,8 +638,8 @@ class Routes {
res.json({
hashrates: hashrates,
difficulty: difficulty,
currentHashrate: await bitcoinClient.getNetworkHashPs(),
currentDifficulty: await bitcoinClient.getDifficulty(),
currentHashrate: currentHashrate,
currentDifficulty: currentDifficulty,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
@ -640,12 +648,12 @@ class Routes {
public async $getHistoricalBlockFees(req: Request, res: Response) {
try {
const blockFees = await mining.$getHistoricalBlockFees(req.params.interval ?? null);
const blockFees = await mining.$getHistoricalBlockFees(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
@ -654,12 +662,12 @@ class Routes {
public async $getHistoricalBlockRewards(req: Request, res: Response) {
try {
const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval ?? null);
const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
@ -668,15 +676,13 @@ class Routes {
public async $getHistoricalBlockFeeRates(req: Request, res: Response) {
try {
const blockFeeRates = await mining.$getHistoricalBlockFeeRates(req.params.interval ?? null);
const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
const blockFeeRates = await mining.$getHistoricalBlockFeeRates(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json({
oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp,
blockFeeRates: blockFeeRates,
});
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
@ -684,13 +690,13 @@ class Routes {
public async $getHistoricalBlockSizeAndWeight(req: Request, res: Response) {
try {
const blockSizes = await mining.$getHistoricalBlockSizes(req.params.interval ?? null);
const blockWeights = await mining.$getHistoricalBlockWeights(req.params.interval ?? null);
const blockSizes = await mining.$getHistoricalBlockSizes(req.params.interval);
const blockWeights = await mining.$getHistoricalBlockWeights(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json({
sizes: blockSizes,
weights: blockWeights
@ -702,8 +708,9 @@ class Routes {
public async getBlock(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getBlock(req.params.hash);
res.json(result);
const block = await blocks.$getBlock(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(block);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
@ -719,19 +726,22 @@ class Routes {
}
}
public async getBlocksExtras(req: Request, res: Response) {
public async getBlocks(req: Request, res: Response) {
try {
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.json(await blocks.$getBlocksExtras(height, 15));
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocks(height, 15));
} else { // Liquid, Bisq
return await this.getLegacyBlocks(req, res);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async getBlocks(req: Request, res: Response) {
try {
loadingIndicators.setProgress('blocks', 0);
public async getLegacyBlocks(req: Request, res: Response) {
try {
const returnBlocks: IEsploraApi.Block[] = [];
const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight();
@ -755,16 +765,15 @@ class Routes {
returnBlocks.push(block);
nextHash = block.previousblockhash;
}
loadingIndicators.setProgress('blocks', i / 10 * 100);
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks);
} catch (e) {
loadingIndicators.setProgress('blocks', 100);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async getBlockTransactions(req: Request, res: Response) {
try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@ -776,9 +785,9 @@ class Routes {
const endIndex = Math.min(startingIndex + 10, txIds.length);
for (let i = startingIndex; i < endIndex; i++) {
try {
const transaction = await transactionUtils.$getTransactionExtended(txIds[i], true);
const transaction = await transactionUtils.$getTransactionExtended(txIds[i], true, true);
transactions.push(transaction);
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i + 1) / endIndex * 100);
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i - startingIndex + 1) / (endIndex - startingIndex) * 100);
} catch (e) {
logger.debug('getBlockTransactions error: ' + (e instanceof Error ? e.message : e));
}
@ -1001,6 +1010,7 @@ class Routes {
public async $getRewardStats(req: Request, res: Response) {
try {
const response = await mining.$getRewardStats(parseInt(req.params.blockCount, 10));
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(response);
} catch (e) {
res.status(500).end();

View File

@ -1,8 +1,10 @@
const https = require('https');
import axios from 'axios';
import poolsParser from '../api/pools-parser';
import config from '../config';
import DB from '../database';
import logger from '../logger';
import { SocksProxyAgent } from 'socks-proxy-agent';
import * as https from 'https';
/**
* Maintain the most recent version of pools.json
@ -14,7 +16,7 @@ class PoolsUpdater {
}
public async updatePoolsJson() {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || config.DATABASE.ENABLED === false) {
return;
}
@ -28,6 +30,13 @@ class PoolsUpdater {
this.lastRun = now;
logger.info('Updating latest mining pools from Github');
if (config.SOCKS5PROXY.ENABLED) {
logger.info('List of public pools will be queried over the Tor network');
} else {
logger.info('List of public pools will be queried over clearnet');
}
try {
const dbSha = await this.getShaFromDb();
const githubSha = await this.fetchPoolsSha(); // Fetch pools.json sha from github
@ -41,7 +50,10 @@ class PoolsUpdater {
}
logger.warn('Pools.json is outdated, fetch latest from github');
const poolsJson = await this.fetchPools();
const poolsJson = await this.query('https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json');
if (poolsJson === undefined) {
return;
}
await poolsParser.migratePoolsJson(poolsJson);
await this.updateDBSha(githubSha);
logger.notice('PoolsUpdater completed');
@ -52,14 +64,6 @@ class PoolsUpdater {
}
}
/**
* Fetch pools.json from github repo
*/
private async fetchPools(): Promise<object> {
const response = await this.query('/repos/mempool/mining-pools/contents/pools.json');
return JSON.parse(Buffer.from(response['content'], 'base64').toString('utf8'));
}
/**
* Fetch our latest pools.json sha from the db
*/
@ -90,11 +94,13 @@ class PoolsUpdater {
* Fetch our latest pools.json sha from github
*/
private async fetchPoolsSha(): Promise<string | undefined> {
const response = await this.query('/repos/mempool/mining-pools/git/trees/master');
const response = await this.query('https://api.github.com/repos/mempool/mining-pools/git/trees/master');
for (const file of response['tree']) {
if (file['path'] === 'pools.json') {
return file['sha'];
if (response !== undefined) {
for (const file of response['tree']) {
if (file['path'] === 'pools.json') {
return file['sha'];
}
}
}
@ -105,35 +111,45 @@ class PoolsUpdater {
/**
* Http request wrapper
*/
private query(path): Promise<string> {
return new Promise((resolve, reject) => {
const options = {
host: 'api.github.com',
path: path,
method: 'GET',
headers: { 'user-agent': 'node.js' }
private async query(path): Promise<object | undefined> {
type axiosOptions = {
httpsAgent?: https.Agent;
}
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
const axiosOptions: axiosOptions = {};
let retry = 0;
if (config.SOCKS5PROXY.ENABLED) {
const socksOptions: any = {
agentOptions: {
keepAlive: true,
},
hostname: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT
};
logger.debug('Querying: api.github.com' + path);
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
socksOptions.username = config.SOCKS5PROXY.USERNAME;
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
}
const request = https.get(options, (response) => {
const chunks_of_data: any[] = [];
response.on('data', (fragments) => {
chunks_of_data.push(fragments);
});
response.on('end', () => {
resolve(JSON.parse(Buffer.concat(chunks_of_data).toString()));
});
response.on('error', (error) => {
reject(error);
});
});
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
}
request.on('error', (error) => {
logger.err('Github API query failed. Reason: ' + error);
reject(error);
});
});
while(retry < 5) {
try {
const data = await axios.get(path, axiosOptions);
if (data.statusText !== 'OK' || !data.data) {
throw new Error(`Could not fetch data from Github, Error: ${data.status}`);
}
return data.data;
} catch (e) {
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
retry++;
}
await setDelay();
}
return undefined;
}
}

View File

@ -1,4 +1,4 @@
import { BlockExtended } from "../mempool.interfaces";
import { BlockExtended } from '../mempool.interfaces';
export function prepareBlock(block: any): BlockExtended {
return <BlockExtended>{
@ -15,11 +15,13 @@ export function prepareBlock(block: any): BlockExtended {
weight: block.weight,
previousblockhash: block.previousblockhash,
extras: {
coinbaseRaw: block.coinbase_raw ?? block.extras.coinbaseRaw,
coinbaseRaw: block.coinbase_raw ?? block.extras?.coinbaseRaw,
medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee,
feeRange: block.feeRange ?? block.fee_range ?? block?.extras?.feeSpan,
feeRange: block.feeRange ?? block.fee_span,
reward: block.reward ?? block?.extras?.reward,
totalFees: block.totalFees ?? block?.fees ?? block?.extras.totalFees,
totalFees: block.totalFees ?? block?.fees ?? block?.extras?.totalFees,
avgFee: block?.extras?.avgFee ?? block.avg_fee,
avgFeeRate: block?.avgFeeRate ?? block.avg_fee_rate,
pool: block?.extras?.pool ?? (block?.pool_id ? {
id: block.pool_id,
name: block.pool_name,

View File

@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of May 15, 2022.
Signed: ayanamidev

View File

@ -1,18 +1,23 @@
# Docker Installation
This directory contains the Dockerfiles used to build and release the official images and a `docker-compose.yml` for end users to run a Mempool instance with minimal effort.
This directory contains the Dockerfiles used to build and release the official images, as well as a `docker-compose.yml` to configure environment variables and other settings.
You can choose to configure Mempool to run with a basic backend powered by just `bitcoind`, or with `bitcoind` along with an Electrum-compatible server for full functionality.
If you are looking to use these Docker images to deploy your own instance of Mempool, note that they only containerize Mempool's frontend and backend. You will still need to deploy and configure Bitcoin Core and an Electrum Server separately, along with any other utilities specific to your use case (e.g., a reverse proxy, etc). Such configuration is mostly beyond the scope of the Mempool project, so please only proceed if you know what you're doing.
## `bitcoind`-only Configuration
Jump to a section in this doc:
- [Configure with Bitcoin Core Only](#configure-with-bitcoin-core-only)
- [Configure with Bitcoin Core + Electrum Server](#configure-with-bitcoin-core--electrum-server)
- [Further Configuration](#further-configuration)
_Note: address lookups require an Electrum server and will not work with this configuration._
## Configure with Bitcoin Core Only
Make sure `bitcoind` is running and synced.
_Note: address lookups require an Electrum Server and will not work with this configuration. [Add an Electrum Server](#configure-with-bitcoin-core--electrum-server) to your backend for full functionality._
The default Docker configuration assumes you have added RPC credentials for a `mempool` user with a `mempool` password in your `bitcoin.conf` file, like so:
The default Docker configuration assumes you have the following configuration in your `bitcoin.conf` file:
```
txindex=1
server=1
rpcuser=mempool
rpcpassword=mempool
```
@ -31,6 +36,8 @@ If you want to use different credentials, specify them in the `docker-compose.ym
The IP address in the example above refers to Docker's default gateway IP address so that the container can hit the `bitcoind` instance running on the host machine. If your setup is different, update it accordingly.
Make sure `bitcoind` is running and synced.
Now, run:
```bash
@ -39,11 +46,11 @@ docker-compose up
Your Mempool instance should be running at http://localhost. The graphs will be populated as new transactions are detected.
## `bitcoind` + Electrum Server Configuration
## Configure with Bitcoin Core + Electrum Server
First, configure `bitcoind` as specified above, and make sure your Electrum server is running and synced.
First, configure `bitcoind` as specified above, and make sure your Electrum Server is running and synced. See [this FAQ](https://mempool.space/docs/faq#address-lookup-issues) if you need help picking an Electrum Server implementation.
Then, make sure the following variables are set in `docker-compose.yml`, as shown below, so Mempool can connect to your Electrum server:
Then, set the following variables in `docker-compose.yml` so Mempool can connect to your Electrum Server:
```
api:
@ -54,6 +61,11 @@ Then, make sure the following variables are set in `docker-compose.yml`, as show
ELECTRUM_TLS_ENABLED: "false"
```
Eligible values for `MEMPOOL_BACKEND`:
- "electrum" if you're using [romanz/electrs](https://github.com/romanz/electrs) or [cculianu/Fulcrum](https://github.com/cculianu/Fulcrum)
- "esplora" if you're using [Blockstream/electrs](https://github.com/Blockstream/electrs)
- "none" if you're not using any Electrum Server
Of course, if your Docker host IP address is different, update accordingly.
With `bitcoind` and Electrum Server set up, run Mempool with:

View File

@ -1,4 +1,4 @@
FROM node:16.10.0-buster-slim AS builder
FROM node:16.15.0-buster-slim AS builder
ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash}
@ -11,7 +11,7 @@ RUN apt-get install -y build-essential python3 pkg-config
RUN npm install
RUN npm run build
FROM node:16.10.0-buster-slim
FROM node:16.15.0-buster-slim
WORKDIR /backend

View File

@ -1,4 +1,4 @@
FROM node:16.10.0-buster-slim AS builder
FROM node:16.15.0-buster-slim AS builder
ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash}

View File

@ -1,8 +1,30 @@
# mempool-frontend
# Mempool Frontend
## Contributing
You can build and run the Mempool frontend and proxy to the production Mempool backend (for easier frontend development), or you can connect it to your own backend for a full Mempool development instance, custom deployment, etc.
This package is used for the https://mempool.space, https://liquid.network and https://bisq.markets websites - there are npm scripts to setup all three, which effectively change how BASE_MODULE is configured:
Jump to a section in this doc:
- [Quick Setup for Frontend Development](#quick-setup-for-frontend-development)
- [Manual Frontend Setup](#manual-setup)
- [Translations](#translations-transifex-project)
## Quick Setup for Frontend Development
If you want to quickly improve the UI, fix typos, or make other updates that don't require any backend changes, you don't need to set up an entire backend—you can simply run the Mempool frontend locally and proxy to the mempool.space backend.
### 1. Clone Mempool Repository
Get the latest Mempool code:
```
git clone https://github.com/mempool/mempool
cd mempool
```
### 2. Specify Website
The same frontend codebase is used for https://mempool.space, https://liquid.network and https://bisq.markets.
Configure the frontend for the site you want by running the corresponding command:
```
$ npm run config:defaults:mempool
@ -10,18 +32,22 @@ $ npm run config:defaults:liquid
$ npm run config:defaults:bisq
```
Changes that affect the frontend codebase only can be done using the production backend so you don't need to spin up the entire Mempool infrastructure. This is very convenient in case you want to quickly improve the UI, fix typos or implement new features that don't require any backend changes.
### 3. Run the Frontend
Make your changes, install the project dependencies and run the frontend server as follows:
_Node.js 16 and npm 7 are recommended._
Install project dependencies and run the frontend server:
```
$ npm install
$ npm run serve:local-prod
```
The frontend will be available at http://localhost:4200/ and all API requests will be proxied to the production server at https://mempool.space
The frontend will be available at http://localhost:4200/ and all API requests will be proxied to the production server at https://mempool.space.
After making your changes, you can run our end-to-end automation suite and check for possible regressions:
### 4. Test
After making your changes, you can run our end-to-end automation suite and check for possible regressions.
Headless:
@ -37,11 +63,43 @@ $ npm run config:defaults:mempool && npm run cypress:open
This will open the Cypress test runner, where you can select any of the test files to run.
If all tests are green, submit your PR and it will be reviewed by someone on the team as soon as possible.
If all tests are green, submit your PR, and it will be reviewed by someone on the team as soon as possible.
## Manual Setup
Set up the [Mempool backend](../backend/) first, if you haven't already.
### 1. Build the Frontend
_Node.js 16 and npm 7 are recommended._
Build the frontend:
```
cd frontend
npm install # add --prod for production
npm run build
```
### 2. Run the Frontend
#### Development
To run your local Mempool frontend with your local Mempool backend:
```
npm run serve
```
#### Production
The `npm run build` command from step 1 above should have generated a `dist` directory. Put the contents of `dist/` onto your web server.
You will probably want to set up a reverse proxy, TLS, etc. There are sample nginx configuration files in the top level of the repository for reference, but note that support for such tasks is outside the scope of this project.
## Translations: Transifex Project
The mempool frontend strings are localized into 20+ locales:
The Mempool frontend strings are localized into 20+ locales:
https://www.transifex.com/mempool/mempool/dashboard/
### Translators

File diff suppressed because it is too large Load Diff

View File

@ -61,26 +61,26 @@
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^13.3.4",
"@angular/animations": "~13.3.5",
"@angular/cli": "~13.3.4",
"@angular/common": "~13.3.5",
"@angular/compiler": "~13.3.5",
"@angular/core": "~13.3.5",
"@angular/forms": "~13.3.5",
"@angular/localize": "^13.3.5",
"@angular/platform-browser": "~13.3.5",
"@angular/platform-browser-dynamic": "~13.3.5",
"@angular/platform-server": "~13.3.5",
"@angular/router": "~13.3.5",
"@fortawesome/angular-fontawesome": "0.10.1",
"@fortawesome/fontawesome-common-types": "0.3.0",
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-solid-svg-icons": "6.0.0",
"@angular-devkit/build-angular": "~13.3.7",
"@angular/animations": "~13.3.10",
"@angular/cli": "~13.3.7",
"@angular/common": "~13.3.10",
"@angular/compiler": "~13.3.10",
"@angular/core": "~13.3.10",
"@angular/forms": "~13.3.10",
"@angular/localize": "~13.3.10",
"@angular/platform-browser": "~13.3.10",
"@angular/platform-browser-dynamic": "~13.3.10",
"@angular/platform-server": "~13.3.10",
"@angular/router": "~13.3.10",
"@fortawesome/angular-fontawesome": "~0.10.2",
"@fortawesome/fontawesome-common-types": "~6.1.1",
"@fortawesome/fontawesome-svg-core": "~6.1.1",
"@fortawesome/free-solid-svg-icons": "~6.1.1",
"@juggle/resize-observer": "^3.3.1",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@nguniversal/express-engine": "12.1.3",
"@nguniversal/express-engine": "~13.1.1",
"@types/qrcode": "~1.4.2",
"bootstrap": "~4.5.0",
"browserify": "^17.0.0",
@ -93,24 +93,24 @@
"ngx-echarts": "8.0.1",
"ngx-infinite-scroll": "^10.0.1",
"qrcode": "1.5.0",
"rxjs": "^6.6.7",
"rxjs": "~7.5.5",
"tinyify": "^3.0.0",
"tlite": "^0.1.9",
"tslib": "^2.2.0",
"zone.js": "~0.11.4"
"tslib": "~2.4.0",
"zone.js": "~0.11.5"
},
"devDependencies": {
"@angular/compiler-cli": "~13.3.5",
"@angular/language-service": "~13.3.5",
"@nguniversal/builders": "~13.1.0",
"@angular/compiler-cli": "~13.3.10",
"@angular/language-service": "~13.3.10",
"@nguniversal/builders": "~13.1.1",
"@types/express": "^4.17.0",
"@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3",
"@types/jasmine": "~4.0.3",
"@types/jasminewd2": "~2.0.10",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.1",
"codelyzer": "~6.0.2",
"http-proxy-middleware": "^1.0.5",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"jasmine-core": "~4.1.0",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.3.19",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
@ -118,11 +118,11 @@
"karma-jasmine-html-reporter": "^1.5.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.4.4"
"typescript": "~4.6.4"
},
"optionalDependencies": {
"@cypress/schematic": "^1.3.0",
"cypress": "^9.5.2",
"cypress": "^9.6.1",
"cypress-fail-on-console-error": "^2.1.3",
"cypress-wait-until": "^1.7.1",
"mock-socket": "^9.0.3",

File diff suppressed because it is too large Load Diff

View File

@ -2,142 +2,23 @@ import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-bro
import { NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { NgxEchartsModule } from 'ngx-echarts';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './components/app/app.component';
import { StartComponent } from './components/start/start.component';
import { ElectrsApiService } from './services/electrs-api.service';
import { TransactionComponent } from './components/transaction/transaction.component';
import { TransactionsListComponent } from './components/transactions-list/transactions-list.component';
import { StateService } from './services/state.service';
import { BlockComponent } from './components/block/block.component';
import { AddressComponent } from './components/address/address.component';
import { SearchFormComponent } from './components/search-form/search-form.component';
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
import { WebsocketService } from './services/websocket.service';
import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
import { AboutComponent } from './components/about/about.component';
import { TelevisionComponent } from './components/television/television.component';
import { StatisticsComponent } from './components/statistics/statistics.component';
import { FooterComponent } from './components/footer/footer.component';
import { AudioService } from './services/audio.service';
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
import { FeeDistributionGraphComponent } from './components/fee-distribution-graph/fee-distribution-graph.component';
import { IncomingTransactionsGraphComponent } from './components/incoming-transactions-graph/incoming-transactions-graph.component';
import { TimeSpanComponent } from './components/time-span/time-span.component';
import { SeoService } from './services/seo.service';
import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component';
import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component';
import { PoolComponent } from './components/pool/pool.component';
import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component';
import { AssetComponent } from './components/asset/asset.component';
import { AssetsComponent } from './components/assets/assets.component';
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
import { MinerComponent } from './components/miner/miner.component';
import { SharedModule } from './shared/shared.module';
import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { DifficultyComponent } from './components/difficulty/difficulty.component';
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload } from '@fortawesome/free-solid-svg-icons';
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component';
import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { LanguageService } from './services/language.service';
import { SponsorComponent } from './components/sponsor/sponsor.component';
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
import { AssetCirculationComponent } from './components/asset-circulation/asset-circulation.component';
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component';
import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component';
import { MiningStartComponent } from './components/mining-start/mining-start.component';
import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { GraphsComponent } from './components/graphs/graphs.component';
import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments-table/difficulty-adjustments-table.components';
import { BlocksList } from './components/blocks-list/blocks-list.component';
import { RewardStatsComponent } from './components/reward-stats/reward-stats.component';
import { DataCyDirective } from './data-cy.directive';
import { BlockFeesGraphComponent } from './components/block-fees-graph/block-fees-graph.component';
import { BlockRewardsGraphComponent } from './components/block-rewards-graph/block-rewards-graph.component';
import { BlockFeeRatesGraphComponent } from './components/block-fee-rates-graph/block-fee-rates-graph.component';
import { LoadingIndicatorComponent } from './components/loading-indicator/loading-indicator.component';
import { IndexingProgressComponent } from './components/indexing-progress/indexing-progress.component';
import { BlockSizesWeightsGraphComponent } from './components/block-sizes-weights-graph/block-sizes-weights-graph.component';
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
@NgModule({
declarations: [
AppComponent,
AboutComponent,
MasterPageComponent,
BisqMasterPageComponent,
LiquidMasterPageComponent,
TelevisionComponent,
StartComponent,
StatisticsComponent,
TransactionComponent,
BlockComponent,
TransactionsListComponent,
AddressComponent,
LatestBlocksComponent,
SearchFormComponent,
TimeSpanComponent,
AddressLabelsComponent,
FooterComponent,
MempoolBlockComponent,
FeeDistributionGraphComponent,
IncomingTransactionsGraphComponent,
MempoolGraphComponent,
PoolRankingComponent,
PoolComponent,
LbtcPegsGraphComponent,
AssetComponent,
AssetsComponent,
MinerComponent,
StatusViewComponent,
FeesBoxComponent,
DashboardComponent,
DifficultyComponent,
TermsOfServiceComponent,
PrivacyPolicyComponent,
TrademarkPolicyComponent,
SponsorComponent,
PushTransactionComponent,
AssetsNavComponent,
AssetsFeaturedComponent,
AssetGroupComponent,
AssetCirculationComponent,
MiningDashboardComponent,
HashrateChartComponent,
HashrateChartPoolsComponent,
MiningStartComponent,
AmountShortenerPipe,
GraphsComponent,
DifficultyAdjustmentsTable,
BlocksList,
DataCyDirective,
RewardStatsComponent,
BlockFeesGraphComponent,
BlockRewardsGraphComponent,
BlockFeeRatesGraphComponent,
LoadingIndicatorComponent,
IndexingProgressComponent,
BlockSizesWeightsGraphComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
@ -145,14 +26,7 @@ import { BlockSizesWeightsGraphComponent } from './components/block-sizes-weight
AppRoutingModule,
HttpClientModule,
BrowserAnimationsModule,
InfiniteScrollModule,
NgbTypeaheadModule,
NgbModule,
FontAwesomeModule,
SharedModule,
NgxEchartsModule.forRoot({
echarts: () => import('echarts')
})
],
providers: [
ElectrsApiService,
@ -163,45 +37,9 @@ import { BlockSizesWeightsGraphComponent } from './components/block-sizes-weight
StorageService,
LanguageService,
ShortenStringPipe,
CapAddressPipe,
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(library: FaIconLibrary) {
library.addIcons(faInfoCircle);
library.addIcons(faChartArea);
library.addIcons(faTv);
library.addIcons(faTachometerAlt);
library.addIcons(faCubes);
library.addIcons(faHammer);
library.addIcons(faCogs);
library.addIcons(faThList);
library.addIcons(faList);
library.addIcons(faTachometerAlt);
library.addIcons(faDatabase);
library.addIcons(faSearch);
library.addIcons(faLink);
library.addIcons(faBolt);
library.addIcons(faTint);
library.addIcons(faFilter);
library.addIcons(faAngleDown);
library.addIcons(faAngleUp);
library.addIcons(faExchangeAlt);
library.addIcons(faAngleDoubleUp);
library.addIcons(faAngleDoubleDown);
library.addIcons(faChevronDown);
library.addIcons(faFileAlt);
library.addIcons(faRedoAlt);
library.addIcons(faArrowAltCircleRight);
library.addIcons(faExternalLinkAlt);
library.addIcons(faSortUp);
library.addIcons(faCaretUp);
library.addIcons(faCaretDown);
library.addIcons(faAngleRight);
library.addIcons(faAngleLeft);
library.addIcons(faBook);
library.addIcons(faListUl);
library.addIcons(faDownload);
}
}
export class AppModule { }

View File

@ -20,7 +20,7 @@
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" title="{{ block.hash }}">{{ block.hash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.hash"></app-clipboard></td>
</tr>
<tr>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ block.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
@ -83,7 +83,7 @@
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td i18n="block.timestamp">Timestamp</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</table>

View File

@ -89,7 +89,7 @@
</table>
</div>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-all">View all &raquo;</a></div>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
@ -98,7 +98,7 @@
<div class="card-body">
<h5 class="card-title text-center" i18n="Latest Trades header">Latest Trades</h5>
<app-bisq-trades [trades$]="trades$" view="small"></app-bisq-trades>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-all">View all &raquo;</a></div>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>

View File

@ -31,7 +31,7 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">

View File

@ -18,7 +18,7 @@
<th style="width: 20%;" i18n>TXID</th>
<th class="d-none d-md-block" style="width: 100%;" i18n>Type</th>
<th style="width: 20%;" i18n>Amount</th>
<th style="width: 20%;" i18n>Confirmed</th>
<th style="width: 20%;" i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</th>
<th class="d-none d-md-block" i18n>Height</th>
</thead>
<tbody *ngIf="transactions.value; else loadingTmpl">

View File

@ -7,7 +7,6 @@ import { LightweightChartsComponent } from './lightweight-charts/lightweight-cha
import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component';
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
@ -24,6 +23,7 @@ import { BisqAddressComponent } from './bisq-address/bisq-address.component';
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [
@ -46,9 +46,9 @@ import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
BisqMainDashboardComponent,
],
imports: [
CommonModule,
BisqRoutingModule,
SharedModule,
NgbPaginationModule,
FontAwesomeModule,
NgxBootstrapMultiselectModule,
],

View File

@ -20,9 +20,6 @@
<a target="_blank" href="https://twitter.com/mempool">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="twitter" class="svg-inline--fa fa-twitter fa-w-16 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>
</a>
<a target="_blank" href="https://keybase.io/team/mempool">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="keybase" class="svg-inline--fa fa-keybase fa-w-14 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M286.17 419a18 18 0 1 0 18 18 18 18 0 0 0-18-18zm111.92-147.6c-9.5-14.62-39.37-52.45-87.26-73.71q-9.1-4.06-18.38-7.27a78.43 78.43 0 0 0-47.88-104.13c-12.41-4.1-23.33-6-32.41-5.77-.6-2-1.89-11 9.4-35L198.66 32l-5.48 7.56c-8.69 12.06-16.92 23.55-24.34 34.89a51 51 0 0 0-8.29-1.25c-41.53-2.45-39-2.33-41.06-2.33-50.61 0-50.75 52.12-50.75 45.88l-2.36 36.68c-1.61 27 19.75 50.21 47.63 51.85l8.93.54a214 214 0 0 0-46.29 35.54C14 304.66 14 374 14 429.77v33.64l23.32-29.8a148.6 148.6 0 0 0 14.56 37.56c5.78 10.13 14.87 9.45 19.64 7.33 4.21-1.87 10-6.92 3.75-20.11a178.29 178.29 0 0 1-15.76-53.13l46.82-59.83-24.66 74.11c58.23-42.4 157.38-61.76 236.25-38.59 34.2 10.05 67.45.69 84.74-23.84.72-1 1.2-2.16 1.85-3.22a156.09 156.09 0 0 1 2.8 28.43c0 23.3-3.69 52.93-14.88 81.64-2.52 6.46 1.76 14.5 8.6 15.74 7.42 1.57 15.33-3.1 18.37-11.15C429 443 434 414 434 382.32c0-38.58-13-77.46-35.91-110.92zM142.37 128.58l-15.7-.93-1.39 21.79 13.13.78a93 93 0 0 0 .32 19.57l-22.38-1.34a12.28 12.28 0 0 1-11.76-12.79L107 119c1-12.17 13.87-11.27 13.26-11.32l29.11 1.73a144.35 144.35 0 0 0-7 19.17zm148.42 172.18a10.51 10.51 0 0 1-14.35-1.39l-9.68-11.49-34.42 27a8.09 8.09 0 0 1-11.13-1.08l-15.78-18.64a7.38 7.38 0 0 1 1.34-10.34l34.57-27.18-14.14-16.74-17.09 13.45a7.75 7.75 0 0 1-10.59-1s-3.72-4.42-3.8-4.53a7.38 7.38 0 0 1 1.37-10.34L214 225.19s-18.51-22-18.6-22.14a9.56 9.56 0 0 1 1.74-13.42 10.38 10.38 0 0 1 14.3 1.37l81.09 96.32a9.58 9.58 0 0 1-1.74 13.44zM187.44 419a18 18 0 1 0 18 18 18 18 0 0 0-18-18z"></path></svg>
</a>
<a target="_blank" href="https://matrix.to/#/#mempool:bitcoin.kyoto">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="matrix" class="svg-inline--fa fa-matrix fa-w-16 fa-4x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1536 1792"><path fill="currentColor" d="M40.467 163.152v1465.696H145.92V1664H0V128h145.92v35.152zm450.757 464.64v74.14h2.069c19.79-28.356 43.717-50.215 71.483-65.575 27.765-15.656 59.963-23.336 96-23.336 34.56 0 66.165 6.795 94.818 20.086 28.652 13.293 50.216 37.22 65.28 70.893 16.246-23.926 38.4-45.194 66.166-63.507 27.766-18.314 60.848-27.472 98.954-27.472 28.948 0 55.828 3.545 80.64 10.635 24.812 7.088 45.785 18.314 63.508 33.968 17.722 15.656 31.31 35.742 41.354 60.85 9.747 25.107 14.768 55.236 14.768 90.683v366.573h-150.35V865.28c0-18.314-.59-35.741-2.068-51.987-1.476-16.247-5.316-30.426-11.52-42.24-6.499-12.112-15.656-21.563-28.062-28.653-12.405-7.088-29.242-10.634-50.214-10.634-21.268 0-38.4 4.135-51.397 12.112-12.997 8.27-23.336 18.608-30.72 31.901-7.386 12.997-12.407 27.765-14.77 44.602-2.363 16.542-3.84 33.379-3.84 50.216v305.133H692.971v-307.2c0-16.247-.294-32.197-1.18-48.149-.591-15.95-3.84-30.424-9.157-44.011-5.317-13.293-14.178-24.223-26.585-32.197-12.406-7.976-30.425-12.112-54.646-12.112-7.088 0-16.542 1.478-28.062 4.726-11.52 3.25-23.04 9.157-33.968 18.02-10.93 8.86-20.383 21.563-28.063 38.103-7.68 16.543-11.52 38.4-11.52 65.28v317.834H349.44V627.792zm1004.309 1001.056V163.152H1390.08V128H1536v1536h-145.92v-35.152z"/></svg>
</a>
@ -250,6 +247,10 @@
<img class="image" src="/resources/profile/marina.svg" />
<span>Marina</span>
</a>
<a href="https://github.com/bitcoin-wallet/bitcoin-wallet/" target="_blank" title="Bitcoin Wallet (Schildbach)">
<img class="image" src="/resources/profile/schildbach.svg" />
<span>Schildbach</span>
</a>
</div>
</div>

View File

@ -118,7 +118,7 @@ export class AddressLabelsComponent implements OnInit {
}
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
this.label = `multisig ${m} of ${n}`;
this.label = $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:`
}
handleVout() {

View File

@ -66,9 +66,14 @@
<div class="text-center">
<ng-template [ngIf]="isLoadingTransactions">
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
<span class="skeleton-loader"></span>
</div>
<ng-container *ngIf="addressLoadingStatus$ as addressLoadingStatus">
<div class="header-bg box" style="padding: 12px; margin-bottom: 10px;">
<div class="progress progress-dark">
<div class="progress-bar progress-light" role="progressbar" [ngStyle]="{'width': addressLoadingStatus + '%' }"></div>
</div>
</div>
</ng-container>
<div class="header-bg box">
<div class="row" style="height: 107px;">
@ -81,13 +86,6 @@
</div>
</div>
<ng-container *ngIf="addressLoadingStatus$ | async as addressLoadingStatus">
<br>
<div class="progress progress-dark">
<div class="progress-bar progress-darklight" role="progressbar" [ngStyle]="{'width': addressLoadingStatus + '%' }"></div>
</div>
</ng-container>
</ng-template>
<ng-template [ngIf]="retryLoadMore">
@ -155,3 +153,9 @@
<ng-template #confidentialTd>
<td i18n="shared.confidential">Confidential</td>
</ng-template>
<ng-template #headerLoader>
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
<span class="skeleton-loader"></span>
</div>
</ng-template>

View File

@ -1,11 +1,13 @@
<div *ngIf="group$ | async as group; else loading">
<div *ngIf="group$ | async as group; else loading;">
<div class="main-title">
<h2>{{ group.group.name }}</h2>
<div class="sub-title" i18n>Group of {{ group.group.assets.length | number }} assets</div>
</div>
<ng-container *ngTemplateOutlet="title; context: group.group"></ng-container>
<ng-template #title>
<div class="main-title">
<h2>{{ group.name }}</h2>
<div class="sub-title" i18n>Group of {{ group.assets.length | number }} assets</div>
</div>
</ng-template>
<div class="clearfix"></div>

View File

@ -19,7 +19,7 @@
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/signet'" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><img src="./resources/signet-logo.png" style="width: 30px;" class="mr-1"> Signet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</a>
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<button ngbDropdownItem class="mainnet active" routerLink="/"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</button>
<a ngbDropdownItem class="mainnet active" routerLink="/"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1"> Liquid</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet"><img src="./resources/liquidtestnet-logo.png" style="width: 30px;" class="mr-1"> Liquid Testnet</a>
</div>

View File

@ -2,41 +2,40 @@
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.block-fee-rates">Block fee rates</span>
<span i18n="mining.block-fee-rates">Block Fee Rates</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 1">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
<input ngbButton type="radio" [value]="'24h'" fragment="24h"> 24h
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 3">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432">
<input ngbButton type="radio" [value]="'3d'" fragment="3d"> 3D
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 7">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008">
<input ngbButton type="radio" [value]="'1w'" fragment="1w"> 1W
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 30">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
<input ngbButton type="radio" [value]="'1m'" fragment="1m"> 1M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 90">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
<input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 180">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
<input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 365">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
<input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 730">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
<input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 1095">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay > 1095">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680">
<input ngbButton type="radio" [value]="'all'" fragment="all"> ALL
</label>
</div>

View File

@ -62,7 +62,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
}
ngOnInit(): void {
this.seoService.setTitle($localize`:@@mining.block-fee-rates:Block Fee Rates`);
this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@ -76,7 +76,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
this.isLoading = true;
return this.apiService.getHistoricalBlockFeeRates$(timespan)
.pipe(
tap((data: any) => {
tap((response) => {
// Group by percentile
const seriesData = {
'Min': [],
@ -87,15 +87,15 @@ export class BlockFeeRatesGraphComponent implements OnInit {
'90th': [],
'Max': []
};
for (const rate of data.blockFeeRates) {
for (const rate of response.body) {
const timestamp = rate.timestamp * 1000;
seriesData['Min'].push([timestamp, rate.avg_fee_0, rate.avg_height]);
seriesData['10th'].push([timestamp, rate.avg_fee_10, rate.avg_height]);
seriesData['25th'].push([timestamp, rate.avg_fee_25, rate.avg_height]);
seriesData['Median'].push([timestamp, rate.avg_fee_50, rate.avg_height]);
seriesData['75th'].push([timestamp, rate.avg_fee_75, rate.avg_height]);
seriesData['90th'].push([timestamp, rate.avg_fee_90, rate.avg_height]);
seriesData['Max'].push([timestamp, rate.avg_fee_100, rate.avg_height]);
seriesData['Min'].push([timestamp, rate.avgFee_0, rate.avgHeight]);
seriesData['10th'].push([timestamp, rate.avgFee_10, rate.avgHeight]);
seriesData['25th'].push([timestamp, rate.avgFee_25, rate.avgHeight]);
seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]);
seriesData['75th'].push([timestamp, rate.avgFee_75, rate.avgHeight]);
seriesData['90th'].push([timestamp, rate.avgFee_90, rate.avgHeight]);
seriesData['Max'].push([timestamp, rate.avgFee_100, rate.avgHeight]);
}
// Prepare chart
@ -130,13 +130,9 @@ export class BlockFeeRatesGraphComponent implements OnInit {
});
this.isLoading = false;
}),
map((data: any) => {
const availableTimespanDay = (
(new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp)
) / 3600 / 24;
map((response) => {
return {
availableTimespanDay: availableTimespanDay,
blockCount: parseInt(response.headers.get('x-total-count'), 10),
};
}),
);

View File

@ -2,11 +2,10 @@
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.block-fees">Block fees</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<span i18n="mining.block-fees">Block Fees</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">

View File

@ -55,7 +55,7 @@ export class BlockFeesGraphComponent implements OnInit {
}
ngOnInit(): void {
this.seoService.setTitle($localize`:@@mining.block-fees:Block Fees`);
this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Block Fees`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@ -71,7 +71,7 @@ export class BlockFeesGraphComponent implements OnInit {
.pipe(
tap((response) => {
this.prepareChartOptions({
blockFees: response.body.map(val => [val.timestamp * 1000, val.avg_fees / 100000000]),
blockFees: response.body.map(val => [val.timestamp * 1000, val.avgFees / 100000000]),
});
this.isLoading = false;
}),
@ -157,7 +157,7 @@ export class BlockFeesGraphComponent implements OnInit {
series: [
{
zlevel: 0,
name: 'Fees',
name: $localize`:@@c20172223f84462032664d717d739297e5a9e2fe:Fees`,
showSymbol: false,
symbol: 'none',
data: data.blockFees,

View File

@ -3,11 +3,10 @@
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.block-rewards">Block rewards</span>
<span i18n="mining.block-rewards">Block Rewards</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">

View File

@ -53,7 +53,7 @@ export class BlockRewardsGraphComponent implements OnInit {
}
ngOnInit(): void {
this.seoService.setTitle($localize`:@@mining.block-reward:Block Reward`);
this.seoService.setTitle($localize`:@@8ba8fe810458280a83df7fdf4c614dfc1a826445:Block Rewards`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@ -69,7 +69,7 @@ export class BlockRewardsGraphComponent implements OnInit {
.pipe(
tap((response) => {
this.prepareChartOptions({
blockRewards: response.body.map(val => [val.timestamp * 1000, val.avg_rewards / 100000000]),
blockRewards: response.body.map(val => [val.timestamp * 1000, val.avgRewards / 100000000]),
});
this.isLoading = false;
}),
@ -157,7 +157,7 @@ export class BlockRewardsGraphComponent implements OnInit {
series: [
{
zlevel: 0,
name: 'Reward',
name: $localize`:@@12f86e6747a5ad39e62d3480ddc472b1aeab5b76:Reward`,
showSymbol: false,
symbol: 'none',
data: data.blockRewards,

View File

@ -1,7 +1,7 @@
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.block-size-weight">Block Sizes and Weights</span>
<span i18n="mining.block-sizes-weights">Block Sizes and Weights</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>

View File

@ -62,7 +62,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
ngOnInit(): void {
let firstRun = true;
this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Weight`);
this.seoService.setTitle($localize`:@@56fa1cd221491b6478998679cba2dc8d55ba330d:Block Sizes and Weights`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@ -83,8 +83,8 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
tap((response) => {
const data = response.body;
this.prepareChartOptions({
sizes: data.sizes.map(val => [val.timestamp * 1000, val.avg_size / 1000000, val.avg_height]),
weights: data.weights.map(val => [val.timestamp * 1000, val.avg_weight / 1000000, val.avg_height]),
sizes: data.sizes.map(val => [val.timestamp * 1000, val.avgSize / 1000000, val.avgHeight]),
weights: data.weights.map(val => [val.timestamp * 1000, val.avgWeight / 1000000, val.avgHeight]),
});
this.isLoading = false;
}),
@ -178,7 +178,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
padding: 10,
data: [
{
name: 'Size',
name: $localize`:@@7faaaa08f56427999f3be41df1093ce4089bbd75:Size`,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
@ -186,7 +186,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
icon: 'roundRect',
},
{
name: 'Weight',
name: $localize`:@@919f2fd60a898850c24b1584362bbf18a4628bcb:Weight`,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
@ -224,7 +224,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
series: data.sizes.length === 0 ? [] : [
{
zlevel: 1,
name: 'Size',
name: $localize`:@@7faaaa08f56427999f3be41df1093ce4089bbd75:Size`,
showSymbol: false,
symbol: 'none',
data: data.sizes,
@ -255,7 +255,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
{
zlevel: 1,
yAxisIndex: 0,
name: 'Weight',
name: $localize`:@@919f2fd60a898850c24b1584362bbf18a4628bcb:Weight`,
showSymbol: false,
symbol: 'none',
data: data.weights,

View File

@ -81,15 +81,26 @@
<ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
<tr>
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
<td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees"><app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="fees * 100000000" digitsInfo="1.0-0"></app-fiat></span></td>
<td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees">
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
<span class="fiat">
<app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat>
</span>
</td>
<ng-template #liquidTotalFees>
<td><app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount>&nbsp; <app-fiat [value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat></td>
<td>
<app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount>&nbsp; <app-fiat
[value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat>
</td>
</ng-template>
</tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
<td>
<app-amount [satoshis]="(blockSubsidy + fees) * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat></span>
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
<span class="fiat">
<app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat>
</span>
</td>
</tr>
</ng-template>
@ -103,9 +114,20 @@
<td><span class="skeleton-loader"></span></td>
</tr>
</ng-template>
<tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="block.miner">Miner</td>
<td><app-miner [coinbaseTransaction]="coinbaseTx"></app-miner></td>
<td *ngIf="stateService.env.MINING_DASHBOARD">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block.extras.pool.name }}
</a>
</td>
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
<span placement="bottom" class="badge"
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block.extras.pool.name }}
</span>
</td>
</tr>
</tbody>
</table>
@ -173,14 +195,29 @@
</div>
<div class="clearfix"></div>
<app-transactions-list [transactions]="transactions"></app-transactions-list>
<app-transactions-list [transactions]="transactions" [paginated]="true"></app-transactions-list>
<ng-template [ngIf]="isLoadingTransactions">
<ng-template [ngIf]="transactionsError">
<div class="text-center">
<br>
<span i18n="error.general-loading-data">Error loading data.</span>
<br><br>
<i>{{ transactionsError.status }}: {{ transactionsError.error }}</i>
<br>
<br>
</div>
</ng-template>
<ng-template [ngIf]="isLoadingTransactions && !transactionsError">
<div class="text-center mb-4" class="tx-skeleton">
<div class="header-bg box">
<span class="skeleton-loader"></span>
</div>
<ng-container *ngIf="(txsLoadingStatus$ | async) as txsLoadingStatus; else headerLoader">
<div class="header-bg box">
<div class="progress progress-dark" style="margin: 4px; height: 14px;">
<div class="progress-bar progress-light" role="progressbar" [ngStyle]="{'width': txsLoadingStatus + '%' }"></div>
</div>
</div>
</ng-container>
<div class="header-bg box">
<div class="row">
@ -193,14 +230,6 @@
</div>
</div>
</div>
<ng-container *ngIf="(txsLoadingStatus$ | async) as txsLoadingStatus">
<br>
<div class="progress progress-dark">
<div class="progress-bar progress-darklight" role="progressbar" [ngStyle]="{'width': txsLoadingStatus + '%' }"></div>
</div>
</ng-container>
</div>
</ng-template>
<ngb-pagination class="pagination-container float-right" [collectionSize]="block.tx_count" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page, blockTxTitle)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
@ -253,9 +282,15 @@
<ng-template [ngIf]="error">
<div class="text-center">
<span i18n="block.error.loading-block-data">Error loading block data.</span>
<span i18n="error.general-loading-data">Error loading data.</span>
<br><br>
<i>{{ error.error }}</i>
<i>{{ error.code }}: {{ error.error }}</i>
</div>
</ng-template>
<ng-template #headerLoader>
<div class="header-bg box">
<span class="skeleton-loader"></span>
</div>
</ng-template>

View File

@ -10,6 +10,7 @@ import { SeoService } from 'src/app/services/seo.service';
import { WebsocketService } from 'src/app/services/websocket.service';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
import { ApiService } from 'src/app/services/api.service';
@Component({
selector: 'app-block',
@ -31,13 +32,13 @@ export class BlockComponent implements OnInit, OnDestroy {
blockSubsidy: number;
fees: number;
paginationMaxSize: number;
coinbaseTx: Transaction;
page = 1;
itemsPerPage: number;
txsLoadingStatus$: Observable<number>;
showDetails = false;
showPreviousBlocklink = true;
showNextBlocklink = true;
transactionsError: any = null;
subscription: Subscription;
keyNavigationSubscription: Subscription;
@ -50,10 +51,11 @@ export class BlockComponent implements OnInit, OnDestroy {
private location: Location,
private router: Router,
private electrsApiService: ElectrsApiService,
private stateService: StateService,
public stateService: StateService,
private seoService: SeoService,
private websocketService: WebsocketService,
private relativeUrlPipe: RelativeUrlPipe,
private apiService: ApiService
) { }
ngOnInit() {
@ -88,7 +90,6 @@ export class BlockComponent implements OnInit, OnDestroy {
const blockHash: string = params.get('id') || '';
this.block = undefined;
this.page = 1;
this.coinbaseTx = undefined;
this.error = undefined;
this.fees = undefined;
this.stateService.markBlock$.next({});
@ -124,7 +125,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.location.replaceState(
this.router.createUrlTree([(this.network ? '/' + this.network : '') + '/block/', hash]).toString()
);
return this.electrsApiService.getBlock$(hash);
return this.apiService.getBlock$(hash);
})
);
}
@ -134,7 +135,7 @@ export class BlockComponent implements OnInit, OnDestroy {
return of(blockInCache);
}
return this.electrsApiService.getBlock$(blockHash);
return this.apiService.getBlock$(blockHash);
}
}),
tap((block: BlockExtended) => {
@ -145,7 +146,6 @@ export class BlockComponent implements OnInit, OnDestroy {
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
this.isLoadingBlock = false;
this.coinbaseTx = block?.extras?.coinbaseTx;
this.setBlockSubsidy();
if (block?.extras?.reward !== undefined) {
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
@ -153,12 +153,13 @@ export class BlockComponent implements OnInit, OnDestroy {
this.stateService.markBlock$.next({ blockHeight: this.blockHeight });
this.isLoadingTransactions = true;
this.transactions = null;
this.transactionsError = null;
}),
debounceTime(300),
switchMap((block) => this.electrsApiService.getBlockTransactions$(block.id)
.pipe(
catchError((err) => {
console.log(err);
this.transactionsError = err;
return of([]);
}))
),
@ -167,9 +168,6 @@ export class BlockComponent implements OnInit, OnDestroy {
if (this.fees === undefined && transactions[0]) {
this.fees = transactions[0].vout.reduce((acc: number, curr: Vout) => acc + curr.value, 0) / 100000000 - this.blockSubsidy;
}
if (!this.coinbaseTx && transactions[0]) {
this.coinbaseTx = transactions[0];
}
this.transactions = transactions;
this.isLoadingTransactions = false;
},
@ -212,22 +210,26 @@ export class BlockComponent implements OnInit, OnDestroy {
this.queryParamsSubscription.unsubscribe();
}
// TODO - Refactor this.fees/this.reward for liquid because it is not
// used anymore on Bitcoin networks (we use block.extras directly)
setBlockSubsidy() {
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
this.blockSubsidy = 0;
return;
}
const halvings = Math.floor(this.block.height / 210000);
this.blockSubsidy = 50 * 2 ** -halvings;
this.blockSubsidy = 0;
}
pageChange(page: number, target: HTMLElement) {
const start = (page - 1) * this.itemsPerPage;
this.isLoadingTransactions = true;
this.transactions = null;
this.transactionsError = null;
target.scrollIntoView(); // works for chrome
this.electrsApiService.getBlockTransactions$(this.block.id, start)
.pipe(
catchError((err) => {
this.transactionsError = err;
return of([]);
})
)
.subscribe((transactions) => {
this.transactions = transactions;
this.isLoadingTransactions = false;

View File

@ -1,35 +1,34 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div class="container-xl" [class]="widget ? 'widget' : 'full-height'">
<h1 *ngIf="!widget" class="float-left" i18n="latest-blocks.blocks">Blocks</h1>
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget, 'legacy': !indexingAvailable}">
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Blocks</h1>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div>
<div style="min-height: 295px">
<table class="table table-borderless">
<thead>
<th class="height" [class]="widget ? 'widget' : ''" i18n="latest-blocks.height">Height</th>
<th class="pool text-left" [class]="widget ? 'widget' : ''" i18n="mining.pool-name">
Pool</th>
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget">Timestamp</th>
<th class="mined" i18n="latest-blocks.mined" *ngIf="!widget">Mined</th>
<th class="reward text-right" i18n="latest-blocks.reward" [class]="widget ? 'widget' : ''">
Reward</th>
<th class="fees text-right" i18n="latest-blocks.fees" *ngIf="!widget">Fees</th>
<th class="txs text-right" i18n="latest-blocks.transactions" [class]="widget ? 'widget' : ''">Txs</th>
<th class="size" i18n="latest-blocks.size" *ngIf="!widget">Size</th>
<th class="height text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}" i18n="latest-blocks.height">Height</th>
<th *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}" i18n="mining.pool-name">Pool</th>
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Timestamp</th>
<th class="mined" i18n="latest-blocks.mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Mined</th>
<th *ngIf="indexingAvailable" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">Reward</th>
<th *ngIf="indexingAvailable && !widget" class="fees text-right" i18n="latest-blocks.fees" [class]="indexingAvailable ? '' : 'legacy'">Fees</th>
<th *ngIf="indexingAvailable" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">TXs</th>
<th *ngIf="!indexingAvailable" class="txs text-right" i18n="dashboard.txs" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">Transactions</th>
<th class="size" i18n="latest-blocks.size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Size</th>
</thead>
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
<td [class]="widget ? 'widget' : ''">
<a [routerLink]="['/block' | relativeUrl, block.height]">{{ block.height
}}</a>
<td class="text-left" [class]="widget ? 'widget' : ''">
<a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
</td>
<td class="pool text-left" [class]="widget ? 'widget' : ''">
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<div class="tooltip-custom">
<a class="clear-link" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]">
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
onError="this.src = './resources/mining-pools/default.svg'">
onError="this.src = './resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
<span class="pool-name">{{ block.extras.pool.name }}</span>
</a>
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
@ -38,19 +37,19 @@
<td class="timestamp" *ngIf="!widget">
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
</td>
<td class="mined" *ngIf="!widget">
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
</td>
<td class="reward text-right" [class]="widget ? 'widget' : ''">
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2"></app-amount>
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
</td>
<td class="fees text-right" *ngIf="!widget">
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2"></app-amount>
<td *ngIf="indexingAvailable && !widget" class="fees text-right" [class]="indexingAvailable ? '' : 'legacy'">
<app-amount [satoshis]="block.extras.totalFees" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
</td>
<td class="txs text-right" [class]="widget ? 'widget' : ''">
<td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
{{ block.tx_count | number }}
</td>
<td class="size" *ngIf="!widget">
<td class="size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<div class="progress">
<div class="progress-bar progress-mempool" role="progressbar"
[ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div>
@ -62,29 +61,29 @@
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="height" [class]="widget ? 'widget' : ''">
<td class="height text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="pool text-left" [class]="widget ? 'widget' : ''">
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<img width="1" height="25" style="opacity: 0">
<span class="skeleton-loader" style="max-width: 125px"></span>
</td>
<td class="timestamp" *ngIf="!widget">
<td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 150px"></span>
</td>
<td class="mined" *ngIf="!widget">
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 125px"></span>
</td>
<td class="reward text-right" [class]="widget ? 'widget' : ''">
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="fees text-right" *ngIf="!widget">
<td *ngIf="indexingAvailable && !widget" class="fees text-right" [class]="indexingAvailable ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="txs text-right" [class]="widget ? 'widget' : ''">
<td class="txs text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="size" *ngIf="!widget">
<td class="size" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<span class="skeleton-loader"></span>
</td>
</tr>

View File

@ -1,3 +1,9 @@
.spinner-border {
height: 25px;
width: 25px;
margin-top: 13px;
}
.container-xl {
max-width: 1400px;
padding-bottom: 100px;
@ -6,18 +12,18 @@
padding-left: 0px;
padding-bottom: 0px;
}
.container-xl.legacy {
max-width: 1140px;
}
.container {
max-width: 100%;
}
td {
padding-top: 0.7rem !important;
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.7rem !important;
@media (max-width: 376px) {
padding-top: 0.73rem !important;
padding-bottom: 0.73rem !important;
}
}
.clear-link {
@ -41,7 +47,7 @@ td {
}
.pool.widget {
width: 40%;
padding-left: 30px;
padding-left: 24px;
@media (max-width: 376px) {
width: 60%;
}
@ -56,11 +62,14 @@ td {
width: 10%;
}
.height.widget {
width: 20%;
width: 15%;
@media (max-width: 576px) {
width: 10%;
}
}
.height.legacy {
width: 15%;
}
.timestamp {
width: 18%;
@ -68,6 +77,9 @@ td {
display: none;
}
}
.timestamp.legacy {
width: 20%;
}
.mined {
width: 13%;
@ -75,6 +87,12 @@ td {
display: none;
}
}
.mined.legacy {
width: 15%;
@media (max-width: 576px) {
display: table-cell;
}
}
.txs {
padding-right: 40px;
@ -91,6 +109,10 @@ td {
display: none;
}
}
.txs.legacy {
padding-right: 80px;
width: 10%;
}
.fees {
width: 10%;
@ -129,6 +151,12 @@ td {
display: none;
}
}
.size.legacy {
width: 20%;
@media (max-width: 576px) {
display: table-cell;
}
}
/* Tooltip text */
.tooltip-custom {

View File

@ -17,6 +17,7 @@ export class BlocksList implements OnInit {
blocks$: Observable<any[]> = undefined;
indexingAvailable = false;
isLoading = true;
fromBlockHeight = undefined;
paginationMaxSize: number;
@ -35,11 +36,14 @@ export class BlocksList implements OnInit {
}
ngOnInit(): void {
this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' &&
this.stateService.env.MINING_DASHBOARD === true);
if (!this.widget) {
this.websocketService.want(['blocks']);
}
this.skeletonLines = this.widget === true ? [...Array(5).keys()] : [...Array(15).keys()];
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
this.blocks$ = combineLatest([
@ -55,17 +59,19 @@ export class BlocksList implements OnInit {
this.isLoading = false;
}),
map(blocks => {
for (const block of blocks) {
// @ts-ignore: Need to add an extra field for the template
block.extras.pool.logo = `./resources/mining-pools/` +
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
if (this.indexingAvailable) {
for (const block of blocks) {
// @ts-ignore: Need to add an extra field for the template
block.extras.pool.logo = `./resources/mining-pools/` +
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
}
}
if (this.widget) {
return blocks.slice(0, 5);
return blocks.slice(0, 6);
}
return blocks;
}),
retryWhen(errors => errors.pipe(delayWhen(() => timer(1000))))
retryWhen(errors => errors.pipe(delayWhen(() => timer(10000))))
)
})
),
@ -81,11 +87,13 @@ export class BlocksList implements OnInit {
return blocks[0];
}
this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1;
// @ts-ignore: Need to add an extra field for the template
blocks[1][0].extras.pool.logo = `./resources/mining-pools/` +
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
if (this.stateService.env.MINING_DASHBOARD) {
// @ts-ignore: Need to add an extra field for the template
blocks[1][0].extras.pool.logo = `./resources/mining-pools/` +
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
}
acc.unshift(blocks[1][0]);
acc = acc.slice(0, this.widget ? 5 : 15);
acc = acc.slice(0, this.widget ? 6 : 15);
return acc;
}, [])
);

View File

@ -1,4 +1,4 @@
<div style="min-height: 295px">
<div style="min-height: 335px">
<table class="table latest-adjustments">
<thead>
<tr>
@ -22,7 +22,7 @@
</tr>
</tbody>
<tbody *ngIf="isLoading">
<tr *ngFor="let item of [1,2,3,4,5]">
<tr *ngFor="let item of [1,2,3,4,5,6]">
<td class="d-none d-md-block w-75"><span class="skeleton-loader"></span></td>
<td class="text-left"><span class="skeleton-loader w-75"></span></td>
<td class="text-right"><span class="skeleton-loader w-75"></span></td>

View File

@ -2,10 +2,15 @@
width: 100%;
text-align: left;
table-layout:fixed;
tr, td, th {
tr, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.7rem !important;
}
td {
border: 0px;
padding-top: 0.71rem !important;
padding-bottom: 0.75rem !important;
width: 25%;
@media (max-width: 376px) {
padding: 0.85rem;

View File

@ -34,10 +34,6 @@ export class DifficultyAdjustmentsTable implements OnInit {
.pipe(
map((response) => {
const data = response.body;
const availableTimespanDay = (
(new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp)
) / 3600 / 24;
const tableData = [];
for (let i = data.difficulty.length - 1; i > 0; --i) {
const selectedPowerOfTen: any = selectPowerOfTen(data.difficulty[i].difficulty);
@ -53,8 +49,7 @@ export class DifficultyAdjustmentsTable implements OnInit {
this.isLoading = false;
return {
availableTimespanDay: availableTimespanDay,
difficulty: tableData.slice(0, 5),
difficulty: tableData.slice(0, 6),
};
}),
);

View File

@ -1,37 +1,23 @@
<div class="mb-3 d-flex menu" style="padding: 0px 35px;">
<div *ngIf="stateService.env.MINING_DASHBOARD" class="mb-3 d-flex menu" style="padding: 0px 35px;">
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1"
[routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a>
<div ngbDropdown *ngIf="stateService.env.MINING_DASHBOARD" class="w-50">
<div ngbDropdown class="w-50">
<button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="mining">Mining</button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]"
i18n="mining.pools">
Pools ranking
</a>
i18n="mining.pools">Pools Ranking</a>
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]"
i18n="mining.pools-dominance">
Pools dominance
</a>
i18n="mining.pools-dominance">Pools Dominance</a>
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="mining.hashrate-difficulty">
Hashrate & Difficulty
</a>
[routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="mining.hashrate-difficulty">Hashrate & Difficulty</a>
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" i18n="mining.block-fee-rates">
Block Fee Rates
</a>
[routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" i18n="mining.block-fee-rates">Block Fee Rates</a>
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-fees' | relativeUrl]" i18n="mining.block-fees">
Block Fees
</a>
[routerLink]="['/graphs/mining/block-fees' | relativeUrl]" i18n="mining.block-fees">Block Fees</a>
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" i18n="mining.block-rewards">
Block Rewards
</a>
[routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" i18n="mining.block-rewards">Block Rewards</a>
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">
Block Sizes and Weights
</a>
[routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a>
</div>
</div>
</div>

View File

@ -11,7 +11,7 @@
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="master-page.blocks">Difficulty</h5>
<h5 class="card-title" i18n="block.difficulty">Difficulty</h5>
<p class="card-text">
{{ hashrates.currentDifficulty | amountShortener }}
</p>
@ -64,13 +64,13 @@
<ng-template #loadingStats>
<div class="pool-distribution">
<div class="item">
<h5 class="card-title" i18n="mining.miners-luck">Hashrate</h5>
<h5 class="card-title" i18n="mining.hashrate">Hashrate</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="master-page.blocks">Difficulty</h5>
<h5 class="card-title" i18n="block.difficulty">Difficulty</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>

View File

@ -52,7 +52,7 @@
.chart-widget {
width: 100%;
height: 100%;
max-height: 270px;
height: 240px;
}
.formRadioGroup {

View File

@ -64,7 +64,7 @@ export class HashrateChartComponent implements OnInit {
if (this.widget) {
this.miningWindowPreference = '1y';
} else {
this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Difficulty`);
this.seoService.setTitle($localize`:@@3510fc6daa1d975f331e3a717bdf1a34efa06dff:Hashrate & Difficulty`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('1m');
}
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
@ -223,7 +223,7 @@ export class HashrateChartComponent implements OnInit {
legend: (this.widget || data.hashrates.length === 0) ? undefined : {
data: [
{
name: 'Hashrate',
name: $localize`:@@79a9dc5b1caca3cbeb1733a19515edacc5fc7920:Hashrate`,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
@ -234,9 +234,9 @@ export class HashrateChartComponent implements OnInit {
},
},
{
name: 'Difficulty',
name: $localize`:@@25148835d92465353fc5fe8897c27d5369978e5a:Difficulty`,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
textStyle: {
color: 'white',
},
icon: 'roundRect',
@ -290,7 +290,7 @@ export class HashrateChartComponent implements OnInit {
series: data.hashrates.length === 0 ? [] : [
{
zlevel: 0,
name: 'Hashrate',
name: $localize`:@@79a9dc5b1caca3cbeb1733a19515edacc5fc7920:Hashrate`,
showSymbol: false,
symbol: 'none',
data: data.hashrates,
@ -302,7 +302,7 @@ export class HashrateChartComponent implements OnInit {
{
zlevel: 1,
yAxisIndex: 1,
name: 'Difficulty',
name: $localize`:@@25148835d92465353fc5fe8897c27d5369978e5a:Difficulty`,
showSymbol: false,
symbol: 'none',
data: data.difficulty,

View File

@ -3,11 +3,10 @@
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.pools-dominance">Mining pools dominance</span>
<span i18n="mining.pools-dominance">Pools Dominance</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">

View File

@ -1,57 +0,0 @@
<div class="container-xl">
<h1 class="float-left" i18n="latest-blocks.blocks">Blocks</h1>
<br>
<div class="clearfix"></div>
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
<thead>
<th style="width: 15%;" i18n="latest-blocks.height">Height</th>
<th class="d-none d-md-block" style="width: 20%;" i18n="latest-blocks.timestamp">Timestamp</th>
<th style="width: 20%;" i18n="latest-blocks.mined">Mined</th>
<th class="d-none d-lg-block" style="width: 15%;" i18n="latest-blocks.transactions">Transactions</th>
<th style="width: 20%;" i18n="latest-blocks.size">Size</th>
</thead>
<tbody>
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
<td><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td class="d-none d-md-block">&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
<td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
<td class="d-none d-lg-block">{{ block.tx_count | number }}</td>
<td>
<div class="progress">
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div>
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
</div>
</td>
</tr>
<ng-template [ngIf]="isLoading">
<tr *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
<td><span class="skeleton-loader"></span></td>
<td class="d-none d-md-block"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
<td class="d-none d-lg-block"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<ng-container *ngIf="(blocksLoadingStatus$ | async) as blocksLoadingStatus">
<tr>
<td colspan="5">
<div class="progress progress-dark">
<div class="progress-bar progress-darklight" role="progressbar" [ngStyle]="{'width': blocksLoadingStatus + '%' }"></div>
</div>
</td>
</tr>
</ng-container>
</ng-template>
</tbody>
</table>
<ng-template [ngIf]="error">
<div class="text-center">
<span>Error loading blocks</span>
<br>
<i>{{ error.error }}</i>
</div>
</ng-template>
</div>

View File

@ -1,14 +0,0 @@
.progress {
background-color: #2d3348;
}
@media (min-width: 768px) {
.d-md-block {
display: table-cell !important;
}
}
@media (min-width: 992px) {
.d-lg-block {
display: table-cell !important;
}
}

View File

@ -1,140 +0,0 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { StateService } from '../../services/state.service';
import { Block } from '../../interfaces/electrs.interface';
import { Subscription, Observable, merge, of } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { WebsocketService } from 'src/app/services/websocket.service';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-latest-blocks',
templateUrl: './latest-blocks.component.html',
styleUrls: ['./latest-blocks.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LatestBlocksComponent implements OnInit, OnDestroy {
network$: Observable<string>;
error: any;
blocks: any[] = [];
blockSubscription: Subscription;
isLoading = true;
interval: any;
blocksLoadingStatus$: Observable<number>;
latestBlockHeight: number;
heightOfPageUntilBlocks = 150;
heightOfBlocksTableChunk = 470;
constructor(
private electrsApiService: ElectrsApiService,
public stateService: StateService,
private seoService: SeoService,
private websocketService: WebsocketService,
private cd: ChangeDetectorRef,
) { }
ngOnInit() {
this.seoService.setTitle($localize`:@@f4cba7faeb126346f09cc6af30124f9a343f7a28:Blocks`);
this.websocketService.want(['blocks']);
this.network$ = merge(of(''), this.stateService.networkChanged$);
this.blocksLoadingStatus$ = this.stateService.loadingIndicators$
.pipe(
map((indicators) => indicators['blocks'] !== undefined ? indicators['blocks'] : 0)
);
this.blockSubscription = this.stateService.blocks$
.subscribe(([block]) => {
if (block === null || !this.blocks.length) {
return;
}
this.latestBlockHeight = block.height;
if (block.height === this.blocks[0].height) {
return;
}
// If we are out of sync, reload the blocks instead
if (block.height > this.blocks[0].height + 1) {
this.loadInitialBlocks();
return;
}
if (block.height <= this.blocks[0].height) {
return;
}
this.blocks.pop();
this.blocks.unshift(block);
this.cd.markForCheck();
});
this.loadInitialBlocks();
}
ngOnDestroy() {
clearInterval(this.interval);
this.blockSubscription.unsubscribe();
}
loadInitialBlocks() {
this.electrsApiService.listBlocks$()
.subscribe((blocks) => {
this.blocks = blocks;
this.isLoading = false;
this.error = undefined;
this.latestBlockHeight = blocks[0].height;
const spaceForBlocks = window.innerHeight - this.heightOfPageUntilBlocks;
const chunks = Math.ceil(spaceForBlocks / this.heightOfBlocksTableChunk) - 1;
if (chunks > 0) {
this.loadMore(chunks);
}
this.cd.markForCheck();
},
(error) => {
console.log(error);
this.error = error;
this.isLoading = false;
this.cd.markForCheck();
});
}
loadMore(chunks = 0) {
if (this.isLoading) {
return;
}
const height = this.blocks[this.blocks.length - 1].height - 1;
if (height < 0) {
return;
}
this.isLoading = true;
this.electrsApiService.listBlocks$(height)
.subscribe((blocks) => {
this.blocks = this.blocks.concat(blocks);
this.isLoading = false;
this.error = undefined;
const chunksLeft = chunks - 1;
if (chunksLeft > 0) {
this.loadMore(chunksLeft);
}
this.cd.markForCheck();
},
(error) => {
console.log(error);
this.error = error;
this.isLoading = false;
this.cd.markForCheck();
});
}
trackByBlock(index: number, block: Block) {
return block.height;
}
}

View File

@ -21,8 +21,8 @@
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</a>
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</a>
<button ngbDropdownItem class="liquid" [class.active]="network.val === 'liquid'" routerLink="/"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1"> Liquid</button>
<button ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" routerLink="/testnet"><img src="./resources/liquidtestnet-logo.png" style="width: 30px;" class="mr-1"> Liquid Testnet</button>
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" routerLink="/"><img src="./resources/liquid-logo.png" style="width: 30px;"> Liquid</a>
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" routerLink="/testnet"><img src="./resources/liquidtestnet-logo.png" style="width: 30px;" class="mr-1"> Liquid Testnet</a>
</div>
</div>

View File

@ -3,7 +3,7 @@
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
<img [src]="officialMempoolSpace ? './resources/mempool-space-logo.png' : './resources/mempool-logo.png'" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
<img [src]="officialMempoolSpace ? './resources/mempool-space-logo.png' : './resources/mempool-logo.png'" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
<div class="connection-badge">
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
@ -13,16 +13,16 @@
<div (window:resize)="onResize($event)" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
<img src="./resources/{{ network.val === '' ? 'bitcoin' : network.val }}-logo.png" style="width: 25px; height: 25px;" class="mr-1">
<img src="./resources/{{ network.val === '' ? 'bitcoin' : network.val }}-logo.png" style="width: 25px; height: 25px;" class="mr-1" [alt]="(network.val === '' ? 'bitcoin' : network.val) + ' logo'">
</button>
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
<button ngbDropdownItem class="mainnet" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 30px;" class="mr-1"> Mainnet</button>
<button ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><img src="./resources/signet-logo.png" style="width: 30px;" class="mr-1"> Signet</button>
<button ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</button>
<a ngbDropdownItem class="mainnet" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 30px;" class="mainnet mr-1" alt="bitcoin logo"> Mainnet</a>
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><img src="./resources/signet-logo.png" style="width: 30px;" class="signet mr-1" alt="logo"> Signet</a>
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1" alt="testnet logo"> Testnet</a>
<h6 *ngIf="env.LIQUID_ENABLED || env.BISQ_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1"> Liquid</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><img src="./resources/liquidtestnet-logo.png" style="width: 30px;" class="mr-1"> Liquid Testnet</a>
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1" alt="bisq logo"> Bisq</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1" alt="liquid mainnet logo"> Liquid</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><img src="./resources/liquidtestnet-logo.png" style="width: 30px;" class="mr-1" alt="liquid testnet logo"> Liquid Testnet</a>
</div>
</div>
@ -32,13 +32,13 @@
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="master-page.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="mining.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" id="btn-blocks" *ngIf="!stateService.env.MINING_DASHBOARD">
<a class="nav-link" [routerLink]="['/blocks' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cubes']" [fixedWidth]="true" i18n-title="master-page.blocks" title="Blocks"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" id="btn-graphs">
<a class="nav-link" [routerLink]="['/graphs/mempool' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'chart-area']" [fixedWidth]="true" i18n-title="master-page.graphs" title="Graphs"></fa-icon></a>
<a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'chart-area']" [fixedWidth]="true" i18n-title="master-page.graphs" title="Graphs"></fa-icon></a>
</li>
<li class="nav-item d-none d-lg-block" routerLinkActive="active" id="btn-tv">
<a class="nav-link" [routerLink]="['/tv' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon></a>

View File

@ -21,7 +21,7 @@
<td><span class="yellow-color">{{ mempoolBlock.feeRange[0] | number:'1.0-0' }} - {{ mempoolBlock.feeRange[mempoolBlock.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
</tr>
<tr>
<td i18n="mempool-block.total-fees">Total fees</td>
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
<td><app-amount [satoshis]="mempoolBlock.totalFees" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolBlock.totalFees" digitsInfo="1.0-0"></app-fiat></span></td>
</tr>
<tr>

View File

@ -68,7 +68,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
getOrdinal(mempoolBlock: MempoolBlock): string {
const blocksInBlock = Math.ceil(mempoolBlock.blockVSize / this.stateService.blockVSize);
if (this.mempoolBlockIndex === 0) {
return $localize`:@@mempool-block.next.block:Next block`;
return $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`;
} else if (this.mempoolBlockIndex === this.stateService.env.KEEP_BLOCKS_AMOUNT - 1 && blocksInBlock > 1) {
return $localize`:@@mempool-block.stack.of.blocks:Stack of ${blocksInBlock}:INTERPOLATION: mempool blocks`;
} else {

View File

@ -1,12 +0,0 @@
<ng-template [ngIf]="loading" [ngIfElse]="done">
<span class="skeleton-loader"></span>
</ng-template>
<ng-template #done>
<ng-template [ngIf]="miner" [ngIfElse]="unknownMiner">
<a placement="bottom" [ngbTooltip]="title" [href]="url" [target]="target" class="badge badge-primary">{{ miner }}</a>
</ng-template>
<ng-template #unknownMiner>
<span class="badge badge-secondary" i18n="miner.tag.unknown-miner">Unknown</span>
</ng-template>
</ng-template>

View File

@ -1,3 +0,0 @@
.badge {
font-size: 14px;
}

View File

@ -1,94 +0,0 @@
import { Component, Input, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { AssetsService } from 'src/app/services/assets.service';
import { Transaction } from 'src/app/interfaces/electrs.interface';
import { StateService } from 'src/app/services/state.service';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-miner',
templateUrl: './miner.component.html',
styleUrls: ['./miner.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MinerComponent implements OnChanges {
@Input() coinbaseTransaction: Transaction;
miner = '';
title = '';
url = '';
target = '_blank';
loading = true;
constructor(
private assetsService: AssetsService,
private cd: ChangeDetectorRef,
public stateService: StateService,
private relativeUrlPipe: RelativeUrlPipe,
) { }
ngOnChanges() {
this.miner = '';
if (this.stateService.env.MINING_DASHBOARD) {
this.miner = 'Unknown';
this.url = this.relativeUrlPipe.transform(`/mining/pool/unknown`);
this.target = '';
}
this.loading = true;
this.findMinerFromCoinbase();
}
findMinerFromCoinbase() {
if (this.coinbaseTransaction == null || this.coinbaseTransaction.vin == null || this.coinbaseTransaction.vin.length === 0) {
return null;
}
this.assetsService.getMiningPools$.subscribe((pools) => {
for (const vout of this.coinbaseTransaction.vout) {
if (!vout.scriptpubkey_address) {
continue;
}
if (pools.payout_addresses[vout.scriptpubkey_address]) {
this.miner = pools.payout_addresses[vout.scriptpubkey_address].name;
this.title = $localize`:@@miner-identified-by-payout:Identified by payout address: '${vout.scriptpubkey_address}:PAYOUT_ADDRESS:'`;
const pool = pools.payout_addresses[vout.scriptpubkey_address];
if (this.stateService.env.MINING_DASHBOARD && pools.slugs && pools.slugs[pool.name] !== undefined) {
this.url = this.relativeUrlPipe.transform(`/mining/pool/${pools.slugs[pool.name]}`);
this.target = '';
} else {
this.url = pool.link;
}
break;
}
for (const tag in pools.coinbase_tags) {
if (pools.coinbase_tags.hasOwnProperty(tag)) {
const coinbaseAscii = this.hex2ascii(this.coinbaseTransaction.vin[0].scriptsig);
if (coinbaseAscii.indexOf(tag) > -1) {
const pool = pools.coinbase_tags[tag];
this.miner = pool.name;
this.title = $localize`:@@miner-identified-by-coinbase:Identified by coinbase tag: '${tag}:TAG:'`;
if (this.stateService.env.MINING_DASHBOARD && pools.slugs && pools.slugs[pool.name] !== undefined) {
this.url = this.relativeUrlPipe.transform(`/mining/pool/${pools.slugs[pool.name]}`);
this.target = '';
} else {
this.url = pool.link;
}
break;
}
}
}
}
this.loading = false;
this.cd.markForCheck();
});
}
hex2ascii(hex: string) {
let str = '';
for (let i = 0; i < hex.length; i += 2) {
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
return str;
}
}

View File

@ -11,7 +11,7 @@
<span style="font-size: xx-small" i18n="mining.144-blocks">(144 blocks)</span>
</div>
<div class="card-wrapper">
<div class="card" style="height: 123px">
<div class="card">
<div class="card-body more-padding">
<app-reward-stats></app-reward-stats>
</div>
@ -22,29 +22,25 @@
<!-- difficulty adjustment -->
<div class="col">
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<div class="card" style="height: 123px">
<app-difficulty [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
</div>
<app-difficulty [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
</div>
<!-- pool distribution -->
<div class="col">
<div class="card">
<div class="card-body">
<div class="col" style="margin-bottom: 1.47rem">
<div class="card graph-card">
<div class="card-body pl-2 pr-2">
<app-pool-ranking [widget]=true></app-pool-ranking>
<div class="mt-1"><a [routerLink]="['/graphs/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
<div class="mt-1"><a [routerLink]="['/graphs/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
<!-- hashrate -->
<div class="col">
<div class="col" style="margin-bottom: 1.47rem">
<div class="card">
<div class="card-body">
<app-hashrate-chart [widget]=true></app-hashrate-chart>
<div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<app-hashrate-chart [widget]="true"></app-hashrate-chart>
<div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
@ -53,12 +49,9 @@
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">
Latest blocks
</h5>
<h5 class="card-title" i18n="dashboard.latest-blocks">Latest blocks</h5>
<app-blocks-list [widget]=true></app-blocks-list>
<div><a [routerLink]="['/mining/blocks' | relativeUrl]" i18n="dashboard.view-more">View
more &raquo;</a></div>
<div><a [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
@ -67,12 +60,9 @@
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">
Adjustments
</h5>
<h5 class="card-title" i18n="dashboard.adjustments">Adjustments</h5>
<app-difficulty-adjustments-table></app-difficulty-adjustments-table>
<div><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
<div><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>

View File

@ -14,6 +14,13 @@
background-color: #1d1f31;
}
.graph-card {
height: 100%;
@media (min-width: 992px) {
height: 385px;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
@ -22,9 +29,6 @@
color: #4a68b9;
}
.card-body {
padding: 1.25rem 1rem 0.75rem 1rem;
}
.card-body.pool-ranking {
padding: 1.25rem 0.25rem 0.75rem 0.25rem;
}

View File

@ -13,7 +13,7 @@ export class MiningDashboardComponent implements OnInit {
private seoService: SeoService,
private websocketService: WebsocketService,
) {
this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`);
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
}
ngOnInit(): void {

View File

@ -1 +0,0 @@
<router-outlet></router-outlet>

View File

@ -1,14 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-mining-start',
templateUrl: './mining-start.component.html',
})
export class MiningStartComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -5,7 +5,7 @@
<div *ngIf="widget">
<div class="pool-distribution" *ngIf="(miningStatsObservable$ | async) as miningStats; else loadingReward">
<div class="item">
<h5 class="card-title" i18n="mining.miners-luck">Pools luck (1w)</h5>
<h5 class="card-title" i18n="mining.miners-luck">Pools Luck (1w)</h5>
<p class="card-text">
{{ miningStats['minersLuck'] }}%
</p>
@ -17,7 +17,7 @@
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.miners-count">Pools count (1w)</h5>
<h5 class="card-title" i18n="mining.miners-count">Pools Count (1w)</h5>
<p class="card-text">
{{ miningStats.pools.length }}
</p>
@ -26,11 +26,10 @@
</div>
<div class="card-header" *ngIf="!widget">
<span i18n="mining.mining-pool-share">Mining pools share</span>
<span i18n="mining.pools">Pools Ranking</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup"
*ngIf="!widget && (miningStatsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
@ -62,7 +61,7 @@
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount > 157680">
<input ngbButton type="radio" [value]="'all'" fragment="all"> ALL
<input ngbButton type="radio" [value]="'all'" fragment="all"><span i18n>All</span>
</label>
</div>
</form>
@ -87,14 +86,15 @@
<th class="" i18n="mining.pool-name">Pool</th>
<th class="" *ngIf="this.miningWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th>
<th class="" i18n="master-page.blocks">Blocks</th>
<th class="d-none d-md-block" i18n="mining.empty-blocks">Empty Blocks</th>
<th class="d-none d-md-block" i18n="mining.empty-blocks">Empty blocks</th>
</tr>
</thead>
<tbody *ngIf="(miningStatsObservable$ | async) as miningStats">
<tr *ngFor="let pool of miningStats.pools">
<td class="d-none d-md-block">{{ pool.rank }}</td>
<td class="text-right"><img width="25" height="25" src="{{ pool.logo }}"
onError="this.src = './resources/mining-pools/default.svg'"></td>
<td class="text-right">
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.src = './resources/mining-pools/default.svg'">
</td>
<td class=""><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
<td class="" *ngIf="this.miningWindowPreference === '24h' && !isLoading">{{ pool.lastEstimatedHashrate }} {{
miningStats.miningUnits.hashrateUnit }}</td>
@ -104,7 +104,7 @@
<tr style="border-top: 1px solid #555">
<td class="d-none d-md-block"></td>
<td class="text-right"></td>
<td class="" i18n="mining.all-miners"><b>All miners</b></td>
<td class=""><b i18n="mining.all-miners">All miners</b></td>
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{
miningStats.miningUnits.hashrateUnit }}</b></td>
<td class=""><b>{{ miningStats.blockCount }}</b></td>
@ -121,7 +121,7 @@
<ng-template #loadingReward>
<div class="pool-distribution">
<div class="item">
<h5 class="card-title" i18n="mining.miners-luck">Pools luck (1w)</h5>
<h5 class="card-title" i18n="mining.miners-luck">Pools Luck (1w)</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>
@ -133,7 +133,7 @@
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.miners-count">Pools count (1w)</h5>
<h5 class="card-title" i18n="mining.miners-count">Pools Count (1w)</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>

View File

@ -27,7 +27,7 @@
.chart-widget {
width: 100%;
height: 100%;
max-height: 270px;
height: 240px;
@media (max-width: 485px) {
max-height: 200px;
}

View File

@ -153,13 +153,14 @@ export class PoolRankingComponent implements OnInit {
},
borderColor: '#000',
formatter: () => {
const i = pool.blockCount.toString();
if (this.miningWindowPreference === '24h') {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.lastEstimatedHashrate.toString() + ' PH/s' +
`<br>` + pool.blockCount.toString() + ` blocks`;
`<br>` + $localize`${i} blocks`;
} else {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.blockCount.toString() + ` blocks`;
$localize`${i} blocks`;
}
}
},

View File

@ -5,7 +5,7 @@
<!-- Pool overview -->
<div *ngIf="poolStats$ | async as poolStats; else loadingMain">
<div style="display:flex" class="mb-3">
<img width="50" height="50" src="{{ poolStats['logo'] }}"
<img width="50" height="50" src="{{ poolStats['logo'] }}" [alt]="poolStats.pool.name + ' mining pool logo'"
onError="this.src = './resources/mining-pools/default.svg'" class="mr-3">
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
</div>
@ -93,10 +93,8 @@
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated
</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th>
</tr>
</thead>
@ -117,10 +115,8 @@
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated
</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
</th>
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th>
</tr>
</thead>
@ -142,7 +138,7 @@
<!-- Mined blocks desktop -->
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.mined-blocks">Mined Blocks</td>
<td class="label" i18n="mining.mined-blocks">Mined blocks</td>
<td class="data">
<table class="table table-xs table-data">
<thead>
@ -166,7 +162,7 @@
<!-- Mined blocks mobile -->
<tr *ngIf="isMobile()">
<td colspan=2>
<span class="label" i18n="mining.mined-blocks">Mined Blocks</span>
<span class="label" i18n="mining.mined-blocks">Mined blocks</span>
<table class="table table-xs table-data">
<thead>
<tr>
@ -216,19 +212,16 @@
<th class="height" i18n="latest-blocks.height">Height</th>
<th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
<th class="mined" i18n="latest-blocks.mined">Mined</th>
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">
Coinbase Tag</th>
<th class="reward text-right" i18n="latest-blocks.reward">
Reward</th>
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
<th class="reward text-right" i18n="latest-blocks.reward">Reward</th>
<th class="fees text-right" i18n="latest-blocks.fees">Fees</th>
<th class="txs text-right" i18n="latest-blocks.transactions">Txs</th>
<th class="txs text-right" i18n="dashboard.txs">TXs</th>
<th class="size" i18n="latest-blocks.size">Size</th>
</thead>
<tbody [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
<td class="height">
<a [routerLink]="['/block' | relativeUrl, block.height]">{{ block.height
}}</a>
<a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a>
</td>
<td class="timestamp">
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
@ -266,12 +259,10 @@
<th class="height" i18n="latest-blocks.height">Height</th>
<th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
<th class="mined" i18n="latest-blocks.mined">Mined</th>
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">
Coinbase Tag</th>
<th class="reward text-right" i18n="latest-blocks.reward">
Reward</th>
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
<th class="reward text-right" i18n="latest-blocks.reward">Reward</th>
<th class="fees text-right" i18n="latest-blocks.fees">Fees</th>
<th class="txs text-right" i18n="latest-blocks.transactions">Txs</th>
<th class="txs text-right" i18n="dashboard.txs">TXs</th>
<th class="size" i18n="latest-blocks.size">Size</th>
</thead>
<tbody>
@ -378,10 +369,8 @@
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated
</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th>
</tr>
</thead>
@ -406,10 +395,8 @@
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated
</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
</th>
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th>
</tr>
</thead>
@ -430,13 +417,13 @@
<!-- Mined blocks desktop -->
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.mined-blocks">Mined Blocks</td>
<td class="label" i18n="mining.mined-blocks">Mined blocks</td>
<td class="data">
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title" style="width: 37%" i18n="24h">24h</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
<th scope="col" class="block-count-title" style="width: 37%">24h</th>
<th scope="col" class="block-count-title" style="width: 37%">1w</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="all">All</th>
</tr>
</thead>
@ -457,12 +444,12 @@
<!-- Mined blocks mobile -->
<tr *ngIf="isMobile()">
<td colspan=2>
<span class="label" i18n="mining.mined-blocks">Mined Blocks</span>
<span class="label" i18n="mining.mined-blocks">Mined blocks</span>
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title" style="width: 33%" i18n="24h">24h</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
<th scope="col" class="block-count-title" style="width: 33%">24h</th>
<th scope="col" class="block-count-title" style="width: 37%">1w</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="all">All</th>
</tr>
</thead>

View File

@ -3,7 +3,7 @@
<form [formGroup]="pushTxForm" (submit)="pushTxForm.valid && postTx()" novalidate>
<div class="mb-3">
<textarea formControlName="txHash" class="form-control" rows="5" i18n-placeholder="transaction.hex" placeholder="Transaction Hex"></textarea>
<textarea formControlName="txHash" class="form-control" rows="5" i18n-placeholder="transaction.hex" placeholder="Transaction hex"></textarea>
</div>
<button [disabled]="isLoading" type="submit" class="btn btn-primary mr-2" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</button>
<p class="red-color d-inline">{{ error }}</p> <a *ngIf="txId" [routerLink]="['/tx/' | relativeUrl, txId]">{{ txId }}</a>

View File

@ -8,7 +8,7 @@
<app-amount [satoshis]="rewardStats.totalReward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
</div>
<span class="fiat">
<app-fiat [value]="rewardStats.totalReward"></app-fiat>
<app-fiat [value]="rewardStats.totalReward" digitsInfo="1.0-0" ></app-fiat>
</span>
</div>
</div>
@ -57,7 +57,7 @@
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.average-fee">Average Fee</h5>
<h5 class="card-title" i18n="mining.average-fee">Reward Per Tx</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>

View File

@ -46,7 +46,7 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
@ -209,7 +209,7 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="transaction.size|Transaction Size">Size</td>
<td i18n="block.size">Size</td>
<td [innerHTML]="'&lrm;' + (tx.size | bytes: 2)"></td>
</tr>
<tr>
@ -217,7 +217,7 @@
<td [innerHTML]="'&lrm;' + (tx.weight / 4 | vbytes: 2)"></td>
</tr>
<tr>
<td i18n="transaction.weight|Transaction Weight">Weight</td>
<td i18n="block.weight">Weight</td>
<td [innerHTML]="'&lrm;' + (tx.weight | wuBytes: 2)"></td>
</tr>
</tbody>
@ -235,7 +235,7 @@
<td [innerHTML]="'&lrm;' + (tx.locktime | number)"></td>
</tr>
<tr>
<td i18n="transaction.hex">Transaction Hex</td>
<td i18n="transaction.hex">Transaction hex</td>
<td><a target="_blank" href="{{ network === '' ? '' : '/' + network }}/api/tx/{{ txId }}/hex"><fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"></fa-icon></a></td>
</tr>
</tbody>
@ -383,7 +383,7 @@
<tbody>
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ tx.fee | number }} <span class="symbol" i18n="transaction.fee.sat|Transaction Fee sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></td>
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></td>
</tr>
<tr>
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>

View File

@ -125,7 +125,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
}),
switchMap(() => {
let transactionObservable$: Observable<Transaction>;
if (history.state.data) {
if (history.state.data && history.state.data.fee !== -1) {
transactionObservable$ = of(history.state.data);
} else {
transactionObservable$ = this.electrsApiService

View File

@ -60,12 +60,18 @@
</ng-container>
<ng-container *ngSwitchDefault>
<ng-template [ngIf]="!vin.prevout" [ngIfElse]="defaultAddress">
<span>{{ vin.issuance ? 'Issuance' : 'UNKNOWN' }}</span>
<span *ngIf="vin.lazy; else defaultNoPrevout" class="skeleton-loader"></span>
<ng-template #defaultNoPrevout>
<span>{{ vin.issuance ? 'Issuance' : 'UNKNOWN' }}</span>
</ng-template>
</ng-template>
<ng-template #defaultAddress>
<a [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vin.prevout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-block">{{ vin.prevout.scriptpubkey_address | shortenString : 35 }}</span>
<span class="d-none d-lg-flex justify-content-start">
<span class="addr-left flex-grow-1" [style]="vin.prevout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vin.prevout.scriptpubkey_address }}</span>
<span *ngIf="vin.prevout.scriptpubkey_address.length > 40" class="addr-right">{{ vin.prevout.scriptpubkey_address | capAddress: 40: 10 }}</span>
</span>
</a>
<div>
<app-address-labels [vin]="vin"></app-address-labels>
@ -84,6 +90,7 @@
</ng-template>
</ng-template>
<ng-template #defaultOutput>
<span *ngIf="vin.lazy" class="skeleton-loader"></span>
<app-amount *ngIf="vin.prevout" [satoshis]="vin.prevout.value"></app-amount>
</ng-template>
</td>
@ -138,7 +145,7 @@
</ng-template>
<tr *ngIf="tx.vin.length > 12 && tx['@vinLimit']">
<td colspan="3" class="text-center">
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@vinLimit'] = false;"><span i18n="transactions-list.load-all">Load all</span> ({{ tx.vin.length - 10 }})</button>
<button class="btn btn-sm btn-primary mt-2" (click)="loadMoreInputs(tx);"><span i18n="show-all">Show all</span> ({{ tx.vin.length - 10 }})</button>
</td>
</tr>
</tbody>
@ -156,7 +163,10 @@
<td>
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-block">{{ vout.scriptpubkey_address | shortenString : 35 }}</span>
<span class="d-none d-lg-flex justify-content-start">
<span class="addr-left flex-grow-1" [style]="vout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vout.scriptpubkey_address }}</span>
<span *ngIf="vout.scriptpubkey_address.length > 40" class="addr-right">{{ vout.scriptpubkey_address | capAddress: 40: 10 }}</span>
</span>
</a>
<div>
<app-address-labels [vout]="vout"></app-address-labels>
@ -232,7 +242,7 @@
<td style="text-align: left;">{{ vout.scriptpubkey }}</td>
</tr>
<tr *ngIf="vout.scriptpubkey_type == 'op_return'">
<td>OP_RETURN <span i18n="transactions-list.vout.scriptpubkey-type.data">data</span></td>
<td>OP_RETURN <span>data</span></td>
<td style="text-align: left;">{{ vout.scriptpubkey_asm | hex2ascii }}</td>
</tr>
<tr *ngIf="vout.scriptpubkey_type">
@ -246,7 +256,7 @@
</ng-template>
<tr *ngIf="tx.vout.length > 12 && tx['@voutLimit'] && !outputIndex">
<td colspan="3" class="text-center">
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="transactions-list.load-all">Load all</span> ({{ tx.vout.length - 10 }})</button>
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vout.length - 10 }})</button>
</td>
</tr>
</tbody>
@ -255,9 +265,10 @@
</div>
<div class="summary">
<div class="float-left mt-2-5" *ngIf="!transactionPage && !tx.vin[0].is_coinbase">
<div class="float-left mt-2-5" *ngIf="!transactionPage && !tx.vin[0].is_coinbase && tx.fee !== -1">
{{ tx.fee / (tx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="d-none d-sm-inline-block">&nbsp;&ndash; {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></span>
</div>
<div class="float-left mt-2-5 grey-info-text" *ngIf="tx.fee === -1" i18n="transactions-list.load-to-reveal-fee-info">Show all inputs to reveal fee data</div>
<div class="float-right">
<ng-container *ngIf="showConfirmations && latestBlock$ | async as latestBlock">

View File

@ -129,3 +129,20 @@ h2 {
.summary {
margin-top: 10px;
}
.addr-left {
font-family: monospace;
overflow: hidden;
text-overflow: ellipsis;
margin-right: -7px
}
.addr-right {
font-family: monospace;
}
.grey-info-text {
color:#6c757d;
font-style: italic;
font-size: 12px;
}

View File

@ -22,6 +22,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
@Input() showConfirmations = false;
@Input() transactionPage = false;
@Input() errorUnblinded = false;
@Input() paginated = false;
@Input() outputIndex: number;
@Input() address: string = '';
@ -29,7 +30,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
latestBlock$: Observable<BlockExtended>;
outspendsSubscription: Subscription;
refreshOutspends$: ReplaySubject<object> = new ReplaySubject();
refreshOutspends$: ReplaySubject<{ [str: string]: Observable<Outspend[]>}> = new ReplaySubject();
showDetails$ = new BehaviorSubject<boolean>(false);
outspends: Outspend[][] = [];
assetsMinimal: any;
@ -84,6 +85,9 @@ export class TransactionsListComponent implements OnInit, OnChanges {
if (!this.transactions || !this.transactions.length) {
return;
}
if (this.paginated) {
this.outspends = [];
}
if (this.outputIndex) {
setTimeout(() => {
const assetBoxElements = document.getElementsByClassName('assetBox');
@ -171,6 +175,17 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}
}
loadMoreInputs(tx: Transaction) {
tx['@vinLimit'] = false;
this.electrsApiService.getTransaction$(tx.txid)
.subscribe((newTx) => {
tx.vin = newTx.vin;
tx.fee = newTx.fee;
this.ref.markForCheck();
});
}
ngOnDestroy() {
this.outspendsSubscription.unsubscribe();
}

View File

@ -121,7 +121,7 @@
<td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
<td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4">
<a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
<img width="20" height="20" src="{{ block.extras.pool['logo'] }}"
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
onError="this.src = './resources/mining-pools/default.svg'">
<span class="pool-name">{{ block.extras.pool.name }}</span>
</a>
@ -136,7 +136,7 @@
</tr>
</tbody>
</table>
<div class=""><a href="" [routerLink]="[(stateService.env.MINING_DASHBOARD ? '/mining/blocks' : '/blocks') | relativeUrl]" i18n="dashboard.view-all">View all &raquo;</a></div>
<div class=""><a href="" [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>

View File

@ -134,6 +134,8 @@
table-layout:fixed;
tr, td, th {
border: 0px;
padding-top: 0.71rem !important;
padding-bottom: 0.75rem !important;
}
td {
overflow:hidden;
@ -182,24 +184,21 @@
text-align: left;
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.7rem !important;
}
.table-cell-height {
width: 15%;
}
.table-cell-mined {
width: 35%;
text-align: right;
@media (min-width: 376px) {
text-align: left;
}
text-align: left;
}
.table-cell-transaction-count {
display: none;
text-align: right;
width: 20%;
@media (min-width: 376px) {
display: table-cell;
}
display: table-cell;
}
.table-cell-size {
display: none;

File diff suppressed because it is too large Load Diff

View File

@ -73,15 +73,15 @@
</ng-template>
</div>
<ng-container *ngIf="network.val === 'bisq' && item.codeExample.hasOwnProperty('bisq');else liquid_code_example" #bisq_code_example>
<app-code-template [hostname]="hostname" [baseNetworkUrl]="baseNetworkUrl" [method]="item.httpRequestMethod" [code]="item.codeExample.bisq" [network]="network.val" ></app-code-template>
<app-code-template [hostname]="hostname" [baseNetworkUrl]="baseNetworkUrl" [method]="item.httpRequestMethod" [code]="item.codeExample.bisq" [network]="network.val" [showCodeExample]="item.showJsExamples"></app-code-template>
</ng-container>
<ng-template #liquid_code_example>
<ng-container *ngIf="network.val === 'liquid' && item.codeExample.hasOwnProperty('liquid');else default_code_example">
<app-code-template [hostname]="hostname" [baseNetworkUrl]="baseNetworkUrl" [method]="item.httpRequestMethod" [code]="item.codeExample.liquid" [network]="network.val" ></app-code-template>
<app-code-template [hostname]="hostname" [baseNetworkUrl]="baseNetworkUrl" [method]="item.httpRequestMethod" [code]="item.codeExample.liquid" [network]="network.val" [showCodeExample]="item.showJsExamples"></app-code-template>
</ng-container>
</ng-template>
<ng-template #default_code_example>
<app-code-template [hostname]="hostname" [baseNetworkUrl]="baseNetworkUrl" [method]="item.httpRequestMethod" [code]="item.codeExample.default" [network]="network.val" ></app-code-template>
<app-code-template [hostname]="hostname" [baseNetworkUrl]="baseNetworkUrl" [method]="item.httpRequestMethod" [code]="item.codeExample.default" [network]="network.val" [showCodeExample]="item.showJsExamples"></app-code-template>
</ng-template>
</div>
</div>
@ -101,7 +101,7 @@
<div class="subtitle" i18n>Description</div>
<div i18n="api-docs.websocket.websocket">Default push: <code>{{ '{' }} action: 'want', data: ['blocks', ...] {{ '}' }}</code> to express what you want pushed. Available: <code>blocks</code>, <code>mempool-blocks</code>, <code>live-2h-chart</code>, and <code>stats</code>.<br><br>Push transactions related to address: <code>{{ '{' }} 'track-address': '3PbJ...bF9B' {{ '}' }}</code> to receive all new transactions containing that address as input or output. Returns an array of transactions. <code>address-transactions</code> for new mempool transactions, and <code>block-transactions</code> for new block confirmed transactions.</div>
</div>
<app-code-template [method]="'websocket'" [hostname]="hostname" [code]="wsDocs" [network]="network.val" ></app-code-template>
<app-code-template [method]="'websocket'" [hostname]="hostname" [code]="wsDocs" [network]="network.val" [showCodeExample]="wsDocs.showJsExamples"></app-code-template>
</div>
</div>
</div>

View File

@ -1,7 +1,6 @@
import { Component, OnInit, Input, ViewChild, ElementRef } from '@angular/core';
import { Env, StateService } from '../../services/state.service';
import { Observable, merge, of } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { tap } from 'rxjs/operators';
import { ActivatedRoute } from "@angular/router";
import { faqData, restApiDocsData, wsApiDocsData } from './api-docs-data';
@ -27,7 +26,6 @@ export class ApiDocsComponent implements OnInit {
constructor(
private stateService: StateService,
private seoService: SeoService,
private route: ActivatedRoute,
) { }
@ -39,13 +37,12 @@ export class ApiDocsComponent implements OnInit {
}
window.addEventListener('scroll', function() {
that.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative";
});
}, { passive: true} );
}, 1 );
}
ngOnInit(): void {
this.env = this.stateService.env;
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
this.network$ = merge(of(''), this.stateService.networkChanged$).pipe(
tap((network: string) => {
if (this.env.BASE_MODULE === 'mempool' && network !== '') {

View File

@ -1,14 +1,14 @@
<div class="code">
<ul ngbNav #navCodeTemplate="ngbNav" class="nav-tabs code-tab">
<li ngbNavItem *ngIf="code.codeTemplate.curl && method !== 'websocket'">
<a ngbNavLink (click)="adjustContainerHeight( $event )">cURL</a>
<ul ngbNav #navCodeTemplate="ngbNav" class="nav-tabs code-tab" role="tablist">
<li ngbNavItem *ngIf="code.codeTemplate.curl && method !== 'websocket'" role="presentation">
<a ngbNavLink (click)="adjustContainerHeight( $event )" role="tab">cURL</a>
<ng-template ngbNavContent>
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapCurlTemplate(code)"></app-clipboard></div>
<pre><code [innerText]="wrapCurlTemplate(code)"></code></pre>
</ng-template>
</li>
<li ngbNavItem *ngIf="network !== 'liquidtestnet'">
<a ngbNavLink (click)="adjustContainerHeight( $event )" >CommonJS</a>
<li ngbNavItem *ngIf="showCodeExample[network]" role="presentation">
<a ngbNavLink (click)="adjustContainerHeight( $event )" role="tab">CommonJS</a>
<ng-template ngbNavContent>
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapCommonJS(code)"></app-clipboard></div>
<div class="links">
@ -17,8 +17,8 @@
<pre><code [innerText]="wrapCommonJS(code)"></code></pre>
</ng-template>
</li>
<li ngbNavItem>
<a ngbNavLink (click)="adjustContainerHeight( $event )" *ngIf="network !== 'liquidtestnet'">ES Module</a>
<li ngbNavItem *ngIf="showCodeExample[network]" role="presentation">
<a ngbNavLink (click)="adjustContainerHeight( $event )" role="tab">ES Module</a>
<ng-template ngbNavContent>
<div class="subtitle"><ng-container i18n="API Docs install lib">Install Package</ng-container> <app-clipboard [text]="wrapImportTemplate()"></app-clipboard></div>
<div class="links">

View File

@ -12,6 +12,7 @@ export class CodeTemplateComponent implements OnInit {
@Input() hostname: string;
@Input() baseNetworkUrl: string;
@Input() method: 'GET' | 'POST' | 'websocket' = 'GET';
@Input() showCodeExample: any;
env: Env;
constructor(

Some files were not shown because too many files have changed in this diff Show More