Compare commits

..
Author SHA1 Message Date
Mononaut 283fd1c58e add sp widget to custom dashboard 2024-12-30 17:22:25 +00:00
76 changed files with 830 additions and 4385 deletions

No files matched your search

+14 -58
View File
@@ -251,7 +251,17 @@ jobs:
strategy:
fail-fast: false
matrix:
module: ["mempool", "liquid", "testnet4"]
module: ["mempool", "liquid"]
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 }}
steps:
@@ -300,10 +310,8 @@ jobs:
- name: Unzip assets before building (src/resources)
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
# mempool
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'mempool' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
@@ -314,9 +322,7 @@ jobs:
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
spec: ${{ matrix.spec }}
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
@@ -326,56 +332,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest"
@@ -403,4 +359,4 @@ jobs:
- name: Validate JSON syntax
run: |
cat mempool-config.json | jq
working-directory: docker/docker/backend
working-directory: docker/docker/backend
+1 -1
View File
@@ -7,7 +7,7 @@ const config: Config.InitialOptions = {
automock: false,
collectCoverage: true,
collectCoverageFrom: ["./src/**/**.ts"],
coverageProvider: "v8",
coverageProvider: "babel",
coverageThreshold: {
global: {
lines: 1
-4
View File
@@ -155,10 +155,6 @@
"API": "https://mempool.space/api/v1/services",
"ACCELERATIONS": false
},
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
},
"FIAT_PRICE": {
"ENABLED": true,
"PAID": false,
+25 -30
View File
@@ -10,6 +10,7 @@
"hasInstallScript": true,
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.7.2",
@@ -17,7 +18,7 @@
"crypto-js": "~4.2.0",
"express": "~4.21.1",
"maxmind": "~4.3.11",
"mysql2": "~3.12.0",
"mysql2": "~3.11.0",
"redis": "^4.7.0",
"rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0",
@@ -25,6 +26,8 @@
"ws": "~8.18.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.25.2",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",
@@ -5997,21 +6000,6 @@
"yallist": "^3.0.2"
}
},
"node_modules/lru.min": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz",
"integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -6173,17 +6161,16 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql2": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
"license": "MIT",
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz",
"integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"long": "^5.2.1",
"lru.min": "^1.0.0",
"lru-cache": "^8.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
@@ -6203,6 +6190,14 @@
"node": ">=0.10.0"
}
},
"node_modules/mysql2/node_modules/lru-cache": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
"engines": {
"node": ">=16.14"
}
},
"node_modules/named-placeholders": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
@@ -12218,11 +12213,6 @@
"yallist": "^3.0.2"
}
},
"lru.min": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz",
"integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q=="
},
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -12337,16 +12327,16 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql2": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz",
"integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==",
"requires": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"long": "^5.2.1",
"lru.min": "^1.0.0",
"lru-cache": "^8.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
@@ -12359,6 +12349,11 @@
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
"lru-cache": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA=="
}
}
},
+4 -1
View File
@@ -39,6 +39,7 @@
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
},
"dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.7.2",
@@ -46,7 +47,7 @@
"crypto-js": "~4.2.0",
"express": "~4.21.1",
"maxmind": "~4.3.11",
"mysql2": "~3.12.0",
"mysql2": "~3.11.0",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.7.0",
"socks-proxy-agent": "~7.0.0",
@@ -54,6 +55,8 @@
"ws": "~8.18.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.25.2",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",
@@ -151,9 +151,5 @@
"ENABLED": true,
"PAID": false,
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
},
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
}
}
-5
View File
@@ -159,11 +159,6 @@ describe('Mempool Backend Config', () => {
PAID: false,
API_KEY: '',
});
expect(config.STRATUM).toStrictEqual({
ENABLED: false,
API: 'http://localhost:1234',
});
});
});
+1 -5
View File
@@ -119,11 +119,7 @@ class RbfCache {
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if ( !newTxExtended
|| !replaced?.length
|| this.txs.has(newTxExtended.txid)
|| !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
) {
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
return;
}
-105
View File
@@ -1,105 +0,0 @@
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();
-28
View File
@@ -38,7 +38,6 @@ interface AddressTransactions {
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { calculateMempoolTxCpfp } from './cpfp';
import { getRecentFirstSeen } from '../utils/file-read';
import stratumApi, { StratumJob } from './services/stratum';
// valid 'want' subscriptions
const wantable = [
@@ -404,16 +403,6 @@ class WebsocketHandler {
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) {
client.send(this.serializeResponse(response));
}
@@ -1395,23 +1384,6 @@ class WebsocketHandler {
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
// and zips it together into a valid JSON object
private serializeResponse(response): string {
-10
View File
@@ -165,10 +165,6 @@ interface IConfig {
WALLETS: {
ENABLED: boolean;
WALLETS: string[];
},
STRATUM: {
ENABLED: boolean;
API: string;
}
}
@@ -336,10 +332,6 @@ const defaults: IConfig = {
'ENABLED': false,
'WALLETS': [],
},
'STRATUM': {
'ENABLED': false,
'API': 'http://localhost:1234',
}
};
class Config implements IConfig {
@@ -362,7 +354,6 @@ class Config implements IConfig {
REDIS: IConfig['REDIS'];
FIAT_PRICE: IConfig['FIAT_PRICE'];
WALLETS: IConfig['WALLETS'];
STRATUM: IConfig['STRATUM'];
constructor() {
const configs = this.merge(configFromFile, defaults);
@@ -385,7 +376,6 @@ class Config implements IConfig {
this.REDIS = configs.REDIS;
this.FIAT_PRICE = configs.FIAT_PRICE;
this.WALLETS = configs.WALLETS;
this.STRATUM = configs.STRATUM;
}
merge = (...objects: object[]): IConfig => {
-4
View File
@@ -48,7 +48,6 @@ import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks';
import walletApi from './api/services/wallets';
import stratumApi from './api/services/stratum';
class Server {
private wss: WebSocket.Server | undefined;
@@ -321,9 +320,6 @@ class Server {
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
accelerationApi.connectWebsocket();
if (config.STRATUM.ENABLED) {
stratumApi.connectWebsocket();
}
}
setUpHttpApiRoutes(): void {
-4
View File
@@ -148,10 +148,6 @@
"API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
},
"STRATUM": {
"ENABLED": __STRATUM_ENABLED__,
"API": "__STRATUM_API__"
},
"REDIS": {
"ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
-8
View File
@@ -149,10 +149,6 @@ __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# STRATUM
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
# REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
@@ -304,10 +300,6 @@ 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_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
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
-2
View File
@@ -45,7 +45,6 @@ __SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
# Export as environment variables to be used by envsubst
export __MAINNET_ENABLED__
@@ -77,7 +76,6 @@ export __SERVICES_API__
export __PUBLIC_ACCELERATIONS__
export __HISTORICAL_PRICE__
export __ADDITIONAL_CURRENCIES__
export __STRATUM_ENABLED__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
echo ${folder}
+6 -1
View File
@@ -38,7 +38,12 @@
}
},
{
"component": "blocks"
"component": "simpleproof",
"mobileOrder": 6,
"props": {
"label": "Executive Decrees",
"key": "el_salvador_decretos"
}
},
{
"component": "addressTransactions",
+1 -3
View File
@@ -344,9 +344,7 @@ describe('Mainnet', () => {
cy.visit('/');
cy.waitForSkeletonGone();
//TODO(knorrium): add a check for the proxied server
// cy.changeNetwork('testnet4');
cy.changeNetwork('testnet4');
cy.changeNetwork('signet');
cy.changeNetwork('mainnet');
});
@@ -27,6 +27,5 @@
"ACCELERATOR": false,
"ACCELERATOR_BUTTON": true,
"PUBLIC_ACCELERATIONS": false,
"STRATUM_ENABLED": false,
"SERVICES_API": "https://mempool.space/api/v1/services"
}
+15 -15
View File
@@ -33,7 +33,7 @@
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.6.0",
"echarts": "~5.5.0",
"esbuild": "^0.24.0",
"ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0",
@@ -8724,12 +8724,12 @@
}
},
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
"integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
"zrender": "5.5.0"
}
},
"node_modules/echarts/node_modules/tslib": {
@@ -18366,9 +18366,9 @@
}
},
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
"integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
"dependencies": {
"tslib": "2.3.0"
}
@@ -24485,12 +24485,12 @@
}
},
"echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
"integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
"requires": {
"tslib": "2.3.0",
"zrender": "5.6.1"
"zrender": "5.5.0"
},
"dependencies": {
"tslib": {
@@ -31485,9 +31485,9 @@
}
},
"zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
"integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
"requires": {
"tslib": "2.3.0"
},
+1 -1
View File
@@ -86,7 +86,7 @@
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.6.0",
"echarts": "~5.5.0",
"ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",
+2 -4
View File
@@ -3,10 +3,8 @@ const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => {
const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.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");
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
});
module.exports = PROXY_CONFIG;
@@ -1,18 +1,10 @@
<div class="box card w-100 accelerate-checkout-inner" [class.input-disabled]="isCheckoutLocked > 0" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
<div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
@if (accelerateError) {
@if (accelerateError.includes('Payment declined')) {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">{{ accelerateError }}</h1>
</div>
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
</div>
} @else {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
</div>
</div>
}
</div>
<div class="row text-center mt-1">
<div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center">
@@ -369,7 +361,7 @@
<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>
</div>
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
<div class="row">
<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>&nbsp;<small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></p>
@@ -397,13 +389,13 @@
</div>
}
</div>
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
<p class="text-nowrap">&mdash;<span i18n="or">OR</span>&mdash;</p>
</div>
}
}
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<app-fiat [value]="cost"></app-fiat> with</p>
@if (canPayWithCashapp) {
@@ -421,13 +413,6 @@
<img src="/resources/google-pay.png" height=37>
</div>
}
@if (canPayWithCardOnFile) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { <span class="mt-1 mb-1"></span> }
<div class="paymentMethod mx-2 d-flex justify-content-center align-items-center" style="width: 200px; height: 55px" (click)="moveToStep('cardonfile')">
<fa-icon style="font-size: 24px; color: white" [icon]="['fas', 'credit-card']"></fa-icon>
<span class="ml-2" style="font-size: 22px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
</div>
}
</div>
}
</div>
@@ -450,7 +435,7 @@
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
</div>
</div>
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') {
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
<!-- Show checkout page -->
<div class="row mb-md-1 text-center" id="confirm-title">
<div class="col-sm" id="confirm-payment-title">
@@ -466,7 +451,7 @@
</div>
</div>
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay || step === 'cardonfile' && !loadingCardOnFile) {
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
<div class="row text-center mt-1">
<div class="col-sm">
<div class="form-group w-100">
@@ -491,24 +476,14 @@
<div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'googlepay') {
<div id="google-pay-button" class="d-inline-block" style="height: 50px" [style]="loadingGooglePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'cardonfile') {
<div class="paymentMethod mx-2 d-flex justify-content-center align-items-center ml-auto mr-auto" style="width: 200px; height: 55px" (click)="requestCardOnFilePayment()" [style]="loadingCardOnFile ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''">
<fa-icon style="font-size: 24px; color: white" [icon]="['fas', 'credit-card']"></fa-icon>
<span class="ml-2" style="font-size: 22px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
</div>
}
@if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) {
@if (loadingCashapp || loadingApplePay || loadingGooglePay) {
<div display="d-flex flex-row justify-content-center">
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></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>
@@ -8,13 +8,6 @@
color: var(--green)
}
.accelerate-checkout-inner {
&.input-disabled {
pointer-events: none;
opacity: 0.75;
}
}
.paymentMethod {
padding: 10px;
background-color: var(--secondary);
@@ -13,7 +13,7 @@ import { EnterpriseService } from '@app/services/enterprise.service';
import { ApiService } from '@app/services/api.service';
import { isDevMode } from '@angular/core';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay' | 'cardOnFile';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
export type AccelerationEstimate = {
hasAccess: boolean;
@@ -26,7 +26,7 @@ export type AccelerationEstimate = {
mempoolBaseFee: number;
vsizeFee: number;
pools: number[];
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number, card?: {card_id: string, last_4: string, brand: string, name: string, billing: any}}>;
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
unavailable?: boolean;
options: { // recommended bid options
fee: number; // recommended userBid in sats
@@ -49,7 +49,7 @@ export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4;
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'cardonfile' | 'processing' | 'paid' | 'success';
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success';
@Component({
selector: 'app-accelerate-checkout',
@@ -65,7 +65,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() cashappEnabled: boolean = true;
@Input() applePayEnabled: boolean = false;
@Input() googlePayEnabled: boolean = true;
@Input() cardOnFileEnabled: boolean = true;
@Input() advancedEnabled: boolean = false;
@Input() forceMobile: boolean = false;
@Input() showDetails: boolean = false;
@@ -77,8 +76,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
calculating = true;
processing = false;
isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
isTokenizing = 0; // reference counter, 0 = false, >0 = true
selectedOption: 'wait' | 'accel';
cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data
@@ -118,7 +115,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
loadingCashapp = false;
loadingApplePay = false;
loadingGooglePay = false;
loadingCardOnFile = false;
payments: any;
cashAppPay: any;
applePay: any;
@@ -158,7 +154,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerateError = null;
this.timePaid = 0;
this.btcpayInvoiceFailed = false;
this.moveToStep('summary', true);
this.moveToStep('summary');
} else {
this.auth = auth;
}
@@ -167,11 +163,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
this.moveToStep('processing', true);
this.moveToStep('processing');
this.insertSquare();
this.setupSquare();
} else {
this.moveToStep('summary', true);
this.moveToStep('summary');
}
this.conversionsSubscription = this.stateService.conversions$.subscribe(
@@ -196,17 +192,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
if (changes.accelerating && this.accelerating) {
if (this.step === 'processing' || this.step === 'paid') {
this.moveToStep('success', true);
this.moveToStep('success');
} else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal();
}
}
}
moveToStep(step: CheckoutStep, force: boolean = false): void {
if (this.isCheckoutLocked > 0 && !force) {
return;
}
moveToStep(step: CheckoutStep): void {
this.processing = false;
this._step = step;
if (this.timeoutTimer) {
@@ -236,10 +229,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'cardonfile' && this.cardOnFileEnabled) {
this.loadingCardOnFile = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'paid') {
this.timePaid = Date.now();
this.timeoutTimer = setTimeout(() => {
@@ -253,7 +242,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
closeModal(): void {
this.completed.emit(true);
this.moveToStep('summary', true);
this.moveToStep('summary');
}
/**
@@ -404,7 +393,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
this.estimateSubscription.unsubscribe();
this.moveToStep('paid', true);
this.moveToStep('paid');
},
error: (response) => {
this.processing = false;
@@ -460,8 +449,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
await this.requestApplePayPayment();
} else if (this._step === 'googlepay') {
await this.requestGooglePayPayment();
} else if (this._step === 'cardonfile') {
this.loadingCardOnFile = false;
}
},
error: () => {
@@ -516,75 +503,56 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
this.loadingApplePay = false;
applePayButton.addEventListener('click', async event => {
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
return;
}
event.preventDefault();
try {
// lock the checkout UI and show a loading spinner until the square modals are finished
this.isCheckoutLocked++;
this.isTokenizing++;
const tokenResult = await this.applePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
// keep checkout in loading state until the acceleration request completes
this.isTokenizing++;
this.isCheckoutLocked++;
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.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')}`, ``));
}, 10000);
}
}
});
} else {
const tokenResult = await this.applePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
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);
return;
}
} finally {
// always unlock the checkout once we're finished
this.isTokenizing--;
this.isCheckoutLocked--;
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
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.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
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);
}
}
});
} 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);
}
});
} catch (e) {
@@ -634,193 +602,70 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = false;
document.getElementById('google-pay-button').addEventListener('click', async event => {
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
return;
}
event.preventDefault();
try {
// lock the checkout UI and show a loading spinner until the square modals are finished
this.isCheckoutLocked++;
this.isTokenizing++;
const tokenResult = await this.googlePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
}
const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
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());
// 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')}`, ``));
}, 10000);
}
}
});
} else {
const tokenResult = await this.googlePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
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);
return;
}
} finally {
// always unlock the checkout once we're finished
this.isTokenizing--;
this.isCheckoutLocked--;
const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
if (!verificationToken) {
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,
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.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);
}
}
});
} 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);
}
});
}
);
}
/**
* Card On File
*/
async requestCardOnFilePayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
const costUSD = this.cost / 100_000_000 * conversions.USD;
if (this.isCheckoutLocked > 0) {
return;
}
const cardOnFile = this.estimate?.availablePaymentMethods?.cardOnFile;
if (!cardOnFile?.card) {
this.accelerateError = 'card_on_file_not_found';
return;
}
this.loadingCardOnFile = false;
try {
this.isCheckoutLocked += 2;
this.isTokenizing += 2;
const nameParts = cardOnFile.card.name.split(' ');
const assumedGivenName = nameParts[0];
const assumedFamilyName = nameParts.length > 1 ? nameParts[1] : undefined;
const verificationDetails = {
card: {
billing: {
givenName: assumedGivenName,
familyName: assumedFamilyName,
addressLines: [cardOnFile.card.billing.addressLine1 ?? ''],
city: cardOnFile.card.billing.locality ?? '',
state: cardOnFile.card.billing.administrativeDistrictLevel1 ?? '',
countyCode: cardOnFile.card.billing.country,
}
}
};
const verificationToken = await this.$verifyBuyer(this.payments, cardOnFile.card.card_id, verificationDetails, costUSD.toFixed(2));
if (!verificationToken || !verificationToken.token) {
console.error(`SCA verification failed`);
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
this.processing = false;
return;
}
this.servicesApiService.accelerateWithCardOnFile$(
this.tx.txid,
cardOnFile.card.card_id,
verificationToken.token,
`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');
setTimeout(() => {
this.isCheckoutLocked--;
this.isTokenizing--;
this.moveToStep('paid', true);
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
this.isCheckoutLocked--;
this.isTokenizing--;
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);
}
}
});
} catch (e) {
console.log(e);
this.isCheckoutLocked--;
this.isTokenizing--;
this.processing = false;
this.accelerateError = e.message;
} finally {
// always unlock the checkout once we're finished
this.isCheckoutLocked--;
this.isTokenizing--;
}
}
);
}
/**
* CASHAPP
*/
@@ -881,7 +726,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.cashAppPay.destroy();
}
setTimeout(() => {
this.moveToStep('paid', true);
this.moveToStep('paid');
if (window.history.replaceState) {
const urlParams = new URLSearchParams(window.location.search);
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
@@ -896,7 +741,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
// 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')}`, ``));
}, 10000);
}, 3000);
}
}
});
@@ -907,9 +752,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
/**
* https://developer.squareup.com/docs/sca-overview
* Required in SCA Mandated Regions: Learn more at https://developer.squareup.com/docs/sca-overview
*/
async $verifyBuyer(payments, token, details, amount): Promise<{token: string, userChallenged: boolean}> {
async $verifyBuyer(payments, token, details, amount) {
const verificationDetails = {
amount: amount,
currencyCode: 'USD',
@@ -929,7 +774,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
token,
verificationDetails,
);
return verificationResults;
return verificationResults.token;
}
/**
@@ -955,7 +800,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
this.estimateSubscription.unsubscribe();
this.moveToStep('paid', true);
this.moveToStep('paid');
}
isLoggedIn(): boolean {
@@ -1066,22 +911,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return false;
}
get canPayWithCardOnFile(): boolean {
if (!this.cardOnFileEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
const paymentMethod = this.estimate?.availablePaymentMethods?.cardOnFile;
if (paymentMethod) {
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
return true;
}
}
return false;
}
get canPayWithBalance(): boolean {
if (!this.hasAccessToBalanceMode) {
return false;
@@ -46,8 +46,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
aggregatedHistory$: Observable<any>;
statsSubscription: Subscription;
aggregatedHistorySubscription: Subscription;
fragmentSubscription: Subscription;
isLoading = true;
formatNumber = formatNumber;
timespan = '';
@@ -81,8 +79,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
}
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
this.fragmentSubscription = this.route.fragment.subscribe((fragment) => {
this.route.fragment.subscribe((fragment) => {
if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
}
@@ -115,7 +113,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
share(),
);
this.aggregatedHistorySubscription = this.aggregatedHistory$.subscribe();
this.aggregatedHistory$.subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
@@ -337,8 +335,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
}
ngOnDestroy(): void {
this.aggregatedHistorySubscription?.unsubscribe();
this.fragmentSubscription?.unsubscribe();
this.statsSubscription?.unsubscribe();
if (this.statsSubscription) {
this.statsSubscription.unsubscribe();
}
}
}
@@ -14,7 +14,7 @@
<th class="time text-right" i18n="accelerator.requested">Requested</th>
</ng-container>
<ng-container *ngIf="!pending">
<th class="fee text-right text-truncate" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="block text-right" i18n="shared.block-title">Block</th>
<th class="pool text-right" i18n="mining.pool-name" *ngIf="!this.widget">Pool</th>
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
@@ -64,8 +64,7 @@
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;⌛</span></span>
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;⌛</span></span>
<span *ngIf="acceleration.status.includes('failed') && acceleration.canceled" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;⌛</span></span>
<span *ngIf="acceleration.status.includes('failed') && !acceleration.canceled" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;⌛</span></span>
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;⌛</span></span>
</td>
<td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
@@ -478,30 +478,25 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
}
extendSummary(summary) {
const extendedSummary = summary.slice();
let extendedSummary = summary.slice();
// Add a point at today's date to make the graph end at the current time
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
extendedSummary.reverse();
let maxTime = Date.now() / 1000;
const oneHour = 60 * 60;
let oneHour = 60 * 60;
// Fill gaps longer than interval
for (let i = 0; i < extendedSummary.length - 1; i++) {
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);
let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);
if (hours > 1) {
for (let j = 1; j < hours; j++) {
const newTime = extendedSummary[i].time - oneHour * j;
let newTime = extendedSummary[i].time + oneHour * j;
extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
}
i += hours - 1;
}
}
return extendedSummary;
return extendedSummary.reverse();
}
}
@@ -41,7 +41,7 @@ export class AppComponent implements OnInit {
@HostListener('document:keydown', ['$event'])
handleKeyboardEvents(event: KeyboardEvent) {
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
if (event.target instanceof HTMLInputElement) {
return;
}
// prevent arrow key horizontal scrolling
@@ -172,19 +172,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
ngOnDestroy(): void {
if (this.animationFrameRequest) {
cancelAnimationFrame(this.animationFrameRequest);
clearTimeout(this.animationHeartBeat);
}
clearTimeout(this.animationHeartBeat);
if (this.canvas) {
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
this.themeChangedSubscription?.unsubscribe();
}
if (this.scene) {
this.scene.destroy();
}
this.vertexArray.destroy();
this.vertexArray = null;
this.themeChangedSubscription?.unsubscribe();
this.searchSubscription?.unsubscribe();
}
clear(direction): void {
@@ -453,7 +447,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
this.applyQueuedUpdates();
// skip re-render if there's no change to the scene
if (this.scene && this.gl && this.vertexArray) {
if (this.scene && this.gl) {
/* SET UP SHADER UNIFORMS */
// screen dimensions
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
@@ -495,7 +489,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (this.running && this.scene && now <= (this.scene.animateUntil + 500)) {
this.doRun();
} else {
clearTimeout(this.animationHeartBeat);
if (this.animationHeartBeat) {
clearTimeout(this.animationHeartBeat);
}
this.animationHeartBeat = window.setTimeout(() => {
this.start();
}, 1000);
@@ -19,7 +19,6 @@ export class FastVertexArray {
freeSlots: number[];
lastSlot: number;
dirty = false;
destroyed = false;
constructor(length, stride) {
this.length = length;
@@ -33,9 +32,6 @@ export class FastVertexArray {
}
insert(sprite: TxSprite): number {
if (this.destroyed) {
return;
}
this.count++;
let position;
@@ -49,14 +45,11 @@ export class FastVertexArray {
}
}
this.sprites[position] = sprite;
this.dirty = true;
return position;
this.dirty = true;
}
remove(index: number): void {
if (this.destroyed) {
return;
}
this.count--;
this.clearData(index);
this.freeSlots.push(index);
@@ -68,26 +61,20 @@ export class FastVertexArray {
}
setData(index: number, dataChunk: number[]): void {
if (this.destroyed) {
return;
}
this.data.set(dataChunk, (index * this.stride));
this.dirty = true;
}
private clearData(index: number): void {
clearData(index: number): void {
this.data.fill(0, (index * this.stride), ((index + 1) * this.stride));
this.dirty = true;
}
getData(index: number): Float32Array {
if (this.destroyed) {
return;
}
return this.data.subarray(index, this.stride);
}
private expand(): void {
expand(): void {
this.length *= 2;
const newData = new Float32Array(this.length * this.stride);
newData.set(this.data);
@@ -95,7 +82,7 @@ export class FastVertexArray {
this.dirty = true;
}
private compact(): void {
compact(): void {
// New array length is the smallest power of 2 larger than the sprite count (but no smaller than 512)
const newLength = Math.max(512, Math.pow(2, Math.ceil(Math.log2(this.count))));
if (newLength !== this.length) {
@@ -123,13 +110,4 @@ export class FastVertexArray {
getVertexData(): Float32Array {
return this.data;
}
destroy(): void {
this.data = null;
this.sprites = null;
this.freeSlots = null;
this.lastSlot = 0;
this.dirty = false;
this.destroyed = true;
}
}
@@ -116,7 +116,7 @@ export class BlockViewComponent implements OnInit, OnDestroy {
this.isLoadingBlock = false;
this.isLoadingOverview = true;
}),
shareReplay({ bufferSize: 1, refCount: true })
shareReplay(1)
);
this.overviewSubscription = block$.pipe(
@@ -176,8 +176,5 @@ export class BlockViewComponent implements OnInit, OnDestroy {
if (this.queryParamsSubscription) {
this.queryParamsSubscription.unsubscribe();
}
if (this.blockGraph) {
this.blockGraph.destroy();
}
}
}
@@ -117,7 +117,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
this.openGraphService.waitOver('block-data-' + this.rawId);
}),
throttleTime(50, asyncScheduler, { leading: true, trailing: true }),
shareReplay({ bufferSize: 1, refCount: true })
shareReplay(1)
);
this.overviewSubscription = block$.pipe(
@@ -1,8 +1,8 @@
import { Component, OnInit, OnDestroy, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core';
import { Location } from '@angular/common';
import { ActivatedRoute, ParamMap, Params, Router } from '@angular/router';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter, take } from 'rxjs/operators';
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter } from 'rxjs/operators';
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs';
import { StateService } from '@app/services/state.service';
import { SeoService } from '@app/services/seo.service';
@@ -68,7 +68,6 @@ export class BlockComponent implements OnInit, OnDestroy {
paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
numUnexpected: number = 0;
mode: 'projected' | 'actual' = 'projected';
currentQueryParams: Params;
overviewSubscription: Subscription;
accelerationsSubscription: Subscription;
@@ -81,8 +80,8 @@ export class BlockComponent implements OnInit, OnDestroy {
timeLtr: boolean;
childChangeSubscription: Subscription;
auditPrefSubscription: Subscription;
isAuditEnabledSubscription: Subscription;
oobSubscription: Subscription;
priceSubscription: Subscription;
blockConversion: Price;
@@ -119,7 +118,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.setAuditAvailable(this.auditSupported);
if (this.auditSupported) {
this.isAuditEnabledSubscription = this.isAuditEnabledFromParam().subscribe(auditParam => {
this.isAuditEnabledFromParam().subscribe(auditParam => {
if (this.auditParamEnabled) {
this.auditModeEnabled = auditParam;
} else {
@@ -282,7 +281,7 @@ export class BlockComponent implements OnInit, OnDestroy {
}
}),
throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
shareReplay({ bufferSize: 1, refCount: true })
shareReplay(1)
);
this.overviewSubscription = this.block$.pipe(
@@ -364,7 +363,6 @@ export class BlockComponent implements OnInit, OnDestroy {
.subscribe((network) => this.network = network);
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.currentQueryParams = params;
if (params.showDetails === 'true') {
this.showDetails = true;
} else {
@@ -416,7 +414,6 @@ export class BlockComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
this.overviewSubscription?.unsubscribe();
this.accelerationsSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe();
this.blocksSubscription?.unsubscribe();
this.cacheBlocksSubscription?.unsubscribe();
@@ -424,16 +421,8 @@ export class BlockComponent implements OnInit, OnDestroy {
this.queryParamsSubscription?.unsubscribe();
this.timeLtrSubscription?.unsubscribe();
this.childChangeSubscription?.unsubscribe();
this.auditPrefSubscription?.unsubscribe();
this.isAuditEnabledSubscription?.unsubscribe();
this.oobSubscription?.unsubscribe();
this.priceSubscription?.unsubscribe();
this.blockGraphProjected.forEach(graph => {
graph.destroy();
});
this.blockGraphActual.forEach(graph => {
graph.destroy();
});
this.oobSubscription?.unsubscribe();
}
// TODO - Refactor this.fees/this.reward for liquid because it is not
@@ -744,18 +733,19 @@ export class BlockComponent implements OnInit, OnDestroy {
toggleAuditMode(): void {
this.stateService.hideAudit.next(this.auditModeEnabled);
const queryParams = { ...this.currentQueryParams };
delete queryParams['audit'];
this.route.queryParams.subscribe(params => {
const queryParams = { ...params };
delete queryParams['audit'];
let newUrl = this.router.url.split('?')[0];
const queryString = new URLSearchParams(queryParams).toString();
if (queryString) {
newUrl += '?' + queryString;
}
this.location.replaceState(newUrl);
let newUrl = this.router.url.split('?')[0];
const queryString = new URLSearchParams(queryParams).toString();
if (queryString) {
newUrl += '?' + queryString;
}
this.location.replaceState(newUrl);
});
// avoid duplicate subscriptions
this.auditPrefSubscription?.unsubscribe();
this.auditPrefSubscription = this.stateService.hideAudit.subscribe((hide) => {
this.auditModeEnabled = !hide;
this.showAudit = this.auditAvailable && this.auditModeEnabled;
@@ -772,7 +762,7 @@ export class BlockComponent implements OnInit, OnDestroy {
return this.route.queryParams.pipe(
map(params => {
this.auditParamEnabled = 'audit' in params;
return this.auditParamEnabled ? !(params['audit'] === 'false') : true;
})
);
@@ -281,11 +281,9 @@
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/wallet/' + widget.props.wallet | relativeUrl]">
<span class="title-link">
<h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
<span>&nbsp;</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>
</span>
<app-address-transactions-widget [addressSummary$]="walletSummary$"></app-address-transactions-widget>
</div>
</div>
@@ -307,6 +305,20 @@
</div>
</div>
}
@case ('simpleproof') {
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/sp/verified' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.recent-blocks">{{ widget.props?.label }}</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<app-simpleproof-widget [label]="widget.props.label" [key]="widget.props.key" [widget]="true"></app-simpleproof-widget>
</div>
</div>
</div>
}
}
}
</div>
@@ -162,9 +162,6 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
this.cacheBlocksSubscription?.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe();
this.blockGraphs.forEach(graph => {
graph.destroy();
});
}
shiftTestBlocks(): void {
@@ -21,8 +21,10 @@
<div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle">
<span>To use the faucet, please&nbsp;</span>
<a routerLink="/login" [queryParams]="{'redirectTo': '/testnet4/faucet'}">login</a>
<span class="mr-2">&nbsp;or</span>
</div>
<app-github-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Github"></app-github-login>
<app-twitter-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Twitter"></app-twitter-login>
</div>
}
@else if (user && user.status === 'pending' && !user.email && user.snsId) {
@@ -34,18 +36,18 @@
</div>
}
@else if (error === 'not_available') {
<!-- User logged in but not a paid user or did not link its Github account -->
<!-- User logged in but not a paid user or did not link its Twitter account -->
<div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle">
<span class="mb-2 mr-2">To use the faucet, please</span>
</div>
<app-github-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Github"></app-github-login>
<app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
</div>
}
@else if (error === 'account_limited') {
<div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle">
<span class="mb-2 mr-2">Your account does not allow you to access the faucet</span>
<span class="mb-2 mr-2">Your Twitter account does not allow you to access the faucet</span>
</div>
</div>
}
@@ -1,6 +0,0 @@
<a href="#" (click)="githubLogin()" [class]="(disabled ? 'disabled': '') + (customClass ? customClass : 'w-100 btn mt-1 d-flex justify-content-center align-items-center')" style="background-color: rgb(31, 35, 40)" [style]="width ? 'width: ' + width : ''">
<svg height="32" viewBox="0 0 18 16" width="32" style="fill: white;">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
<span class="ml-2 text-light align-middle">{{ buttonString }}</span>
</a>
@@ -1,25 +0,0 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-github-login',
templateUrl: './github-login.component.html',
})
export class GithubLogin {
@Input() width: string | null = null;
@Input() customClass: string | null = null;
@Input() buttonString: string= 'unset';
@Input() redirectTo: string | null = null;
@Output() clicked = new EventEmitter<boolean>();
@Input() disabled: boolean = false;
constructor() {}
githubLogin() {
this.clicked.emit(true);
if (this.redirectTo) {
location.replace(`/api/v1/services/auth/login/github?redirectTo=${encodeURIComponent(this.redirectTo)}`);
} else {
location.replace(`/api/v1/services/auth/login/github?redirectTo=${location.href}`);
}
return false;
}
}
@@ -120,7 +120,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
}
ngOnDestroy(): void {
this.blockGraph?.destroy();
this.blockSub.unsubscribe();
this.timeLtrSubscription.unsubscribe();
this.websocketService.stopTrackMempoolBlock();
@@ -10,7 +10,7 @@
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
</div>
<div class="box pool-details">
<div class="box">
<div class="row">
<div class="col-lg-6">
@@ -173,123 +173,7 @@
<div class="spinner-border text-light"></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>&nbsp;</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) {
@if ($index === 0 && branch) {
<a [routerLink]="['/tx' | relativeUrl, reverseHash(branch)]"><td class="merkle" [style.background-color]="branch ? '#' + branch.slice(0, 6) : ''"><fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 14px; color: white"></fa-icon></td></a>
} @else {
<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 -->
<h2 i18n="master-page.blocks">Blocks</h2>
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5"
[infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
<ng-container *ngIf="blocks$ | async as blocks; else skeleton">
@@ -49,110 +49,111 @@ div.scrollable {
max-height: 75px;
}
.pool-details {
.box {
padding-bottom: 5px;
@media (min-width: 767.98px) {
min-height: 187px;
}
}
.label {
width: 25%;
@media (min-width: 767.98px) {
vertical-align: middle;
}
@media (max-width: 767.98px) {
font-weight: bold;
}
}
.label.addresses {
vertical-align: top;
padding-top: 25px;
}
.addresses-data {
vertical-align: top;
font-family: monospace;
font-size: 14px;
}
.data {
text-align: right;
padding-left: 5%;
@media (max-width: 992px) {
text-align: left;
padding-left: 12px;
}
@media (max-width: 450px) {
text-align: right;
}
}
.progress {
background-color: var(--secondary);
}
.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%;
}
@media (max-width: 650px) {
width: 20%;
}
@media (max-width: 450px) {
display: none;
}
}
.scriptmessage {
overflow: hidden;
display: inline-block;
text-overflow: ellipsis;
.label {
width: 25%;
@media (min-width: 767.98px) {
vertical-align: middle;
width: auto;
text-align: left;
}
@media (max-width: 767.98px) {
font-weight: bold;
}
}
.label.addresses {
vertical-align: top;
padding-top: 25px;
}
.addresses-data {
vertical-align: top;
font-family: monospace;
font-size: 14px;
}
.data {
text-align: right;
padding-left: 5%;
@media (max-width: 992px) {
text-align: left;
padding-left: 12px;
}
@media (max-width: 450px) {
text-align: right;
}
}
.progress {
background-color: var(--secondary);
}
.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%;
}
@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 {
@@ -213,56 +214,4 @@ div.scrollable {
.taller-row {
height: 75px;
}
.stratum-table {
width: 100%;
.merkle {
width: 100px;
text-align: center;
}
.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,9 +10,6 @@ import { selectPowerOfTen } from '@app/bitcoin.utils';
import { formatNumber } from '@angular/common';
import { SeoService } from '@app/services/seo.service';
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 {
cost: number,
@@ -30,16 +27,12 @@ export class PoolComponent implements OnInit {
@Input() left: number | string = 75;
gfg = true;
stratumEnabled = this.stateService.env.STRATUM_ENABLED;
formatNumber = formatNumber;
Math = Math;
slugSubscription: Subscription;
poolStats$: Observable<PoolStat>;
blocks$: Observable<BlockExtended[]>;
oobFees$: Observable<AccelerationTotal[]>;
job$: Observable<StratumJob | null>;
expectedBlockTime$: Observable<number>;
isLoading = true;
error: HttpErrorResponse | null = null;
@@ -60,8 +53,6 @@ export class PoolComponent implements OnInit {
private apiService: ApiService,
private route: ActivatedRoute,
public stateService: StateService,
private websocketService: WebsocketService,
private miningService: MiningService,
private seoService: SeoService,
) {
this.auditAvailable = this.stateService.env.AUDIT;
@@ -71,7 +62,7 @@ export class PoolComponent implements OnInit {
this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
this.isLoading = true;
this.blocks = [];
this.chartOptions = {};
this.chartOptions = {};
this.slug = slug;
this.initializeObservables();
});
@@ -138,31 +129,6 @@ export class PoolComponent implements OnInit {
}),
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) {
@@ -361,10 +327,6 @@ export class PoolComponent implements OnInit {
return block.height;
}
reverseHash(hash: string) {
return hash.match(/../g).reverse().join('');
}
ngOnDestroy(): void {
this.slugSubscription.unsubscribe();
}
@@ -0,0 +1,75 @@
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget}">
<div *ngIf="!widget" class="float-left" style="display: flex; width: 100%; align-items: center;">
<h1>{{ label }}</h1>
<div *ngIf="!widget && isLoading" class="spinner-border" role="status"></div>
</div>
<div class="clearfix"></div>
@if (isLoading) {
loading!
<div class="spinner-wrapper">
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
} @else if (error || !verified.length) {
<div class="error-wrapper">
<span>temporarily unavailable</span>
</div>
} @else {
<div style="min-height: 295px">
<table class="table table-borderless" [class.table-fixed]="widget">
<thead>
<th class="filename text-left" [ngClass]="{'widget': widget}" i18n="simpleproof.filename">Filename</th>
<th class="hash text-left" [ngClass]="{'widget': widget}" i18n="simpleproof.hash">Hash</th>
<th class="verified text-right" [ngClass]="{'widget': widget}" i18n="simpleproof.verified">Verified</th>
<th class="proof text-right" [ngClass]="{'widget': widget}" i18n="simpleproof.proof">Proof</th>
</thead>
<tbody *ngIf="verifiedPage; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let item of verifiedPage">
<td class="filename text-left" [class]="widget ? 'widget' : ''">{{ item.file_name }}</td>
<td class="hash text-left" [class]="widget ? 'widget' : ''">{{ item.sha256 }}</td>
<td class="verified text-right" [class]="widget ? 'widget' : ''">
<app-timestamp [unixTime]="item.block_time" [customFormat]="'yyyy-MM-dd'" [hideTimeSince]="true"></app-timestamp>
</td>
<td class="proof text-right" [class]="widget ? 'widget' : ''">
<a [href]="item.sanitized_url" target="_blank" class="badge badge-primary badge-verify">
<span class="icon">
<img class="icon-img" src="/resources/sp.svg">
</span>
<span i18n="simpleproof.verify">Verify</span>
</a>
</td>
</tr>
</tbody>
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of [].constructor(itemsPerPage)">
<td class="filename text-left" [ngClass]="{'widget': widget}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="hash text-left" [ngClass]="{'widget': widget}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="verified text-right" [ngClass]="{'widget': widget}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="proof text-right" [ngClass]="{'widget': widget}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
</tr>
</tbody>
</ng-template>
</table>
<ngb-pagination *ngIf="!widget" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
[collectionSize]="verified.length" [rotate]="true" [maxSize]="paginationMaxSize" [pageSize]="itemsPerPage" [(page)]="page"
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
</ngb-pagination>
<ng-template [ngIf]="!widget">
<div class="clearfix"></div>
<br>
</ng-template>
</div>
}
</div>
@@ -0,0 +1,114 @@
.spinner-wrapper, .error-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
}
.spinner-border {
height: 25px;
width: 25px;
margin-top: -10px;
margin-left: -13px;
flex-shrink: 0;
}
.container-xl {
max-width: 1400px;
}
.container-xl.widget {
padding-left: 0px;
padding-bottom: 0px;
padding-right: 0px;
}
.container-xl.legacy {
max-width: 1140px;
}
.container {
max-width: 100%;
}
tr, td, th {
border: 0px;
padding-top: 0.71rem !important;
padding-bottom: 0.7rem !important;
}
.clear-link {
color: white;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}
.progress {
background-color: var(--secondary);
}
.filename {
width: 50%;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hash {
width: 25%;
max-width: 700px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
td.hash {
font-family: monospace;
}
.widget .hash {
display: none;
}
@media (max-width: 1200px) {
.hash {
display: none;
}
}
.verified {
width: 25%;
}
td.verified {
color: var(--tertiary);
}
.proof {
width: 25%;
}
.badge-verify {
font-size: 1.05em;
font-weight: normal;
background: var(--nav-bg);
color: white;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: auto;
.icon {
margin: -0.25em;
margin-right: 0.5em;
.icon-img {
width: 16px;
font-size: 16px;
}
}
}
@@ -0,0 +1,86 @@
import { Component, Input, SecurityContext, SimpleChanges, OnChanges } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { ServicesApiServices } from '@app/services/services-api.service';
import { catchError, of } from 'rxjs';
export interface SimpleProof {
file_name: string;
sha256: string;
ots_verification: string;
block_height: number;
block_hash: string;
block_time: number;
simpleproof_url: string;
key?: string;
sanitized_url?: SafeResourceUrl;
}
@Component({
selector: 'app-simpleproof-widget',
templateUrl: './simpleproof-widget.component.html',
styleUrls: ['./simpleproof-widget.component.scss'],
})
export class SimpleProofWidgetComponent implements OnChanges {
@Input() key: string = window['__env']?.customize?.dashboard.widgets?.find(w => w.component ==='simpleproof')?.props?.key ?? '';
@Input() label: string = window['__env']?.customize?.dashboard.widgets?.find(w => w.component ==='simpleproof')?.props?.label ?? 'Verified Documents';
@Input() widget: boolean = false;
@Input() width = 300;
@Input() height = 400;
verified: SimpleProof[] = [];
verifiedPage: SimpleProof[] = [];
isLoading: boolean = true;
error: boolean = false;
page = 1;
lastPage = 1;
itemsPerPage = 15;
paginationMaxSize = window.innerWidth <= 767.98 ? 3 : 5;
constructor(
private servicesApiService: ServicesApiServices,
public sanitizer: DomSanitizer,
) {}
ngOnInit(): void {
this.loadVerifications();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.widget) {
this.itemsPerPage = this.widget ? 6 : 15;
}
if (changes.key) {
this.loadVerifications();
}
}
loadVerifications(): void {
if (this.key) {
this.isLoading = true;
this.servicesApiService.getSimpleProofs$(this.key).pipe(
catchError(() => {
this.isLoading = false;
this.error = true;
return of({});
}),
).subscribe((data: Record<string, SimpleProof>) => {
if (Object.keys(data).length) {
this.verified = Object.keys(data).map(key => ({
...data[key],
file_name: data[key].file_name.replace('source-', '').replace('_', ' '),
key,
sanitized_url: this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, data[key]['simpleproof-url']) ?? ''),
})).sort((a, b) => b.key.localeCompare(a.key));
this.verifiedPage = this.verified.slice((this.page - 1) * this.itemsPerPage, this.page * this.itemsPerPage);
this.isLoading = false;
this.error = false;
}
});
}
}
pageChange(page: number): void {
this.page = page;
this.verifiedPage = this.verified.slice((this.page - 1) * this.itemsPerPage, this.page * this.itemsPerPage);
}
}
@@ -1,55 +0,0 @@
<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="merkle" [attr.colspan]="rows[0]?.merkleCells?.length || 4">
Merkle Branches
</td>
<td class="pool">Pool</td>
<td class="tag">Coinbase Tag</td>
<td class="reward">Reward</td>
<td class="height">Height</td>
</tr>
</thead>
<tbody>
@for (row of rows; track row.job.pool) {
<tr>
@for (cell of row.merkleCells; track $index) {
<td class="merkle" [style.background-color]="cell.hash ? '#' + cell.hash.slice(0, 6) : ''">
@if ($index === 0 && cell.hash) {
<a [routerLink]="['/tx' | relativeUrl, reverseHash(cell.hash)]" class="cell-link">
<div class="pipe-segment" [class]="pipeToClass(cell.type)"></div>
</a>
} @else {
<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>
<td class="tag">
{{ row.job.tag }}
</td>
<td class="reward">
<app-amount [satoshis]="row.job.reward"></app-amount>
</td>
<td class="height">
{{ row.job.height }}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
@@ -1,138 +0,0 @@
.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;
}
}
}
.cell-link {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
color: inherit;
text-decoration: none;
}
}
@media (max-width: 800px) {
.stratum-table {
td {
&.tag {
display: none;
}
}
}
}
@media (max-width: 650px) {
.stratum-table {
td {
&.reward {
display: none;
}
}
}
}
.badge {
position: relative;
color: #FFF;
}
.pool-logo {
width: 15px;
height: 15px;
position: relative;
top: -1px;
margin-right: 2px;
}
@@ -1,230 +0,0 @@
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, poolId: number): string[] {
let lastHash = '';
const ids: string[] = [];
for (let i = 0; i < numBranches; i++) {
if (merkleBranches[i]) {
lastHash = merkleBranches[i];
ids.push(`${i}-${lastHash}`);
} else {
ids.push(`${i}-${lastHash}-${poolId}`);
}
}
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, job.pool) };
}
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];
}
reverseHash(hash: string) {
return hash.match(/../g).reverse().join('');
}
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,7 +24,6 @@
[height]="tx?.status?.block_height"
[replaced]="replaced"
[removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed"
[cached]="isCached"
></app-confirmations>
</div>
</ng-container>
@@ -240,7 +240,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
retry({ count: 2, delay: 2000 }),
// Try again until we either get a valid response, or the transaction is confirmed
repeat({ delay: 2000 }),
filter((transactionTimes) => transactionTimes?.[0] > 0 || this.tx.status?.confirmed),
filter((transactionTimes) => transactionTimes?.length && transactionTimes[0] > 0 && !this.tx.status?.confirmed),
take(1),
)),
)
@@ -202,12 +202,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
for (const address of this.addresses) {
switch (address.length) {
case 130: {
if (v.scriptpubkey === '41' + address + 'ac') {
if (v.scriptpubkey === '21' + address + 'ac') {
return v.value;
}
} break;
case 66: {
if (v.scriptpubkey === '21' + address + 'ac') {
if (v.scriptpubkey === '41' + address + 'ac') {
return v.value;
}
} break;
@@ -224,12 +224,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
for (const address of this.addresses) {
switch (address.length) {
case 130: {
if (v.prevout?.scriptpubkey === '41' + address + 'ac') {
if (v.prevout?.scriptpubkey === '21' + address + 'ac') {
return v.prevout?.value;
}
} break;
case 66: {
if (v.prevout?.scriptpubkey === '21' + address + 'ac') {
if (v.prevout?.scriptpubkey === '41' + address + 'ac') {
return v.prevout?.value;
}
} break;
@@ -1,6 +1,6 @@
<div class="container-xl" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
<div class="title-address">
<h1>{{ walletName }}</h1>
<h1 i18n="shared.wallet">Wallet</h1>
</div>
<div class="clearfix"></div>
@@ -74,36 +74,6 @@
</ng-container>
</ng-container>
<br>
<div class="title-tx">
<h2 class="text-left" i18n="address.transactions">Transactions</h2>
</div>
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" [addresses]="addressStrings" (loadMore)="loadMore()"></app-transactions-list>
<div class="text-center">
<ng-template [ngIf]="isLoadingTransactions">
<div class="header-bg box">
<div class="row" style="height: 107px;">
<div class="col-sm">
<span class="skeleton-loader"></span>
</div>
<div class="col-sm">
<span class="skeleton-loader"></span>
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="retryLoadMore">
<br>
<button type="button" class="btn btn-outline-info btn-sm" (click)="loadMore()"><fa-icon [icon]="['fas', 'redo-alt']" [fixedWidth]="true"></fa-icon></button>
</ng-template>
</div>
<ng-template #loadingTemplate>
<div class="box" *ngIf="!error; else errorTemplate">
@@ -9,8 +9,6 @@ import { of, Observable, Subscription } from 'rxjs';
import { SeoService } from '@app/services/seo.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { WalletAddress } from '@interfaces/node-api.interface';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { AudioService } from '@app/services/audio.service';
class WalletStats implements ChainStats {
addresses: string[];
@@ -26,7 +24,6 @@ class WalletStats implements ChainStats {
acc.funded_txo_sum += stat.funded_txo_sum;
acc.spent_txo_count += stat.spent_txo_count;
acc.spent_txo_sum += stat.spent_txo_sum;
acc.tx_count += stat.tx_count;
return acc;
}, {
funded_txo_count: 0,
@@ -112,17 +109,12 @@ export class WalletComponent implements OnInit, OnDestroy {
addressStrings: string[] = [];
walletName: string;
isLoadingWallet = true;
isLoadingTransactions = true;
transactions: Transaction[];
totalTransactionCount: number;
retryLoadMore = false;
wallet$: Observable<Record<string, WalletAddress>>;
walletAddresses$: Observable<Record<string, Address>>;
walletSummary$: Observable<AddressTxSummary[]>;
walletStats$: Observable<WalletStats>;
error: any;
walletSubscription: Subscription;
transactionSubscription: Subscription;
collapseAddresses: boolean = true;
@@ -137,8 +129,6 @@ export class WalletComponent implements OnInit, OnDestroy {
private websocketService: WebsocketService,
private stateService: StateService,
private apiService: ApiService,
private electrsApiService: ElectrsApiService,
private audioService: AudioService,
private seoService: SeoService,
) { }
@@ -182,21 +172,6 @@ export class WalletComponent implements OnInit, OnDestroy {
}),
switchMap(initial => this.stateService.walletTransactions$.pipe(
startWith(null),
tap((transactions) => {
if (!transactions?.length) {
return;
}
for (const transaction of transactions) {
const tx = this.transactions.find((t) => t.txid === transaction.txid);
if (tx) {
tx.status = transaction.status;
} else {
this.transactions.unshift(transaction);
}
}
this.transactions = this.transactions.slice();
this.audioService.playSound('magic');
}),
scan((wallet, walletTransactions) => {
for (const tx of (walletTransactions || [])) {
const funded: Record<string, number> = {};
@@ -292,57 +267,8 @@ export class WalletComponent implements OnInit, OnDestroy {
return stats;
}, walletStats),
);
})
}),
);
this.transactionSubscription = this.wallet$.pipe(
switchMap(wallet => {
const addresses = Object.keys(wallet).map(addr => this.normalizeAddress(addr));
return this.electrsApiService.getAddressesTransactions$(addresses);
}),
map(transactions => {
// only confirmed transactions supported for now
return transactions.filter(tx => tx.status.confirmed).sort((a, b) => b.status.block_height - a.status.block_height);
}),
catchError((error) => {
console.log(error);
this.error = error;
this.seoService.logSoft404();
this.isLoadingWallet = false;
return of([]);
})
).subscribe((transactions: Transaction[] | null) => {
if (!transactions) {
return;
}
this.transactions = transactions;
this.isLoadingTransactions = false;
});
}
loadMore(): void {
if (this.isLoadingTransactions || this.fullyLoaded) {
return;
}
this.isLoadingTransactions = true;
this.retryLoadMore = false;
this.electrsApiService.getAddressesTransactions$(this.addressStrings, this.transactions[this.transactions.length - 1].txid)
.subscribe((transactions: Transaction[]) => {
if (transactions && transactions.length) {
this.transactions = this.transactions.concat(transactions.sort((a, b) => b.status.block_height - a.status.block_height));
} else {
this.fullyLoaded = true;
}
this.isLoadingTransactions = false;
},
(error) => {
this.isLoadingTransactions = false;
this.retryLoadMore = true;
// In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page.
if (error.status === 422) {
window.location.reload();
}
});
}
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
@@ -373,6 +299,5 @@ export class WalletComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.websocketService.stopTrackingWallet();
this.walletSubscription.unsubscribe();
this.transactionSubscription.unsubscribe();
}
}
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,6 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Env, StateService } from '@app/services/state.service';
import { restApiDocsData, wsApiDocsData } from '@app/docs/api-docs/api-docs-data';
import { restApiDocsData } from '@app/docs/api-docs/api-docs-data';
import { faqData } from '@app/docs/api-docs/api-docs-data';
@Component({
@@ -28,8 +28,6 @@ export class ApiDocsNavComponent implements OnInit {
this.auditEnabled = this.env.AUDIT;
if (this.whichTab === 'rest') {
this.tabData = restApiDocsData;
} else if (this.whichTab === 'websocket') {
this.tabData = wsApiDocsData;
} else if (this.whichTab === 'faq') {
this.tabData = faqData;
}
@@ -108,43 +108,18 @@
</div>
</div>
<div id="websocketAPI" *ngIf="whichTab === 'websocket'">
<div id="doc-nav-desktop" class="hide-on-mobile" [ngClass]="desktopDocsNavPosition">
<app-api-docs-nav (navLinkClickEvent)="anchorLinkClick( $event )" [network]="{ val: network$ | async }" [whichTab]="whichTab"></app-api-docs-nav>
</div>
<div class="doc-content">
<div id="enterprise-cta-mobile" *ngIf="officialMempoolInstance && showMobileEnterpriseUpsell">
<p>Get higher API limits with <span class="no-line-break">Mempool Enterprise®</span></p>
<div class="button-group">
<a class="btn btn-small btn-secondary" (click)="showMobileEnterpriseUpsell = false">No Thanks</a>
<a class="btn btn-small btn-purple" href="https://mempool.space/enterprise">More Info <fa-icon [icon]="['fas', 'angle-right']" [styles]="{'font-size': '12px'}"></fa-icon></a>
<div id="websocketAPI" *ngIf="( whichTab === 'websocket' )">
<div class="api-category">
<div class="websocket">
<div class="endpoint">
<div class="subtitle" i18n="Api docs endpoint">Endpoint</div>
{{ wrapUrl(network.val, wsDocs, true) }}
</div>
</div>
<p class="doc-welcome-note">Below is a reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} <ng-container i18n="api-docs.title-websocket">Websocket service</ng-container> running at {{ websocketUrl(network.val) }}.</p>
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that usage limits apply to our WebSocket API. Consider an <a href="https://mempool.space/enterprise">enterprise sponsorship</a> if you need higher API limits, such as higher tracking limits.</p>
<div class="doc-item-container" *ngFor="let item of wsDocs">
<div *ngIf="!item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance )">
<h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3>
<div *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )" class="endpoint-container" id="{{ item.fragment }}">
<a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick({event: $event, fragment: item.fragment})">{{ item.title }} <span>{{ item.category }}</span></a>
<div class="endpoint-content">
<div class="description">
<div class="subtitle" i18n>Description</div>
<div [innerHTML]="item.description.default" i18n></div>
</div>
<div class="description">
<div class="subtitle" i18n>Payload</div>
<pre><code [innerText]="item.payload"></code></pre>
</div>
<app-code-template [hostname]="hostname" [baseNetworkUrl]="baseNetworkUrl" [method]="item.httpRequestMethod" [code]="item.codeExample.default" [network]="network.val" [showCodeExample]="item.showJsExamples"></app-code-template>
</div>
</div>
<div class="description">
<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" [showCodeExample]="wsDocs.showJsExamples"></app-code-template>
</div>
</div>
</div>
@@ -470,21 +470,3 @@ dd {
margin-left: 1em;
}
}
code {
background-color: var(--bg);
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;
}
pre {
display: block;
font-size: 87.5%;
color: #f18920;
background-color: var(--bg);
padding: 30px;
code{
background-color: transparent;
white-space: break-spaces;
word-break: break-all;
}
}
@@ -145,7 +145,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
if (document.getElementById( targetId + "-tab-header" )) {
tabHeaderHeight = document.getElementById( targetId + "-tab-header" ).scrollHeight;
}
if( ( window.innerWidth <= 992 ) && ( ( this.whichTab === 'rest' ) || ( this.whichTab === 'faq' ) || ( this.whichTab === 'websocket' ) ) && targetId ) {
if( ( window.innerWidth <= 992 ) && ( ( this.whichTab === 'rest' ) || ( this.whichTab === 'faq' ) ) && targetId ) {
const endpointContainerEl = document.querySelector<HTMLElement>( "#" + targetId );
const endpointContentEl = document.querySelector<HTMLElement>( "#" + targetId + " .endpoint-content" );
const endPointContentElHeight = endpointContentEl.clientHeight;
@@ -207,29 +207,13 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
text = text.replace('%{' + indexNumber + '}', curlText);
}
if (websocket) {
const wsHostname = this.hostname.replace('https://', 'wss://');
wsHostname.replace('http://', 'ws://');
return `${wsHostname}${curlNetwork}${text}`;
}
return `${this.hostname}${curlNetwork}${text}`;
}
websocketUrl(network: string) {
let curlNetwork = '';
if (this.env.BASE_MODULE === 'mempool') {
if (!['', 'mainnet'].includes(network)) {
curlNetwork = `/${network}`;
}
} else if (this.env.BASE_MODULE === 'liquid') {
if (!['', 'liquid'].includes(network)) {
curlNetwork = `/${network}`;
}
}
if (network === this.env.ROOT_NETWORK) {
curlNetwork = '';
}
let wsHostname = this.hostname.replace('https://', 'wss://');
wsHostname = wsHostname.replace('http://', 'ws://');
return `${wsHostname}${curlNetwork}/api/v1/ws`;
}
}
@@ -1,4 +1,4 @@
import { AddressTxSummary, Block, ChainStats } from "./electrs.interface";
import { AddressTxSummary, Block, ChainStats, Transaction } from "./electrs.interface";
export interface OptimizedMempoolStats {
added: number;
@@ -412,13 +412,13 @@ export interface Acceleration {
feeDelta: number;
blockHash: string;
blockHeight: number;
acceleratedFeeRate?: number;
boost?: number;
bidBoost?: number;
boostCost?: number;
boostRate?: number;
minedByPoolUniqueId?: number;
canceled?: number;
}
export interface AccelerationHistoryParams {
@@ -21,8 +21,6 @@ export interface WebsocketResponse {
rbfInfo?: RbfTree;
rbfLatest?: RbfTree[];
rbfLatestSummary?: ReplacementInfo[];
stratumJob?: StratumJob;
stratumJobs?: Record<number, StratumJob>;
utxoSpent?: object;
transactions?: TransactionStripped[];
loadingIndicators?: ILoadingIndicators;
@@ -39,7 +37,6 @@ export interface WebsocketResponse {
'track-rbf-summary'?: boolean;
'track-accelerations'?: boolean;
'track-wallet'?: string;
'track-stratum'?: string | number;
'watch-mempool'?: boolean;
'refresh-blocks'?: boolean;
}
@@ -153,24 +150,3 @@ export interface HealthCheckHost {
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;
}
+9 -12
View File
@@ -10,10 +10,10 @@ import { TestTransactionsComponent } from '@components/test-transactions/test-tr
import { CalculatorComponent } from '@components/calculator/calculator.component';
import { BlocksList } from '@components/blocks-list/blocks-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 { ServerStatusComponent } from '@components/server-health/server-status.component';
import { FaucetComponent } from '@components/faucet/faucet.component';
import { FaucetComponent } from '@components/faucet/faucet.component'
import { SimpleProofWidgetComponent } from './components/simpleproof-widget/simpleproof-widget.component';
const browserWindow = window || {};
// @ts-ignore
@@ -57,16 +57,6 @@ const routes: Routes = [
path: 'rbf',
component: RbfList,
},
...(browserWindowEnv.STRATUM_ENABLED ? [{
path: 'stratum',
component: StartComponent,
children: [
{
path: '',
component: StratumList,
}
]
}] : []),
{
path: 'terms-of-service',
loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),
@@ -141,6 +131,13 @@ if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
}
}
if (window['__env']?.customize?.dashboard.widgets?.some(w => w.component ==='simpleproof')) {
routes[0].children.push({
path: 'sp/verified',
component: SimpleProofWidgetComponent,
});
}
@NgModule({
imports: [
RouterModule.forChild(routes)
@@ -58,7 +58,6 @@ export class AuthServiceMempool {
setAuth(auth: any) {
if (!auth) {
localStorage.removeItem('auth');
localStorage.removeItem('authenticatorStatus');
} else {
localStorage.setItem('auth', JSON.stringify(auth));
}
@@ -142,16 +142,12 @@ export class ElectrsApiService {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
}
getAddressesTransactions$(addresses: string[], txid?: string): Observable<Transaction[]> {
getAddressesTransactions$(addresses: string[], txid?: string): Observable<Transaction[]> {
let params = new HttpParams();
if (txid) {
params = params.append('after_txid', txid);
}
return this.httpClient.post<Transaction[]>(
this.apiBaseUrl + this.apiBasePath + '/api/addresses/txs',
addresses,
{ params }
);
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs?addresses=${addresses.join(',')}`, { params });
}
getAddressSummary$(address: string, txid?: string): Observable<AddressTxSummary[]> {
@@ -167,7 +163,7 @@ export class ElectrsApiService {
if (txid) {
params = params.append('after_txid', txid);
}
return this.httpClient.post<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + '/api/addresses/txs/summary', addresses, { params });
return this.httpClient.get<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs/summary?addresses=${addresses.join(',')}`, { params });
}
getScriptHashTransactions$(script: string, txid?: string): Observable<Transaction[]> {
@@ -186,7 +182,7 @@ export class ElectrsApiService {
params = params.append('after_txid', txid);
}
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
switchMap(scriptHashes => this.httpClient.post<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthashes/txs', scriptHashes, { params })),
switchMap(scriptHashes => this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs?scripthashes=${scriptHashes.join(',')}`, { params })),
);
}
@@ -216,7 +212,7 @@ export class ElectrsApiService {
params = params.append('after_txid', txid);
}
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
switchMap(scriptHashes => this.httpClient.post<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthashes/txs/summary', scriptHashes, { params })),
switchMap(scriptHashes => this.httpClient.get<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs/summary?scripthashes=${scriptHashes.join(',')}`, { params })),
);
}
+3 -2
View File
@@ -64,8 +64,8 @@ export class MiningService {
);
}
}
/**
/**
* Get names and slugs of all pools
*/
public getPools(): Observable<any[]> {
@@ -75,6 +75,7 @@ export class MiningService {
return this.poolsData;
})
);
}
/**
* Set the hashrate power of ten we want to display
@@ -8,6 +8,7 @@ import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMa
import { IBackendInfo } from '@interfaces/websocket.interface';
import { Acceleration, AccelerationHistoryParams } from '@interfaces/node-api.interface';
import { AccelerationStats } from '@components/acceleration/acceleration-stats/acceleration-stats.component';
import { SimpleProof } from '../components/simpleproof-widget/simpleproof-widget.component';
export interface IUser {
username: string;
@@ -18,6 +19,7 @@ export interface IUser {
subscription_tag: string;
status: 'pending' | 'verified' | 'disabled';
features: string | null;
fullName: string | null;
countryCode: string | null;
imageMd5: string;
ogRank: number | null;
@@ -142,12 +144,8 @@ export class ServicesApiServices {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
}
accelerateWithGooglePay$(txInput: string, token: string, verificationToken: string, cardTag: string, referenceId: string, userApprovedUSD: number, userChallenged: boolean) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged });
}
accelerateWithCardOnFile$(txInput: string, token: string, verificationToken: string, referenceId: string, userApprovedUSD: number, userChallenged: boolean) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cardOnFile`, { txInput: txInput, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged });
accelerateWithGooglePay$(txInput: string, token: string, verificationToken: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
}
getAccelerations$(): Observable<Acceleration[]> {
@@ -220,4 +218,8 @@ export class ServicesApiServices {
getPaymentStatus$(orderId: string): Observable<any> {
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/payments/bitcoin/check?order_id=${orderId}`, { observe: 'response' });
}
getSimpleProofs$(key: string): Observable<Record<string, SimpleProof>> {
return this.httpClient.get<Record<string, SimpleProof>>(`${this.stateService.env.SERVICES_API}/sp/verified/${key}`);
}
}
+2 -24
View File
@@ -1,7 +1,7 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { Transaction } from '@interfaces/electrs.interface';
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, StratumJob, isMempoolState } from '@interfaces/websocket.interface';
import { AddressTxSummary, Transaction } from '@interfaces/electrs.interface';
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '@interfaces/websocket.interface';
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '@interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
@@ -81,7 +81,6 @@ export interface Env {
ADDITIONAL_CURRENCIES: boolean;
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
STRATUM_ENABLED: boolean;
SERVICES_API?: string;
customize?: Customization;
PROD_DOMAINS: string[];
@@ -124,7 +123,6 @@ const defaultEnv: Env = {
'ACCELERATOR_BUTTON': true,
'PUBLIC_ACCELERATIONS': false,
'ADDITIONAL_CURRENCIES': false,
'STRATUM_ENABLED': false,
'SERVICES_API': 'https://mempool.space/api/v1/services',
'PROD_DOMAINS': [],
};
@@ -161,8 +159,6 @@ export class StateService {
liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
accelerations$ = new Subject<AccelerationDelta>();
liveAccelerations$: Observable<Acceleration[]>;
stratumJobUpdate$ = new Subject<{ state: Record<string, StratumJob> } | { job: StratumJob }>();
stratumJobs$ = new BehaviorSubject<Record<string, StratumJob>>({});
txConfirmed$ = new Subject<[string, BlockExtended]>();
txReplaced$ = new Subject<ReplacedTransaction>();
txRbfInfo$ = new Subject<RbfTree>();
@@ -307,24 +303,6 @@ export class StateService {
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.transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
this.blocksSubject$.next([]);
@@ -36,7 +36,6 @@ export class WebsocketService {
private isTrackingAccelerations: boolean = false;
private isTrackingWallet: boolean = false;
private trackingWalletName: string;
private isTrackingStratum: string | number | false = false;
private trackingMempoolBlock: number;
private trackingMempoolBlockNetwork: string;
private stoppingTrackMempoolBlock: any | null = null;
@@ -144,9 +143,6 @@ export class WebsocketService {
if (this.isTrackingWallet) {
this.startTrackingWallet(this.trackingWalletName);
}
if (this.isTrackingStratum !== false) {
this.startTrackStratum(this.isTrackingStratum);
}
this.stateService.connectionState$.next(2);
}
@@ -293,18 +289,6 @@ 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) {
this.websocketSubject.next({ historicalDate });
}
@@ -528,14 +512,6 @@ export class WebsocketService {
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']) {
this.stateService.serverHealth$.next(response['tomahawk']);
}
@@ -11,9 +11,9 @@
<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>
</ng-template>
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && (removed || cached)">
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && removed">
<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 [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !(removed || cached)">
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !removed">
<button type="button" class="btn btn-sm btn-danger no-cursor {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
</ng-template>
@@ -12,7 +12,6 @@ export class ConfirmationsComponent implements OnChanges {
@Input() height: number;
@Input() replaced: boolean = false;
@Input() removed: boolean = false;
@Input() cached: boolean = false;
@Input() hideUnconfirmed: boolean = false;
@Input() buttonClass: string = '';
+4 -14
View File
@@ -4,10 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
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, 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, faRobot, faShareNodes, faCreditCard } 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 } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '@components/menu/menu.component';
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
@@ -83,7 +80,6 @@ import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
import { DifficultyAdjustmentsTable } from '@components/difficulty-adjustments-table/difficulty-adjustments-table.components';
import { BlocksList } from '@components/blocks-list/blocks-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 { DataCyDirective } from '@app/data-cy.directive';
import { LoadingIndicatorComponent } from '@components/loading-indicator/loading-indicator.component';
@@ -120,12 +116,12 @@ import { CalculatorComponent } from '@components/calculator/calculator.component
import { BitcoinsatoshisPipe } from '@app/shared/pipes/bitcoinsatoshis.pipe';
import { HttpErrorComponent } from '@app/shared/components/http-error/http-error.component';
import { TwitterWidgetComponent } from '@components/twitter-widget/twitter-widget.component';
import { SimpleProofWidgetComponent } from '@components/simpleproof-widget/simpleproof-widget.component';
import { FaucetComponent } from '@components/faucet/faucet.component';
import { TwitterLogin } from '@components/twitter-login/twitter-login.component';
import { BitcoinInvoiceComponent } from '@components/bitcoin-invoice/bitcoin-invoice.component';
import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/weight-directives/weight-directives';
import { GithubLogin } from '../components/github-login.component/github-login.component';
@NgModule({
declarations: [
@@ -203,7 +199,6 @@ import { GithubLogin } from '../components/github-login.component/github-login.c
DifficultyAdjustmentsTable,
BlocksList,
RbfList,
StratumList,
DataCyDirective,
RewardStatsComponent,
LoadingIndicatorComponent,
@@ -241,9 +236,9 @@ import { GithubLogin } from '../components/github-login.component/github-login.c
OrdDataComponent,
HttpErrorComponent,
TwitterWidgetComponent,
SimpleProofWidgetComponent,
FaucetComponent,
TwitterLogin,
GithubLogin,
BitcoinInvoiceComponent,
],
imports: [
@@ -349,7 +344,6 @@ import { GithubLogin } from '../components/github-login.component/github-login.c
AmountShortenerPipe,
DifficultyAdjustmentsTable,
BlocksList,
StratumList,
DataCyDirective,
RewardStatsComponent,
LoadingIndicatorComponent,
@@ -377,8 +371,8 @@ import { GithubLogin } from '../components/github-login.component/github-login.c
OrdDataComponent,
HttpErrorComponent,
TwitterWidgetComponent,
SimpleProofWidgetComponent,
TwitterLogin,
GithubLogin,
BitcoinInvoiceComponent,
BitcoinsatoshisPipe,
@@ -460,9 +454,5 @@ export class SharedModule {
library.addIcons(faTimeline);
library.addIcons(faCircleXmark);
library.addIcons(faCalendarCheck);
library.addIcons(faMoneyBillTrendUp);
library.addIcons(faRobot);
library.addIcons(faShareNodes);
library.addIcons(faCreditCard);
}
}
+1 -13
View File
@@ -3,21 +3,11 @@
<head>
<meta charset="utf-8">
<!-- LOAD BEARING COMMENTS - DO NOT TOUCH! -->
<!-- TITLE -->
<title>mempool - Bitcoin Explorer</title>
<!-- END TITLE -->
<!-- CONFIG -->
<script src="/resources/config.js"></script>
<!-- END CONFIG -->
<!-- CUSTOMIZATION -->
<script src="/resources/customize.js"></script>
<!-- END CUSTOMIZATION -->
<base href="/">
<!-- META -->
<meta name="description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more." />
<meta property="og:image" content="https://mempool.space/resources/previews/mempool-space-preview.jpg" />
<meta property="og:image:type" content="image/jpeg" />
@@ -31,21 +21,19 @@
<meta name="twitter:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more." />
<meta name="twitter:image" content="https://mempool.space/resources/previews/mempool-space-preview.jpg" />
<meta name="twitter:domain" content="mempool.space">
<!-- END META -->
<!-- FAVICONS -->
<link rel="apple-touch-icon" sizes="180x180" href="/resources/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/resources/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/resources/favicons/favicon-16x16.png">
<link rel="manifest" href="/resources/favicons/site.webmanifest">
<link rel="shortcut icon" href="/resources/favicons/favicon.ico">
<link id="canonical" rel="canonical" href="https://mempool.space">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-config" content="/resources/favicons/browserconfig.xml">
<meta name="theme-color" content="#1d1f31">
<!-- END FAVICONS -->
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="Layer_1"
version="1.1"
viewBox="0 0 492.10001 575.79999"
width="492.10001"
height="575.79999"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
<defs
id="defs184">
<style
id="style182">
.st0 {
fill: #fff;
}
.st1 {
fill: #f88e2b;
}
</style>
</defs>
<g
id="g216"
transform="translate(-159.5,-152.1)">
<polygon
class="st0"
points="296.6,375.6 296.6,459.1 404.9,524.2 651.6,378.5 651.6,294.6 651.6,294.5 405.3,440.4 "
id="polygon212" />
<polygon
class="st1"
points="405.5,644.5 231.3,542 231.3,335.6 405.5,235 520.9,301.7 592.1,259.8 405.5,152.1 159.5,294.1 159.5,583.1 405.5,727.9 651.6,583.1 651.6,447.1 579.7,489.4 579.7,542 "
id="polygon214" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 996 B

-128
View File
@@ -1,128 +0,0 @@
import fs from 'fs';
const defaultConfig = {
"domains": ["mempool.space"],
"theme": "default",
"enterprise": "mempool",
"branding": {
"name": "mempool",
"title": "mempool",
"site_id": 5,
},
"meta": {
"title": "mempool - Bitcoin Explorer",
"description": "Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more.",
},
"unfurls": {
"preview": {
"src": "https://mempool.space/resources/previews/mempool-space-preview.jpg",
"type": "image/jpeg",
"width": "2000",
"height": "1000"
}
}
}
function deepMerge(target, source) {
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
target[key] = target[key] || {};
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
function addDefaultsToConfig(config) {
return deepMerge(structuredClone(defaultConfig), config);
}
function substitute(indexhtml, config) {
let newhtml = indexhtml;
// substitute title
newhtml = newhtml.replace(/\<\!\-\- TITLE \-\-\>.*\<\!\-\- END TITLE \-\-\>/gis, `<title>${config.meta.title}</title>`);
// substitute customization script
newhtml = newhtml.replace(/\<\!\-\- CUSTOMIZATION \-\-\>.*\<\!\-\- END CUSTOMIZATION \-\-\>/gis, `<script>
window.__env = window.__env || {};
window.__env.CUSTOMIZATION = 'auto';
window.__env.customize = ${JSON.stringify(config)};
</script>`);
// substitute meta tags
newhtml = newhtml.replace(/\<\!\-\- META \-\-\>.*\<\!\-\- END META \-\-\>/gis, `<meta name="description" content="${config.meta.description}" />
<meta property="og:image" content="${config.unfurls.preview.src}" />
<meta property="og:image:type" content="${config.unfurls.preview.type}" />
<meta property="og:image:width" content="${config.unfurls.preview.width}" />
<meta property="og:image:height" content="${config.unfurls.preview.height}" />
<meta property="og:description" content="${config.meta.description}" />
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@mempool">
<meta name="twitter:creator" content="@mempool">
<meta name="twitter:title" content="${config.meta.title}">
<meta name="twitter:description" content="${config.meta.description}" />
<meta name="twitter:image" content="${config.unfurls.preview.src}" />
<meta name="twitter:domain" content="${config.domains[0]}">`);
// substitute favicons
newhtml = newhtml.replace(/\<\!\-\- FAVICONS -->.*\<\!\-\- END FAVICONS \-\-\>/gis, `<link rel="apple-touch-icon" sizes="180x180" href="/resources/${config.enterprise}/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/resources/${config.enterprise}/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/resources/${config.enterprise}/favicons/favicon-16x16.png">
<link rel="manifest" href="/resources/${config.enterprise}/favicons/site.webmanifest">
<link rel="shortcut icon" href="/resources/${config.enterprise}/favicons/favicon.ico">
<link id="canonical" rel="canonical" href="${config.domains[0]}">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-config" content="/resources/${config.enterprise}/favicons/browserconfig.xml">
<meta name="theme-color" content="#1d1f31">`);
return newhtml;
}
async function run() {
const servicesHost = process.argv[2] || 'http://localhost:9000';
const mempoolDir = process.argv[3] || '../';
console.log('fetching list of custom builds');
const customBuilds = await (await fetch(`${servicesHost}/api/v1/services/custom/list`)).json();
// fetch config for each custom build from `$SERVICES/api/v1/services/custom/config/<custom_build_id>`
const customConfigs = await Promise.all(customBuilds.map(async (build) => {
console.log(`fetching config for ${build} `);
return addDefaultsToConfig(await (await fetch(`${servicesHost}/api/v1/services/custom/config/${build}`)).json());
}));
// for each custom build config:
let i = 0;
for (const config of customConfigs) {
console.log(`generating ${config.enterprise} build (${i + 1}/${customConfigs.length})`);
const browserDir = `${mempoolDir}/frontend/dist/mempool/browser`;
const locales = fs.readdirSync(browserDir)
.filter(file => fs.statSync(`${browserDir}/${file}`).isDirectory())
.filter(file => fs.existsSync(`${browserDir}/${file}/index.html`));
// Process each locale's index.html
for (const locale of locales) {
const indexPath = `${browserDir}/${locale}/index.html`;
const indexContent = fs.readFileSync(indexPath, 'utf-8').toString();
const processedHtml = substitute(indexContent, config);
// Save processed HTML
const outputPath = `${browserDir}/${locale}/index.${config.enterprise}.html`;
fs.writeFileSync(outputPath, processedHtml);
console.log(`updated index.${config.enterprise}.html for locale ${locale}`);
}
console.log(`finished generating ${config.enterprise} build`);
i++;
}
console.log('finished updating custom builds');
}
run();
-4
View File
@@ -154,9 +154,5 @@
"WALLETS": {
"ENABLED": true,
"WALLETS": ["BITB", "3350"]
},
"STRATUM": {
"ENABLED": true,
"API": "http://127.0.0.1:81/api/v1/stratum/ws"
}
}
@@ -4,7 +4,8 @@
"TESTNET4_ENABLED": true,
"LIQUID_ENABLED": false,
"LIQUID_TESTNET_ENABLED": false,
"STRATUM_ENABLED": true,
"BISQ_ENABLED": true,
"BISQ_SEPARATE_BACKEND": true,
"SIGNET_ENABLED": true,
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
+1 -2
View File
@@ -140,8 +140,7 @@ location @mempool-api-v1-cache-normal {
proxy_cache_valid 200 2s;
proxy_redirect off;
# cache for 2 seconds on server, but send expires -1 so browser doesn't cache
expires -1;
expires 2s;
}
location @mempool-api-v1-cache-disabled {