Merge branch 'master' into knorrium/update-node-matrix
This commit is contained in:
commit
5f5e96984a
72
.github/workflows/ci.yml
vendored
72
.github/workflows/ci.yml
vendored
@ -251,17 +251,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
module: ["mempool", "liquid"]
|
module: ["mempool", "liquid", "testnet4"]
|
||||||
include:
|
|
||||||
- module: "mempool"
|
|
||||||
spec: |
|
|
||||||
cypress/e2e/mainnet/*.spec.ts
|
|
||||||
cypress/e2e/signet/*.spec.ts
|
|
||||||
cypress/e2e/testnet4/*.spec.ts
|
|
||||||
- module: "liquid"
|
|
||||||
spec: |
|
|
||||||
cypress/e2e/liquid/liquid.spec.ts
|
|
||||||
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
|
|
||||||
|
|
||||||
name: E2E tests for ${{ matrix.module }}
|
name: E2E tests for ${{ matrix.module }}
|
||||||
steps:
|
steps:
|
||||||
@ -310,8 +300,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Unzip assets before building (src/resources)
|
- name: Unzip assets before building (src/resources)
|
||||||
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
|
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
|
||||||
|
|
||||||
|
# mempool
|
||||||
- name: Chrome browser tests (${{ matrix.module }})
|
- name: Chrome browser tests (${{ matrix.module }})
|
||||||
|
if: ${{ matrix.module == 'mempool' }}
|
||||||
uses: cypress-io/github-action@v5
|
uses: cypress-io/github-action@v5
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.event_name }}
|
tag: ${{ github.event_name }}
|
||||||
@ -322,7 +314,9 @@ jobs:
|
|||||||
wait-on-timeout: 120
|
wait-on-timeout: 120
|
||||||
record: true
|
record: true
|
||||||
parallel: true
|
parallel: true
|
||||||
spec: ${{ matrix.spec }}
|
spec: |
|
||||||
|
cypress/e2e/mainnet/*.spec.ts
|
||||||
|
cypress/e2e/signet/*.spec.ts
|
||||||
group: Tests on Chrome (${{ matrix.module }})
|
group: Tests on Chrome (${{ matrix.module }})
|
||||||
browser: "chrome"
|
browser: "chrome"
|
||||||
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
|
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
|
||||||
@ -332,6 +326,56 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
|
# liquid
|
||||||
|
- name: Chrome browser tests (${{ matrix.module }})
|
||||||
|
if: ${{ matrix.module == 'liquid' }}
|
||||||
|
uses: cypress-io/github-action@v5
|
||||||
|
with:
|
||||||
|
tag: ${{ github.event_name }}
|
||||||
|
working-directory: ${{ matrix.module }}/frontend
|
||||||
|
build: npm run config:defaults:${{ matrix.module }}
|
||||||
|
start: npm run start:local-staging
|
||||||
|
wait-on: "http://localhost:4200"
|
||||||
|
wait-on-timeout: 120
|
||||||
|
record: true
|
||||||
|
parallel: true
|
||||||
|
spec: |
|
||||||
|
cypress/e2e/liquid/liquid.spec.ts
|
||||||
|
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
|
||||||
|
group: Tests on Chrome (${{ matrix.module }})
|
||||||
|
browser: "chrome"
|
||||||
|
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
|
||||||
|
env:
|
||||||
|
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
|
# testnet
|
||||||
|
- name: Chrome browser tests (${{ matrix.module }})
|
||||||
|
if: ${{ matrix.module == 'testnet4' }}
|
||||||
|
uses: cypress-io/github-action@v5
|
||||||
|
with:
|
||||||
|
tag: ${{ github.event_name }}
|
||||||
|
working-directory: ${{ matrix.module }}/frontend
|
||||||
|
build: npm run config:defaults:mempool
|
||||||
|
start: npm run start:local-staging
|
||||||
|
wait-on: "http://localhost:4200"
|
||||||
|
wait-on-timeout: 120
|
||||||
|
record: true
|
||||||
|
parallel: true
|
||||||
|
spec: |
|
||||||
|
cypress/e2e/testnet4/*.spec.ts
|
||||||
|
group: Tests on Chrome (${{ matrix.module }})
|
||||||
|
browser: "chrome"
|
||||||
|
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
|
||||||
|
env:
|
||||||
|
CYPRESS_REROUTE_TESTNET: true
|
||||||
|
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
validate_docker_json:
|
validate_docker_json:
|
||||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
@ -359,4 +403,4 @@ jobs:
|
|||||||
- name: Validate JSON syntax
|
- name: Validate JSON syntax
|
||||||
run: |
|
run: |
|
||||||
cat mempool-config.json | jq
|
cat mempool-config.json | jq
|
||||||
working-directory: docker/docker/backend
|
working-directory: docker/docker/backend
|
@ -155,6 +155,10 @@
|
|||||||
"API": "https://mempool.space/api/v1/services",
|
"API": "https://mempool.space/api/v1/services",
|
||||||
"ACCELERATIONS": false
|
"ACCELERATIONS": false
|
||||||
},
|
},
|
||||||
|
"STRATUM": {
|
||||||
|
"ENABLED": false,
|
||||||
|
"API": "http://localhost:1234"
|
||||||
|
},
|
||||||
"FIAT_PRICE": {
|
"FIAT_PRICE": {
|
||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
"PAID": false,
|
"PAID": false,
|
||||||
|
@ -151,5 +151,9 @@
|
|||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
"PAID": false,
|
"PAID": false,
|
||||||
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
|
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
|
||||||
|
},
|
||||||
|
"STRATUM": {
|
||||||
|
"ENABLED": false,
|
||||||
|
"API": "http://localhost:1234"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,6 +159,11 @@ describe('Mempool Backend Config', () => {
|
|||||||
PAID: false,
|
PAID: false,
|
||||||
API_KEY: '',
|
API_KEY: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(config.STRATUM).toStrictEqual({
|
||||||
|
ENABLED: false,
|
||||||
|
API: 'http://localhost:1234',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -119,7 +119,11 @@ class RbfCache {
|
|||||||
|
|
||||||
|
|
||||||
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
if ( !newTxExtended
|
||||||
|
|| !replaced?.length
|
||||||
|
|| this.txs.has(newTxExtended.txid)
|
||||||
|
|| !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
105
backend/src/api/services/stratum.ts
Normal file
105
backend/src/api/services/stratum.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { WebSocket } from 'ws';
|
||||||
|
import logger from '../../logger';
|
||||||
|
import config from '../../config';
|
||||||
|
import websocketHandler from '../websocket-handler';
|
||||||
|
|
||||||
|
export interface StratumJob {
|
||||||
|
pool: number;
|
||||||
|
height: number;
|
||||||
|
coinbase: string;
|
||||||
|
scriptsig: string;
|
||||||
|
reward: number;
|
||||||
|
jobId: string;
|
||||||
|
extraNonce: string;
|
||||||
|
extraNonce2Size: number;
|
||||||
|
prevHash: string;
|
||||||
|
coinbase1: string;
|
||||||
|
coinbase2: string;
|
||||||
|
merkleBranches: string[];
|
||||||
|
version: string;
|
||||||
|
bits: string;
|
||||||
|
time: string;
|
||||||
|
timestamp: number;
|
||||||
|
cleanJobs: boolean;
|
||||||
|
received: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStratumJob(obj: any): obj is StratumJob {
|
||||||
|
return obj
|
||||||
|
&& typeof obj === 'object'
|
||||||
|
&& 'pool' in obj
|
||||||
|
&& 'prevHash' in obj
|
||||||
|
&& 'height' in obj
|
||||||
|
&& 'received' in obj
|
||||||
|
&& 'version' in obj
|
||||||
|
&& 'timestamp' in obj
|
||||||
|
&& 'bits' in obj
|
||||||
|
&& 'merkleBranches' in obj
|
||||||
|
&& 'cleanJobs' in obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StratumApi {
|
||||||
|
private ws: WebSocket | null = null;
|
||||||
|
private runWebsocketLoop: boolean = false;
|
||||||
|
private startedWebsocketLoop: boolean = false;
|
||||||
|
private websocketConnected: boolean = false;
|
||||||
|
private jobs: Record<string, StratumJob> = {};
|
||||||
|
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
public getJobs(): Record<string, StratumJob> {
|
||||||
|
return this.jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleWebsocketMessage(msg: any): void {
|
||||||
|
if (isStratumJob(msg)) {
|
||||||
|
this.jobs[msg.pool] = msg;
|
||||||
|
websocketHandler.handleNewStratumJob(this.jobs[msg.pool]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connectWebsocket(): Promise<void> {
|
||||||
|
if (!config.STRATUM.ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.runWebsocketLoop = true;
|
||||||
|
if (this.startedWebsocketLoop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (this.runWebsocketLoop) {
|
||||||
|
this.startedWebsocketLoop = true;
|
||||||
|
if (!this.ws) {
|
||||||
|
this.ws = new WebSocket(`${config.STRATUM.API}`);
|
||||||
|
this.websocketConnected = true;
|
||||||
|
|
||||||
|
this.ws.on('open', () => {
|
||||||
|
logger.info('Stratum websocket opened');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('error', (error) => {
|
||||||
|
logger.err('Stratum websocket error: ' + error);
|
||||||
|
this.ws = null;
|
||||||
|
this.websocketConnected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('close', () => {
|
||||||
|
logger.info('Stratum websocket closed');
|
||||||
|
this.ws = null;
|
||||||
|
this.websocketConnected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('message', (data, isBinary) => {
|
||||||
|
try {
|
||||||
|
const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string);
|
||||||
|
this.handleWebsocketMessage(parsedMsg);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Failed to parse stratum websocket message: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new StratumApi();
|
@ -38,6 +38,7 @@ interface AddressTransactions {
|
|||||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||||
import { calculateMempoolTxCpfp } from './cpfp';
|
import { calculateMempoolTxCpfp } from './cpfp';
|
||||||
import { getRecentFirstSeen } from '../utils/file-read';
|
import { getRecentFirstSeen } from '../utils/file-read';
|
||||||
|
import stratumApi, { StratumJob } from './services/stratum';
|
||||||
|
|
||||||
// valid 'want' subscriptions
|
// valid 'want' subscriptions
|
||||||
const wantable = [
|
const wantable = [
|
||||||
@ -403,6 +404,16 @@ class WebsocketHandler {
|
|||||||
delete client['track-mempool'];
|
delete client['track-mempool'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsedMessage && parsedMessage['track-stratum'] != null) {
|
||||||
|
if (parsedMessage['track-stratum']) {
|
||||||
|
const sub = parsedMessage['track-stratum'];
|
||||||
|
client['track-stratum'] = sub;
|
||||||
|
response['stratumJobs'] = this.socketData['stratumJobs'];
|
||||||
|
} else {
|
||||||
|
client['track-stratum'] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(response).length) {
|
if (Object.keys(response).length) {
|
||||||
client.send(this.serializeResponse(response));
|
client.send(this.serializeResponse(response));
|
||||||
}
|
}
|
||||||
@ -1384,6 +1395,23 @@ class WebsocketHandler {
|
|||||||
await statistics.runStatistics();
|
await statistics.runStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public handleNewStratumJob(job: StratumJob): void {
|
||||||
|
this.updateSocketDataFields({ 'stratumJobs': stratumApi.getJobs() });
|
||||||
|
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach((client) => {
|
||||||
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (client['track-stratum'] && (client['track-stratum'] === 'all' || client['track-stratum'] === job.pool)) {
|
||||||
|
client.send(JSON.stringify({
|
||||||
|
'stratumJob': job
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// takes a dictionary of JSON serialized values
|
// takes a dictionary of JSON serialized values
|
||||||
// and zips it together into a valid JSON object
|
// and zips it together into a valid JSON object
|
||||||
private serializeResponse(response): string {
|
private serializeResponse(response): string {
|
||||||
|
@ -165,6 +165,10 @@ interface IConfig {
|
|||||||
WALLETS: {
|
WALLETS: {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
WALLETS: string[];
|
WALLETS: string[];
|
||||||
|
},
|
||||||
|
STRATUM: {
|
||||||
|
ENABLED: boolean;
|
||||||
|
API: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,6 +336,10 @@ const defaults: IConfig = {
|
|||||||
'ENABLED': false,
|
'ENABLED': false,
|
||||||
'WALLETS': [],
|
'WALLETS': [],
|
||||||
},
|
},
|
||||||
|
'STRATUM': {
|
||||||
|
'ENABLED': false,
|
||||||
|
'API': 'http://localhost:1234',
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class Config implements IConfig {
|
class Config implements IConfig {
|
||||||
@ -354,6 +362,7 @@ class Config implements IConfig {
|
|||||||
REDIS: IConfig['REDIS'];
|
REDIS: IConfig['REDIS'];
|
||||||
FIAT_PRICE: IConfig['FIAT_PRICE'];
|
FIAT_PRICE: IConfig['FIAT_PRICE'];
|
||||||
WALLETS: IConfig['WALLETS'];
|
WALLETS: IConfig['WALLETS'];
|
||||||
|
STRATUM: IConfig['STRATUM'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const configs = this.merge(configFromFile, defaults);
|
const configs = this.merge(configFromFile, defaults);
|
||||||
@ -376,6 +385,7 @@ class Config implements IConfig {
|
|||||||
this.REDIS = configs.REDIS;
|
this.REDIS = configs.REDIS;
|
||||||
this.FIAT_PRICE = configs.FIAT_PRICE;
|
this.FIAT_PRICE = configs.FIAT_PRICE;
|
||||||
this.WALLETS = configs.WALLETS;
|
this.WALLETS = configs.WALLETS;
|
||||||
|
this.STRATUM = configs.STRATUM;
|
||||||
}
|
}
|
||||||
|
|
||||||
merge = (...objects: object[]): IConfig => {
|
merge = (...objects: object[]): IConfig => {
|
||||||
|
@ -48,6 +48,7 @@ import accelerationRoutes from './api/acceleration/acceleration.routes';
|
|||||||
import aboutRoutes from './api/about.routes';
|
import aboutRoutes from './api/about.routes';
|
||||||
import mempoolBlocks from './api/mempool-blocks';
|
import mempoolBlocks from './api/mempool-blocks';
|
||||||
import walletApi from './api/services/wallets';
|
import walletApi from './api/services/wallets';
|
||||||
|
import stratumApi from './api/services/stratum';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@ -320,6 +321,9 @@ class Server {
|
|||||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||||
|
|
||||||
accelerationApi.connectWebsocket();
|
accelerationApi.connectWebsocket();
|
||||||
|
if (config.STRATUM.ENABLED) {
|
||||||
|
stratumApi.connectWebsocket();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setUpHttpApiRoutes(): void {
|
setUpHttpApiRoutes(): void {
|
||||||
|
@ -148,6 +148,10 @@
|
|||||||
"API": "__MEMPOOL_SERVICES_API__",
|
"API": "__MEMPOOL_SERVICES_API__",
|
||||||
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
|
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
|
||||||
},
|
},
|
||||||
|
"STRATUM": {
|
||||||
|
"ENABLED": __STRATUM_ENABLED__,
|
||||||
|
"API": "__STRATUM_API__"
|
||||||
|
},
|
||||||
"REDIS": {
|
"REDIS": {
|
||||||
"ENABLED": __REDIS_ENABLED__,
|
"ENABLED": __REDIS_ENABLED__,
|
||||||
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
|
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
|
||||||
|
@ -149,6 +149,10 @@ __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
|||||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
|
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
|
||||||
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||||
|
|
||||||
|
# STRATUM
|
||||||
|
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
|
||||||
|
__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
|
||||||
|
|
||||||
# REDIS
|
# REDIS
|
||||||
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
|
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
|
||||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
|
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
|
||||||
@ -300,6 +304,10 @@ sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.j
|
|||||||
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
|
||||||
|
|
||||||
|
# STRATUM
|
||||||
|
sed -i "s!__STRATUM_ENABLED__!${__STRATUM_ENABLED__}!g" mempool-config.json
|
||||||
|
sed -i "s!__STRATUM_API__!${__STRATUM_API__}!g" mempool-config.json
|
||||||
|
|
||||||
# REDIS
|
# REDIS
|
||||||
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
||||||
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||||
|
@ -344,7 +344,9 @@ describe('Mainnet', () => {
|
|||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
|
|
||||||
cy.changeNetwork('testnet4');
|
//TODO(knorrium): add a check for the proxied server
|
||||||
|
// cy.changeNetwork('testnet4');
|
||||||
|
|
||||||
cy.changeNetwork('signet');
|
cy.changeNetwork('signet');
|
||||||
cy.changeNetwork('mainnet');
|
cy.changeNetwork('mainnet');
|
||||||
});
|
});
|
||||||
|
@ -27,5 +27,6 @@
|
|||||||
"ACCELERATOR": false,
|
"ACCELERATOR": false,
|
||||||
"ACCELERATOR_BUTTON": true,
|
"ACCELERATOR_BUTTON": true,
|
||||||
"PUBLIC_ACCELERATIONS": false,
|
"PUBLIC_ACCELERATIONS": false,
|
||||||
|
"STRATUM_ENABLED": false,
|
||||||
"SERVICES_API": "https://mempool.space/api/v1/services"
|
"SERVICES_API": "https://mempool.space/api/v1/services"
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,10 @@ const fs = require('fs');
|
|||||||
let PROXY_CONFIG = require('./proxy.conf');
|
let PROXY_CONFIG = require('./proxy.conf');
|
||||||
|
|
||||||
PROXY_CONFIG.forEach(entry => {
|
PROXY_CONFIG.forEach(entry => {
|
||||||
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
|
const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space';
|
||||||
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
|
console.log(`e2e tests running against ${hostname}`);
|
||||||
|
entry.target = entry.target.replace("mempool.space", hostname);
|
||||||
|
entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space");
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = PROXY_CONFIG;
|
module.exports = PROXY_CONFIG;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
|
<div class="box card w-100 accelerate-checkout-inner" [class.input-disabled]="isCheckoutLocked > 0" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
|
||||||
@if (accelerateError) {
|
@if (accelerateError) {
|
||||||
<div class="row mb-1 text-center">
|
<div class="row mb-1 text-center">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
@ -361,7 +361,7 @@
|
|||||||
<div class="row text-center justify-content-center mx-2">
|
<div class="row text-center justify-content-center mx-2">
|
||||||
<p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p>
|
<p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p>
|
||||||
</div>
|
</div>
|
||||||
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
|
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
||||||
<p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container> <small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></p>
|
<p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container> <small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></p>
|
||||||
@ -484,6 +484,11 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@if (isTokenizing > 0) {
|
||||||
|
<div class="d-flex flex-row justify-content-center">
|
||||||
|
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -8,6 +8,13 @@
|
|||||||
color: var(--green)
|
color: var(--green)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accelerate-checkout-inner {
|
||||||
|
&.input-disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.paymentMethod {
|
.paymentMethod {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: var(--secondary);
|
background-color: var(--secondary);
|
||||||
|
@ -76,6 +76,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
calculating = true;
|
calculating = true;
|
||||||
processing = false;
|
processing = false;
|
||||||
|
isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
|
||||||
|
isTokenizing = 0; // reference counter, 0 = false, >0 = true
|
||||||
selectedOption: 'wait' | 'accel';
|
selectedOption: 'wait' | 'accel';
|
||||||
cantPayReason = '';
|
cantPayReason = '';
|
||||||
quoteError = ''; // error fetching estimate or initial data
|
quoteError = ''; // error fetching estimate or initial data
|
||||||
@ -154,7 +156,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerateError = null;
|
this.accelerateError = null;
|
||||||
this.timePaid = 0;
|
this.timePaid = 0;
|
||||||
this.btcpayInvoiceFailed = false;
|
this.btcpayInvoiceFailed = false;
|
||||||
this.moveToStep('summary');
|
this.moveToStep('summary', true);
|
||||||
} else {
|
} else {
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
}
|
}
|
||||||
@ -163,11 +165,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
|
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
|
||||||
this.moveToStep('processing');
|
this.moveToStep('processing', true);
|
||||||
this.insertSquare();
|
this.insertSquare();
|
||||||
this.setupSquare();
|
this.setupSquare();
|
||||||
} else {
|
} else {
|
||||||
this.moveToStep('summary');
|
this.moveToStep('summary', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
@ -192,14 +194,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
if (changes.accelerating && this.accelerating) {
|
if (changes.accelerating && this.accelerating) {
|
||||||
if (this.step === 'processing' || this.step === 'paid') {
|
if (this.step === 'processing' || this.step === 'paid') {
|
||||||
this.moveToStep('success');
|
this.moveToStep('success', true);
|
||||||
} else { // Edge case where the transaction gets accelerated by someone else or on another session
|
} else { // Edge case where the transaction gets accelerated by someone else or on another session
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
moveToStep(step: CheckoutStep): void {
|
moveToStep(step: CheckoutStep, force: boolean = false): void {
|
||||||
|
if (this.isCheckoutLocked > 0 && !force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this._step = step;
|
this._step = step;
|
||||||
if (this.timeoutTimer) {
|
if (this.timeoutTimer) {
|
||||||
@ -242,7 +247,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
closeModal(): void {
|
closeModal(): void {
|
||||||
this.completed.emit(true);
|
this.completed.emit(true);
|
||||||
this.moveToStep('summary');
|
this.moveToStep('summary', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -393,7 +398,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
this.showSuccess = true;
|
this.showSuccess = true;
|
||||||
this.estimateSubscription.unsubscribe();
|
this.estimateSubscription.unsubscribe();
|
||||||
this.moveToStep('paid');
|
this.moveToStep('paid', true);
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
@ -503,56 +508,75 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.loadingApplePay = false;
|
this.loadingApplePay = false;
|
||||||
applePayButton.addEventListener('click', async event => {
|
applePayButton.addEventListener('click', async event => {
|
||||||
|
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const tokenResult = await this.applePay.tokenize();
|
try {
|
||||||
if (tokenResult?.status === 'OK') {
|
// lock the checkout UI and show a loading spinner until the square modals are finished
|
||||||
const card = tokenResult.details?.card;
|
this.isCheckoutLocked++;
|
||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
this.isTokenizing++;
|
||||||
console.error(`Cannot retreive payment card details`);
|
const tokenResult = await this.applePay.tokenize();
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
if (tokenResult?.status === 'OK') {
|
||||||
this.processing = false;
|
const card = tokenResult.details?.card;
|
||||||
return;
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
}
|
console.error(`Cannot retreive payment card details`);
|
||||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
this.servicesApiService.accelerateWithApplePay$(
|
|
||||||
this.tx.txid,
|
|
||||||
tokenResult.token,
|
|
||||||
cardTag,
|
|
||||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
|
||||||
costUSD
|
|
||||||
).subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
return;
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
|
||||||
if (this.applePay) {
|
|
||||||
this.applePay.destroy();
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
this.moveToStep('paid');
|
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
error: (response) => {
|
|
||||||
this.processing = false;
|
|
||||||
this.accelerateError = response.error;
|
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
|
||||||
setTimeout(() => {
|
|
||||||
// Reset everything by reloading the page :D, can be improved
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||||
} else {
|
// keep checkout in loading state until the acceleration request completes
|
||||||
this.processing = false;
|
this.isTokenizing++;
|
||||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
this.isCheckoutLocked++;
|
||||||
if (tokenResult.errors) {
|
this.servicesApiService.accelerateWithApplePay$(
|
||||||
errorMessage += ` and errors: ${JSON.stringify(
|
this.tx.txid,
|
||||||
tokenResult.errors,
|
tokenResult.token,
|
||||||
)}`;
|
cardTag,
|
||||||
|
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||||
|
costUSD
|
||||||
|
).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
|
if (this.applePay) {
|
||||||
|
this.applePay.destroy();
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
this.moveToStep('paid', true);
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
|
this.accelerateError = response.error;
|
||||||
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
// Reset everything by reloading the page :D, can be improved
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.processing = false;
|
||||||
|
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||||
|
if (tokenResult.errors) {
|
||||||
|
errorMessage += ` and errors: ${JSON.stringify(
|
||||||
|
tokenResult.errors,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
} finally {
|
||||||
|
// always unlock the checkout once we're finished
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -602,65 +626,84 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.loadingGooglePay = false;
|
this.loadingGooglePay = false;
|
||||||
|
|
||||||
document.getElementById('google-pay-button').addEventListener('click', async event => {
|
document.getElementById('google-pay-button').addEventListener('click', async event => {
|
||||||
|
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const tokenResult = await this.googlePay.tokenize();
|
try {
|
||||||
if (tokenResult?.status === 'OK') {
|
// lock the checkout UI and show a loading spinner until the square modals are finished
|
||||||
const card = tokenResult.details?.card;
|
this.isCheckoutLocked++;
|
||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
this.isTokenizing++;
|
||||||
console.error(`Cannot retreive payment card details`);
|
const tokenResult = await this.googlePay.tokenize();
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
if (tokenResult?.status === 'OK') {
|
||||||
this.processing = false;
|
const card = tokenResult.details?.card;
|
||||||
return;
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
}
|
console.error(`Cannot retreive payment card details`);
|
||||||
const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
if (!verificationToken || !verificationToken.token) {
|
|
||||||
console.error(`SCA verification failed`);
|
|
||||||
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
|
|
||||||
this.processing = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
|
||||||
this.servicesApiService.accelerateWithGooglePay$(
|
|
||||||
this.tx.txid,
|
|
||||||
tokenResult.token,
|
|
||||||
verificationToken.token,
|
|
||||||
cardTag,
|
|
||||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
|
||||||
costUSD,
|
|
||||||
verificationToken.userChallenged
|
|
||||||
).subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
return;
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
|
||||||
if (this.googlePay) {
|
|
||||||
this.googlePay.destroy();
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
this.moveToStep('paid');
|
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
error: (response) => {
|
|
||||||
this.processing = false;
|
|
||||||
this.accelerateError = response.error;
|
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
|
||||||
setTimeout(() => {
|
|
||||||
// Reset everything by reloading the page :D, can be improved
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
|
||||||
} else {
|
if (!verificationToken || !verificationToken.token) {
|
||||||
this.processing = false;
|
console.error(`SCA verification failed`);
|
||||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
|
||||||
if (tokenResult.errors) {
|
this.processing = false;
|
||||||
errorMessage += ` and errors: ${JSON.stringify(
|
return;
|
||||||
tokenResult.errors,
|
}
|
||||||
)}`;
|
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||||
|
// keep checkout in loading state until the acceleration request completes
|
||||||
|
this.isCheckoutLocked++;
|
||||||
|
this.isTokenizing++;
|
||||||
|
this.servicesApiService.accelerateWithGooglePay$(
|
||||||
|
this.tx.txid,
|
||||||
|
tokenResult.token,
|
||||||
|
verificationToken.token,
|
||||||
|
cardTag,
|
||||||
|
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||||
|
costUSD,
|
||||||
|
verificationToken.userChallenged
|
||||||
|
).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
|
if (this.googlePay) {
|
||||||
|
this.googlePay.destroy();
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
this.moveToStep('paid', true);
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
|
this.accelerateError = response.error;
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Reset everything by reloading the page :D, can be improved
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.processing = false;
|
||||||
|
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||||
|
if (tokenResult.errors) {
|
||||||
|
errorMessage += ` and errors: ${JSON.stringify(
|
||||||
|
tokenResult.errors,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
} finally {
|
||||||
|
// always unlock the checkout once we're finished
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -727,7 +770,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.cashAppPay.destroy();
|
this.cashAppPay.destroy();
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.moveToStep('paid');
|
this.moveToStep('paid', true);
|
||||||
if (window.history.replaceState) {
|
if (window.history.replaceState) {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
|
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
|
||||||
@ -801,7 +844,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
this.estimateSubscription.unsubscribe();
|
this.estimateSubscription.unsubscribe();
|
||||||
this.moveToStep('paid');
|
this.moveToStep('paid', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggedIn(): boolean {
|
isLoggedIn(): boolean {
|
||||||
|
@ -478,25 +478,30 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extendSummary(summary) {
|
extendSummary(summary) {
|
||||||
let extendedSummary = summary.slice();
|
const extendedSummary = summary.slice();
|
||||||
|
|
||||||
// Add a point at today's date to make the graph end at the current time
|
// Add a point at today's date to make the graph end at the current time
|
||||||
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
|
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
|
||||||
extendedSummary.reverse();
|
|
||||||
|
|
||||||
let oneHour = 60 * 60;
|
let maxTime = Date.now() / 1000;
|
||||||
|
|
||||||
|
const oneHour = 60 * 60;
|
||||||
// Fill gaps longer than interval
|
// Fill gaps longer than interval
|
||||||
for (let i = 0; i < extendedSummary.length - 1; i++) {
|
for (let i = 0; i < extendedSummary.length - 1; i++) {
|
||||||
let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);
|
if (extendedSummary[i].time > maxTime) {
|
||||||
|
extendedSummary[i].time = maxTime - 30;
|
||||||
|
}
|
||||||
|
maxTime = extendedSummary[i].time;
|
||||||
|
const hours = Math.floor((extendedSummary[i].time - extendedSummary[i + 1].time) / oneHour);
|
||||||
if (hours > 1) {
|
if (hours > 1) {
|
||||||
for (let j = 1; j < hours; j++) {
|
for (let j = 1; j < hours; j++) {
|
||||||
let newTime = extendedSummary[i].time + oneHour * j;
|
const newTime = extendedSummary[i].time - oneHour * j;
|
||||||
extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
|
extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
|
||||||
}
|
}
|
||||||
i += hours - 1;
|
i += hours - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return extendedSummary.reverse();
|
return extendedSummary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
|
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box pool-details">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
@ -173,7 +173,119 @@
|
|||||||
<div class="spinner-border text-light"></div>
|
<div class="spinner-border text-light"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Stratum Job -->
|
||||||
|
<ng-container *ngIf="(job$ | async) as job;">
|
||||||
|
<h2 i18n="pool.next_block">Next block</h2>
|
||||||
|
<div class="box mb-3">
|
||||||
|
<div class="row" >
|
||||||
|
<div class="col">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="job-table table table-xs table-borderless table-fixed table-data">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="data-title clip text-center height" i18n="latest-blocks.height">Height</th>
|
||||||
|
<th class="data-title clip text-center expected" i18n="next-block.expected-time">Expected</th>
|
||||||
|
<th class="data-title clip text-center reward" i18n="latest-blocks.reward">Reward</th>
|
||||||
|
<th class="data-title clip text-center timestamp" i18n="next-block.timestamp">Timestamp</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center height">
|
||||||
|
{{ job.height }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center expected">
|
||||||
|
<ng-container *ngIf="(expectedBlockTime$ | async) as expectedBlockTime; else expectedPlaceholder">
|
||||||
|
<app-time kind="until" [time]="expectedBlockTime" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #expectedPlaceholder>~</ng-template>
|
||||||
|
</td>
|
||||||
|
<td class="text-center reward">
|
||||||
|
<app-amount [satoshis]="job.reward"></app-amount>
|
||||||
|
</td>
|
||||||
|
<td class="text-center timestamp">
|
||||||
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.timestamp" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="job-table table table-xs table-borderless table-fixed table-data">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="data-title clip text-center coinbase" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
|
||||||
|
<th class="data-title clip text-center clean" i18n="next-block.clean">Clean</th>
|
||||||
|
<th class="data-title clip text-center prevhash" i18n="next-block.prevhash">Prevhash</th>
|
||||||
|
<th class="data-title clip text-center job-received" i18n="next-block.job-received">Job Received</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center coinbase">
|
||||||
|
{{ job.scriptsig | hex2ascii }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center clean">
|
||||||
|
@if (job.cleanJobs) {
|
||||||
|
<fa-icon [icon]="['fas', 'check-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
} @else {
|
||||||
|
<fa-icon [icon]="['fas', 'times-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="text-center prevhash">
|
||||||
|
<a [routerLink]="['/block' | relativeUrl, job.prevHash]">
|
||||||
|
<app-truncate [text]="job.prevHash" [lastChars]="8"></app-truncate>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-center job-received">
|
||||||
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.received / 1000" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="stratum-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="data-title clip text-center" [attr.colspan]="Math.max(job.merkleBranches.length, 12)">
|
||||||
|
<a class="title-link" href="" [routerLink]="['/stratum' | relativeUrl]">
|
||||||
|
Merkle Branches
|
||||||
|
<span> </span>
|
||||||
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
@for (branch of job.merkleBranches; track $index) {
|
||||||
|
<td class="merkle" [style.background-color]="branch ? '#' + branch.slice(0, 6) : ''"></td>
|
||||||
|
}
|
||||||
|
@for (_ of [].constructor(Math.max(0, 12 - job.merkleBranches.length)); track $index) {
|
||||||
|
<td class="merkle empty-branch"></td>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<!-- Blocks list -->
|
<!-- Blocks list -->
|
||||||
|
<h2 i18n="master-page.blocks">Blocks</h2>
|
||||||
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5"
|
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5"
|
||||||
[infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
|
[infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
|
||||||
<ng-container *ngIf="blocks$ | async as blocks; else skeleton">
|
<ng-container *ngIf="blocks$ | async as blocks; else skeleton">
|
||||||
|
@ -49,111 +49,110 @@ div.scrollable {
|
|||||||
max-height: 75px;
|
max-height: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.pool-details {
|
||||||
padding-bottom: 5px;
|
|
||||||
@media (min-width: 767.98px) {
|
@media (min-width: 767.98px) {
|
||||||
min-height: 187px;
|
min-height: 187px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
@media (min-width: 767.98px) {
|
@media (min-width: 767.98px) {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 767.98px) {
|
.label.addresses {
|
||||||
font-weight: bold;
|
vertical-align: top;
|
||||||
|
padding-top: 25px;
|
||||||
|
}
|
||||||
|
.addresses-data {
|
||||||
|
vertical-align: top;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.label.addresses {
|
|
||||||
vertical-align: top;
|
|
||||||
padding-top: 25px;
|
|
||||||
}
|
|
||||||
.addresses-data {
|
|
||||||
vertical-align: top;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data {
|
.data {
|
||||||
text-align: right;
|
|
||||||
padding-left: 5%;
|
|
||||||
@media (max-width: 992px) {
|
|
||||||
text-align: left;
|
|
||||||
padding-left: 12px;
|
|
||||||
}
|
|
||||||
@media (max-width: 450px) {
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
padding-left: 5%;
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
@media (max-width: 450px) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
background-color: var(--secondary);
|
background-color: var(--secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coinbase {
|
.coinbase {
|
||||||
width: 20%;
|
|
||||||
@media (max-width: 875px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.height {
|
|
||||||
width: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timestamp {
|
|
||||||
@media (max-width: 875px) {
|
|
||||||
padding-left: 50px;
|
|
||||||
}
|
|
||||||
@media (max-width: 685px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mined {
|
|
||||||
width: 13%;
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.txs {
|
|
||||||
padding-right: 40px;
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
@media (max-width: 875px) {
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
@media (max-width: 567px) {
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.size {
|
|
||||||
width: 12%;
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
width: 15%;
|
|
||||||
}
|
|
||||||
@media (max-width: 875px) {
|
|
||||||
width: 20%;
|
width: 20%;
|
||||||
|
@media (max-width: 875px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 650px) {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
@media (max-width: 450px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scriptmessage {
|
.height {
|
||||||
overflow: hidden;
|
width: 10%;
|
||||||
display: inline-block;
|
}
|
||||||
text-overflow: ellipsis;
|
|
||||||
vertical-align: middle;
|
.timestamp {
|
||||||
width: auto;
|
@media (max-width: 875px) {
|
||||||
text-align: left;
|
padding-left: 50px;
|
||||||
|
}
|
||||||
|
@media (max-width: 685px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mined {
|
||||||
|
width: 13%;
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.txs {
|
||||||
|
padding-right: 40px;
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
@media (max-width: 875px) {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
@media (max-width: 567px) {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.size {
|
||||||
|
width: 12%;
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
@media (max-width: 875px) {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
@media (max-width: 450px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scriptmessage {
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-loader {
|
.skeleton-loader {
|
||||||
@ -214,4 +213,55 @@ div.scrollable {
|
|||||||
|
|
||||||
.taller-row {
|
.taller-row {
|
||||||
height: 75px;
|
height: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stratum-table {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.merkle {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-branch {
|
||||||
|
outline: solid 1px white;
|
||||||
|
outline-offset: -1px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(to top left, transparent, transparent 48%, white 49%, white 51%, transparent 52%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
position: relative;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-table {
|
||||||
|
td, th {
|
||||||
|
width: 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
min-width: 25%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0.1rem 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.expected, .timestamp, .clean, .job-received {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
@ -10,6 +10,9 @@ import { selectPowerOfTen } from '@app/bitcoin.utils';
|
|||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { SeoService } from '@app/services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { StratumJob } from '../../interfaces/websocket.interface';
|
||||||
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
|
import { MiningService } from '../../services/mining.service';
|
||||||
|
|
||||||
interface AccelerationTotal {
|
interface AccelerationTotal {
|
||||||
cost: number,
|
cost: number,
|
||||||
@ -27,12 +30,16 @@ export class PoolComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
gfg = true;
|
gfg = true;
|
||||||
|
stratumEnabled = this.stateService.env.STRATUM_ENABLED;
|
||||||
|
|
||||||
formatNumber = formatNumber;
|
formatNumber = formatNumber;
|
||||||
|
Math = Math;
|
||||||
slugSubscription: Subscription;
|
slugSubscription: Subscription;
|
||||||
poolStats$: Observable<PoolStat>;
|
poolStats$: Observable<PoolStat>;
|
||||||
blocks$: Observable<BlockExtended[]>;
|
blocks$: Observable<BlockExtended[]>;
|
||||||
oobFees$: Observable<AccelerationTotal[]>;
|
oobFees$: Observable<AccelerationTotal[]>;
|
||||||
|
job$: Observable<StratumJob | null>;
|
||||||
|
expectedBlockTime$: Observable<number>;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
error: HttpErrorResponse | null = null;
|
error: HttpErrorResponse | null = null;
|
||||||
|
|
||||||
@ -53,6 +60,8 @@ export class PoolComponent implements OnInit {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
|
private websocketService: WebsocketService,
|
||||||
|
private miningService: MiningService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
) {
|
) {
|
||||||
this.auditAvailable = this.stateService.env.AUDIT;
|
this.auditAvailable = this.stateService.env.AUDIT;
|
||||||
@ -62,7 +71,7 @@ export class PoolComponent implements OnInit {
|
|||||||
this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
|
this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.blocks = [];
|
this.blocks = [];
|
||||||
this.chartOptions = {};
|
this.chartOptions = {};
|
||||||
this.slug = slug;
|
this.slug = slug;
|
||||||
this.initializeObservables();
|
this.initializeObservables();
|
||||||
});
|
});
|
||||||
@ -129,6 +138,31 @@ export class PoolComponent implements OnInit {
|
|||||||
}),
|
}),
|
||||||
filter(oob => oob.length === 3 && oob[2].count > 0)
|
filter(oob => oob.length === 3 && oob[2].count > 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.stratumEnabled) {
|
||||||
|
this.job$ = combineLatest([
|
||||||
|
this.poolStats$.pipe(
|
||||||
|
tap((poolStats) => {
|
||||||
|
this.websocketService.startTrackStratum(poolStats.pool.unique_id);
|
||||||
|
})
|
||||||
|
),
|
||||||
|
this.stateService.stratumJobs$
|
||||||
|
]).pipe(
|
||||||
|
map(([poolStats, jobs]) => {
|
||||||
|
return jobs[poolStats.pool.unique_id];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.expectedBlockTime$ = combineLatest([
|
||||||
|
this.miningService.getMiningStats('1w'),
|
||||||
|
this.poolStats$,
|
||||||
|
this.stateService.difficultyAdjustment$
|
||||||
|
]).pipe(
|
||||||
|
map(([miningStats, poolStat, da]) => {
|
||||||
|
return (da.timeAvg / ((poolStat.estimatedHashrate || 0) / (miningStats.lastEstimatedHashrate * 1_000_000_000_000_000_000))) + Date.now() + da.timeOffset;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareChartOptions(hashrate, share) {
|
prepareChartOptions(hashrate, share) {
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
<div class="container-xl" style="min-height: 335px">
|
||||||
|
<h1 class="float-left" i18n="master-page.blocks">Stratum Jobs</h1>
|
||||||
|
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
|
<div style="min-height: 295px">
|
||||||
|
<table *ngIf="poolsReady && (rows$ | async) as rows;" class="stratum-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td class="height">Height</td>
|
||||||
|
<td class="reward">Reward</td>
|
||||||
|
<td class="tag">Coinbase Tag</td>
|
||||||
|
<td class="merkle" [attr.colspan]="rows[0]?.merkleCells?.length || 4">
|
||||||
|
Merkle Branches
|
||||||
|
</td>
|
||||||
|
<td class="pool">Pool</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (row of rows; track row.job.pool) {
|
||||||
|
<tr>
|
||||||
|
<td class="height">
|
||||||
|
{{ row.job.height }}
|
||||||
|
</td>
|
||||||
|
<td class="reward">
|
||||||
|
<app-amount [satoshis]="row.job.reward"></app-amount>
|
||||||
|
</td>
|
||||||
|
<td class="tag">
|
||||||
|
{{ row.job.tag }}
|
||||||
|
</td>
|
||||||
|
@for (cell of row.merkleCells; track $index) {
|
||||||
|
<td class="merkle" [style.background-color]="cell.hash ? '#' + cell.hash.slice(0, 6) : ''">
|
||||||
|
<div class="pipe-segment" [class]="pipeToClass(cell.type)"></div>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
<td class="pool">
|
||||||
|
@if (pools[row.job.pool]) {
|
||||||
|
<a class="badge" [routerLink]="[('/mining/pool/' + pools[row.job.pool].slug) | relativeUrl]">
|
||||||
|
<img class="pool-logo" [src]="'/resources/mining-pools/' + pools[row.job.pool].slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pools[row.job.pool].name + ' mining pool'">
|
||||||
|
{{ pools[row.job.pool].name}}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,108 @@
|
|||||||
|
.stratum-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
position: relative;
|
||||||
|
height: 2em;
|
||||||
|
|
||||||
|
&.height, &.reward, &.tag {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag {
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pool {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.merkle {
|
||||||
|
width: 100px;
|
||||||
|
.pipe-segment {
|
||||||
|
position: absolute;
|
||||||
|
border-color: white;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
border-left: solid 4px;
|
||||||
|
}
|
||||||
|
&.horizontal {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
border-top: solid 4px;
|
||||||
|
}
|
||||||
|
&.branch-top {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
border-top: solid 4px;
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: content-box;
|
||||||
|
top: -4px;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 50%;
|
||||||
|
border-top: solid 4px;
|
||||||
|
border-left: solid 4px;
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.branch-mid {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0px;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
border-left: solid 4px;
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: content-box;
|
||||||
|
top: -4px;
|
||||||
|
left: -4px;
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
border-bottom: solid 4px;
|
||||||
|
border-left: solid 4px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.branch-end {
|
||||||
|
top: -4px;
|
||||||
|
right: 0;
|
||||||
|
width: 50%;
|
||||||
|
height: 50%;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
border-bottom: solid 4px;
|
||||||
|
border-left: solid 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: relative;
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-logo {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
@ -0,0 +1,224 @@
|
|||||||
|
import { Component, OnInit, ChangeDetectionStrategy, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||||
|
import { StateService } from '../../../services/state.service';
|
||||||
|
import { WebsocketService } from '../../../services/websocket.service';
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { StratumJob } from '../../../interfaces/websocket.interface';
|
||||||
|
import { MiningService } from '../../../services/mining.service';
|
||||||
|
import { SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||||
|
|
||||||
|
type MerkleCellType = ' ' | '┬' | '├' | '└' | '│' | '─' | 'leaf';
|
||||||
|
|
||||||
|
|
||||||
|
interface TaggedStratumJob extends StratumJob {
|
||||||
|
tag: string;
|
||||||
|
merkleBranchIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MerkleCell {
|
||||||
|
hash: string;
|
||||||
|
type: MerkleCellType;
|
||||||
|
job?: TaggedStratumJob;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MerkleTree {
|
||||||
|
hash?: string;
|
||||||
|
job: string;
|
||||||
|
size: number;
|
||||||
|
children?: MerkleTree[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PoolRow {
|
||||||
|
job: TaggedStratumJob;
|
||||||
|
merkleCells: MerkleCell[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTag(scriptSig: string): string {
|
||||||
|
const hex = scriptSig.slice(8).replace(/6d6d.{64}/, '');
|
||||||
|
const bytes: number[] = [];
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes.push(parseInt(hex.substr(i, 2), 16));
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const ascii = new TextDecoder('utf8').decode(Uint8Array.from(bytes)).replace(/\uFFFD/g, '').replace(/\\0/g, '').replace(/[\x00-\x1F\x7F-\x9F]/g, '');
|
||||||
|
if (ascii.includes('/ViaBTC/')) {
|
||||||
|
return '/ViaBTC/';
|
||||||
|
} else if (ascii.includes('SpiderPool/')) {
|
||||||
|
return 'SpiderPool/';
|
||||||
|
}
|
||||||
|
return (ascii.match(/\/.*\//)?.[0] || ascii).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMerkleBranchIds(merkleBranches: string[], numBranches: number): string[] {
|
||||||
|
let lastHash = '';
|
||||||
|
const ids: string[] = [];
|
||||||
|
for (let i = 0; i < numBranches; i++) {
|
||||||
|
if (merkleBranches[i]) {
|
||||||
|
lastHash = merkleBranches[i];
|
||||||
|
}
|
||||||
|
ids.push(`${i}-${lastHash}`);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-stratum-list',
|
||||||
|
templateUrl: './stratum-list.component.html',
|
||||||
|
styleUrls: ['./stratum-list.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class StratumList implements OnInit, OnDestroy {
|
||||||
|
rows$: Observable<PoolRow[]>;
|
||||||
|
pools: { [id: number]: SinglePoolStats } = {};
|
||||||
|
poolsReady: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private stateService: StateService,
|
||||||
|
private websocketService: WebsocketService,
|
||||||
|
private miningService: MiningService,
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.websocketService.want(['stats', 'blocks', 'mempool-blocks']);
|
||||||
|
this.miningService.getPools().subscribe(pools => {
|
||||||
|
this.pools = {};
|
||||||
|
for (const pool of pools) {
|
||||||
|
this.pools[pool.unique_id] = pool;
|
||||||
|
}
|
||||||
|
this.poolsReady = true;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
this.rows$ = this.stateService.stratumJobs$.pipe(
|
||||||
|
map((jobs) => this.processJobs(jobs)),
|
||||||
|
);
|
||||||
|
this.websocketService.startTrackStratum('all');
|
||||||
|
}
|
||||||
|
|
||||||
|
processJobs(rawJobs: Record<string, StratumJob>): PoolRow[] {
|
||||||
|
const numBranches = Math.max(...Object.values(rawJobs).map(job => job.merkleBranches.length));
|
||||||
|
const jobs: Record<string, TaggedStratumJob> = {};
|
||||||
|
for (const [id, job] of Object.entries(rawJobs)) {
|
||||||
|
jobs[id] = { ...job, tag: parseTag(job.scriptsig), merkleBranchIds: getMerkleBranchIds(job.merkleBranches, numBranches) };
|
||||||
|
}
|
||||||
|
if (Object.keys(jobs).length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let trees: MerkleTree[] = Object.keys(jobs).map(job => ({
|
||||||
|
job,
|
||||||
|
size: 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// build tree from bottom up
|
||||||
|
for (let col = numBranches - 1; col >= 0; col--) {
|
||||||
|
const groups: Record<string, MerkleTree[]> = {};
|
||||||
|
for (const tree of trees) {
|
||||||
|
const branchId = jobs[tree.job].merkleBranchIds[col];
|
||||||
|
if (!groups[branchId]) {
|
||||||
|
groups[branchId] = [];
|
||||||
|
}
|
||||||
|
groups[branchId].push(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
trees = Object.values(groups).map(group => ({
|
||||||
|
hash: jobs[group[0].job].merkleBranches[col],
|
||||||
|
job: group[0].job,
|
||||||
|
children: group,
|
||||||
|
size: group.reduce((acc, tree) => acc + tree.size, 0),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize grid of cells
|
||||||
|
const rows: (MerkleCell | null)[][] = [];
|
||||||
|
for (let i = 0; i < Object.keys(jobs).length; i++) {
|
||||||
|
const row: (MerkleCell | null)[] = [];
|
||||||
|
for (let j = 0; j <= numBranches; j++) {
|
||||||
|
row.push(null);
|
||||||
|
}
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill in the cells
|
||||||
|
let colTrees = [trees.sort((a, b) => {
|
||||||
|
if (a.size !== b.size) {
|
||||||
|
return b.size - a.size;
|
||||||
|
}
|
||||||
|
return a.job.localeCompare(b.job);
|
||||||
|
})];
|
||||||
|
for (let col = 0; col <= numBranches; col++) {
|
||||||
|
let row = 0;
|
||||||
|
const nextTrees: MerkleTree[][] = [];
|
||||||
|
for (let g = 0; g < colTrees.length; g++) {
|
||||||
|
for (let t = 0; t < colTrees[g].length; t++) {
|
||||||
|
const tree = colTrees[g][t];
|
||||||
|
const isFirstTree = (t === 0);
|
||||||
|
const isLastTree = (t === colTrees[g].length - 1);
|
||||||
|
for (let i = 0; i < tree.size; i++) {
|
||||||
|
const isFirstCell = (i === 0);
|
||||||
|
const isLeaf = (col === numBranches);
|
||||||
|
rows[row][col] = {
|
||||||
|
hash: tree.hash,
|
||||||
|
job: isLeaf ? jobs[tree.job] : undefined,
|
||||||
|
type: 'leaf',
|
||||||
|
};
|
||||||
|
if (col > 0) {
|
||||||
|
rows[row][col - 1].type = getCellType(isFirstCell, isFirstTree, isLastTree);
|
||||||
|
}
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
if (tree.children) {
|
||||||
|
nextTrees.push(tree.children.sort((a, b) => {
|
||||||
|
if (a.size !== b.size) {
|
||||||
|
return b.size - a.size;
|
||||||
|
}
|
||||||
|
return a.job.localeCompare(b.job);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colTrees = nextTrees;
|
||||||
|
}
|
||||||
|
return rows.map(row => ({
|
||||||
|
job: row[row.length - 1].job,
|
||||||
|
merkleCells: row.slice(0, -1),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeToClass(type: MerkleCellType): string {
|
||||||
|
return {
|
||||||
|
' ': 'empty',
|
||||||
|
'┬': 'branch-top',
|
||||||
|
'├': 'branch-mid',
|
||||||
|
'└': 'branch-end',
|
||||||
|
'│': 'vertical',
|
||||||
|
'─': 'horizontal',
|
||||||
|
'leaf': 'leaf'
|
||||||
|
}[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.websocketService.stopTrackStratum();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCellType(isFirstCell, isFirstTree, isLastTree): MerkleCellType {
|
||||||
|
if (isFirstCell) {
|
||||||
|
if (isFirstTree) {
|
||||||
|
if (isLastTree) {
|
||||||
|
return '─';
|
||||||
|
} else {
|
||||||
|
return '┬';
|
||||||
|
}
|
||||||
|
} else if (isLastTree) {
|
||||||
|
return '└';
|
||||||
|
} else {
|
||||||
|
return '├';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isLastTree) {
|
||||||
|
return ' ';
|
||||||
|
} else {
|
||||||
|
return '│';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@
|
|||||||
[height]="tx?.status?.block_height"
|
[height]="tx?.status?.block_height"
|
||||||
[replaced]="replaced"
|
[replaced]="replaced"
|
||||||
[removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed"
|
[removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed"
|
||||||
|
[cached]="isCached"
|
||||||
></app-confirmations>
|
></app-confirmations>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -21,6 +21,8 @@ export interface WebsocketResponse {
|
|||||||
rbfInfo?: RbfTree;
|
rbfInfo?: RbfTree;
|
||||||
rbfLatest?: RbfTree[];
|
rbfLatest?: RbfTree[];
|
||||||
rbfLatestSummary?: ReplacementInfo[];
|
rbfLatestSummary?: ReplacementInfo[];
|
||||||
|
stratumJob?: StratumJob;
|
||||||
|
stratumJobs?: Record<number, StratumJob>;
|
||||||
utxoSpent?: object;
|
utxoSpent?: object;
|
||||||
transactions?: TransactionStripped[];
|
transactions?: TransactionStripped[];
|
||||||
loadingIndicators?: ILoadingIndicators;
|
loadingIndicators?: ILoadingIndicators;
|
||||||
@ -37,6 +39,7 @@ export interface WebsocketResponse {
|
|||||||
'track-rbf-summary'?: boolean;
|
'track-rbf-summary'?: boolean;
|
||||||
'track-accelerations'?: boolean;
|
'track-accelerations'?: boolean;
|
||||||
'track-wallet'?: string;
|
'track-wallet'?: string;
|
||||||
|
'track-stratum'?: string | number;
|
||||||
'watch-mempool'?: boolean;
|
'watch-mempool'?: boolean;
|
||||||
'refresh-blocks'?: boolean;
|
'refresh-blocks'?: boolean;
|
||||||
}
|
}
|
||||||
@ -150,3 +153,24 @@ export interface HealthCheckHost {
|
|||||||
electrs?: string;
|
electrs?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StratumJob {
|
||||||
|
pool: number;
|
||||||
|
height: number;
|
||||||
|
coinbase: string;
|
||||||
|
scriptsig: string;
|
||||||
|
reward: number;
|
||||||
|
jobId: string;
|
||||||
|
extraNonce: string;
|
||||||
|
extraNonce2Size: number;
|
||||||
|
prevHash: string;
|
||||||
|
coinbase1: string;
|
||||||
|
coinbase2: string;
|
||||||
|
merkleBranches: string[];
|
||||||
|
version: string;
|
||||||
|
bits: string;
|
||||||
|
time: string;
|
||||||
|
timestamp: number;
|
||||||
|
cleanJobs: boolean;
|
||||||
|
received: number;
|
||||||
|
}
|
||||||
|
@ -10,9 +10,10 @@ import { TestTransactionsComponent } from '@components/test-transactions/test-tr
|
|||||||
import { CalculatorComponent } from '@components/calculator/calculator.component';
|
import { CalculatorComponent } from '@components/calculator/calculator.component';
|
||||||
import { BlocksList } from '@components/blocks-list/blocks-list.component';
|
import { BlocksList } from '@components/blocks-list/blocks-list.component';
|
||||||
import { RbfList } from '@components/rbf-list/rbf-list.component';
|
import { RbfList } from '@components/rbf-list/rbf-list.component';
|
||||||
|
import { StratumList } from '@components/stratum/stratum-list/stratum-list.component';
|
||||||
import { ServerHealthComponent } from '@components/server-health/server-health.component';
|
import { ServerHealthComponent } from '@components/server-health/server-health.component';
|
||||||
import { ServerStatusComponent } from '@components/server-health/server-status.component';
|
import { ServerStatusComponent } from '@components/server-health/server-status.component';
|
||||||
import { FaucetComponent } from '@components/faucet/faucet.component'
|
import { FaucetComponent } from '@components/faucet/faucet.component';
|
||||||
|
|
||||||
const browserWindow = window || {};
|
const browserWindow = window || {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -56,6 +57,16 @@ const routes: Routes = [
|
|||||||
path: 'rbf',
|
path: 'rbf',
|
||||||
component: RbfList,
|
component: RbfList,
|
||||||
},
|
},
|
||||||
|
...(browserWindowEnv.STRATUM_ENABLED ? [{
|
||||||
|
path: 'stratum',
|
||||||
|
component: StartComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: StratumList,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}] : []),
|
||||||
{
|
{
|
||||||
path: 'terms-of-service',
|
path: 'terms-of-service',
|
||||||
loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),
|
loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),
|
||||||
|
@ -64,8 +64,8 @@ export class MiningService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get names and slugs of all pools
|
* Get names and slugs of all pools
|
||||||
*/
|
*/
|
||||||
public getPools(): Observable<any[]> {
|
public getPools(): Observable<any[]> {
|
||||||
@ -75,7 +75,6 @@ export class MiningService {
|
|||||||
return this.poolsData;
|
return this.poolsData;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Set the hashrate power of ten we want to display
|
* Set the hashrate power of ten we want to display
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
||||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
|
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
|
||||||
import { AddressTxSummary, Transaction } from '@interfaces/electrs.interface';
|
import { Transaction } from '@interfaces/electrs.interface';
|
||||||
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '@interfaces/websocket.interface';
|
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, StratumJob, isMempoolState } from '@interfaces/websocket.interface';
|
||||||
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '@interfaces/node-api.interface';
|
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '@interfaces/node-api.interface';
|
||||||
import { Router, NavigationStart } from '@angular/router';
|
import { Router, NavigationStart } from '@angular/router';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
@ -81,6 +81,7 @@ export interface Env {
|
|||||||
ADDITIONAL_CURRENCIES: boolean;
|
ADDITIONAL_CURRENCIES: boolean;
|
||||||
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
|
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
|
||||||
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
|
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
|
||||||
|
STRATUM_ENABLED: boolean;
|
||||||
SERVICES_API?: string;
|
SERVICES_API?: string;
|
||||||
customize?: Customization;
|
customize?: Customization;
|
||||||
PROD_DOMAINS: string[];
|
PROD_DOMAINS: string[];
|
||||||
@ -123,6 +124,7 @@ const defaultEnv: Env = {
|
|||||||
'ACCELERATOR_BUTTON': true,
|
'ACCELERATOR_BUTTON': true,
|
||||||
'PUBLIC_ACCELERATIONS': false,
|
'PUBLIC_ACCELERATIONS': false,
|
||||||
'ADDITIONAL_CURRENCIES': false,
|
'ADDITIONAL_CURRENCIES': false,
|
||||||
|
'STRATUM_ENABLED': false,
|
||||||
'SERVICES_API': 'https://mempool.space/api/v1/services',
|
'SERVICES_API': 'https://mempool.space/api/v1/services',
|
||||||
'PROD_DOMAINS': [],
|
'PROD_DOMAINS': [],
|
||||||
};
|
};
|
||||||
@ -159,6 +161,8 @@ export class StateService {
|
|||||||
liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
|
liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
|
||||||
accelerations$ = new Subject<AccelerationDelta>();
|
accelerations$ = new Subject<AccelerationDelta>();
|
||||||
liveAccelerations$: Observable<Acceleration[]>;
|
liveAccelerations$: Observable<Acceleration[]>;
|
||||||
|
stratumJobUpdate$ = new Subject<{ state: Record<string, StratumJob> } | { job: StratumJob }>();
|
||||||
|
stratumJobs$ = new BehaviorSubject<Record<string, StratumJob>>({});
|
||||||
txConfirmed$ = new Subject<[string, BlockExtended]>();
|
txConfirmed$ = new Subject<[string, BlockExtended]>();
|
||||||
txReplaced$ = new Subject<ReplacedTransaction>();
|
txReplaced$ = new Subject<ReplacedTransaction>();
|
||||||
txRbfInfo$ = new Subject<RbfTree>();
|
txRbfInfo$ = new Subject<RbfTree>();
|
||||||
@ -303,6 +307,24 @@ export class StateService {
|
|||||||
map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added))
|
map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.stratumJobUpdate$.pipe(
|
||||||
|
scan((acc: Record<string, StratumJob>, update: { state: Record<string, StratumJob> } | { job: StratumJob }) => {
|
||||||
|
if ('state' in update) {
|
||||||
|
// Replace the entire state
|
||||||
|
return update.state;
|
||||||
|
} else {
|
||||||
|
// Update or create a single job entry
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[update.job.pool]: update.job
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, {}),
|
||||||
|
shareReplay(1)
|
||||||
|
).subscribe(val => {
|
||||||
|
this.stratumJobs$.next(val);
|
||||||
|
});
|
||||||
|
|
||||||
this.networkChanged$.subscribe((network) => {
|
this.networkChanged$.subscribe((network) => {
|
||||||
this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
|
this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
|
||||||
this.blocksSubject$.next([]);
|
this.blocksSubject$.next([]);
|
||||||
|
@ -36,6 +36,7 @@ export class WebsocketService {
|
|||||||
private isTrackingAccelerations: boolean = false;
|
private isTrackingAccelerations: boolean = false;
|
||||||
private isTrackingWallet: boolean = false;
|
private isTrackingWallet: boolean = false;
|
||||||
private trackingWalletName: string;
|
private trackingWalletName: string;
|
||||||
|
private isTrackingStratum: string | number | false = false;
|
||||||
private trackingMempoolBlock: number;
|
private trackingMempoolBlock: number;
|
||||||
private trackingMempoolBlockNetwork: string;
|
private trackingMempoolBlockNetwork: string;
|
||||||
private stoppingTrackMempoolBlock: any | null = null;
|
private stoppingTrackMempoolBlock: any | null = null;
|
||||||
@ -143,6 +144,9 @@ export class WebsocketService {
|
|||||||
if (this.isTrackingWallet) {
|
if (this.isTrackingWallet) {
|
||||||
this.startTrackingWallet(this.trackingWalletName);
|
this.startTrackingWallet(this.trackingWalletName);
|
||||||
}
|
}
|
||||||
|
if (this.isTrackingStratum !== false) {
|
||||||
|
this.startTrackStratum(this.isTrackingStratum);
|
||||||
|
}
|
||||||
this.stateService.connectionState$.next(2);
|
this.stateService.connectionState$.next(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,6 +293,18 @@ export class WebsocketService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startTrackStratum(pool: number | string) {
|
||||||
|
this.websocketSubject.next({ 'track-stratum': pool });
|
||||||
|
this.isTrackingStratum = pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTrackStratum() {
|
||||||
|
if (this.isTrackingStratum) {
|
||||||
|
this.websocketSubject.next({ 'track-stratum': null });
|
||||||
|
this.isTrackingStratum = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fetchStatistics(historicalDate: string) {
|
fetchStatistics(historicalDate: string) {
|
||||||
this.websocketSubject.next({ historicalDate });
|
this.websocketSubject.next({ historicalDate });
|
||||||
}
|
}
|
||||||
@ -512,6 +528,14 @@ export class WebsocketService {
|
|||||||
this.stateService.previousRetarget$.next(response.previousRetarget);
|
this.stateService.previousRetarget$.next(response.previousRetarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.stratumJobs) {
|
||||||
|
this.stateService.stratumJobUpdate$.next({ state: response.stratumJobs });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.stratumJob) {
|
||||||
|
this.stateService.stratumJobUpdate$.next({ job: response.stratumJob });
|
||||||
|
}
|
||||||
|
|
||||||
if (response['tomahawk']) {
|
if (response['tomahawk']) {
|
||||||
this.stateService.serverHealth$.next(response['tomahawk']);
|
this.stateService.serverHealth$.next(response['tomahawk']);
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,9 @@
|
|||||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
|
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
|
||||||
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
|
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && removed">
|
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && (removed || cached)">
|
||||||
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
|
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !removed">
|
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !(removed || cached)">
|
||||||
<button type="button" class="btn btn-sm btn-danger no-cursor {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
<button type="button" class="btn btn-sm btn-danger no-cursor {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
||||||
</ng-template>
|
</ng-template>
|
@ -12,6 +12,7 @@ export class ConfirmationsComponent implements OnChanges {
|
|||||||
@Input() height: number;
|
@Input() height: number;
|
||||||
@Input() replaced: boolean = false;
|
@Input() replaced: boolean = false;
|
||||||
@Input() removed: boolean = false;
|
@Input() removed: boolean = false;
|
||||||
|
@Input() cached: boolean = false;
|
||||||
@Input() hideUnconfirmed: boolean = false;
|
@Input() hideUnconfirmed: boolean = false;
|
||||||
@Input() buttonClass: string = '';
|
@Input() buttonClass: string = '';
|
||||||
|
|
||||||
|
@ -4,7 +4,10 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
|
|||||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||||
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
|
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
|
||||||
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
|
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
|
||||||
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck, faMoneyBillTrendUp } from '@fortawesome/free-solid-svg-icons';
|
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft,
|
||||||
|
faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
|
||||||
|
faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline,
|
||||||
|
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||||
import { MenuComponent } from '@components/menu/menu.component';
|
import { MenuComponent } from '@components/menu/menu.component';
|
||||||
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
|
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
|
||||||
@ -80,6 +83,7 @@ import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
|
|||||||
import { DifficultyAdjustmentsTable } from '@components/difficulty-adjustments-table/difficulty-adjustments-table.components';
|
import { DifficultyAdjustmentsTable } from '@components/difficulty-adjustments-table/difficulty-adjustments-table.components';
|
||||||
import { BlocksList } from '@components/blocks-list/blocks-list.component';
|
import { BlocksList } from '@components/blocks-list/blocks-list.component';
|
||||||
import { RbfList } from '@components/rbf-list/rbf-list.component';
|
import { RbfList } from '@components/rbf-list/rbf-list.component';
|
||||||
|
import { StratumList } from '@components/stratum/stratum-list/stratum-list.component';
|
||||||
import { RewardStatsComponent } from '@components/reward-stats/reward-stats.component';
|
import { RewardStatsComponent } from '@components/reward-stats/reward-stats.component';
|
||||||
import { DataCyDirective } from '@app/data-cy.directive';
|
import { DataCyDirective } from '@app/data-cy.directive';
|
||||||
import { LoadingIndicatorComponent } from '@components/loading-indicator/loading-indicator.component';
|
import { LoadingIndicatorComponent } from '@components/loading-indicator/loading-indicator.component';
|
||||||
@ -198,6 +202,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
|
|||||||
DifficultyAdjustmentsTable,
|
DifficultyAdjustmentsTable,
|
||||||
BlocksList,
|
BlocksList,
|
||||||
RbfList,
|
RbfList,
|
||||||
|
StratumList,
|
||||||
DataCyDirective,
|
DataCyDirective,
|
||||||
RewardStatsComponent,
|
RewardStatsComponent,
|
||||||
LoadingIndicatorComponent,
|
LoadingIndicatorComponent,
|
||||||
@ -342,6 +347,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
|
|||||||
AmountShortenerPipe,
|
AmountShortenerPipe,
|
||||||
DifficultyAdjustmentsTable,
|
DifficultyAdjustmentsTable,
|
||||||
BlocksList,
|
BlocksList,
|
||||||
|
StratumList,
|
||||||
DataCyDirective,
|
DataCyDirective,
|
||||||
RewardStatsComponent,
|
RewardStatsComponent,
|
||||||
LoadingIndicatorComponent,
|
LoadingIndicatorComponent,
|
||||||
@ -452,5 +458,7 @@ export class SharedModule {
|
|||||||
library.addIcons(faCircleXmark);
|
library.addIcons(faCircleXmark);
|
||||||
library.addIcons(faCalendarCheck);
|
library.addIcons(faCalendarCheck);
|
||||||
library.addIcons(faMoneyBillTrendUp);
|
library.addIcons(faMoneyBillTrendUp);
|
||||||
|
library.addIcons(faRobot);
|
||||||
|
library.addIcons(faShareNodes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user