Compare commits

..

6 Commits

Author SHA1 Message Date
nymkappa
e3953a6dca
Merge branch 'master' into 5376-merge-attempt 2024-12-09 08:56:55 +01:00
nymkappa
78844f5787
Attempt to merge master into #5376 2024-12-09 08:54:26 +01:00
Mononaut
79e2883ebe
update unfurler and build config 2024-07-26 14:17:55 +00:00
Mononaut
fdbca80920
check in new resources 2024-07-26 14:17:12 +00:00
Mononaut
64baade3b3
custom dashboard wallet widgets 2024-07-26 14:17:12 +00:00
Mononaut
4d06636d83
wallet tracking backend support 2024-07-26 14:17:07 +00:00
143 changed files with 1180 additions and 6157 deletions

View File

@ -251,7 +251,17 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: 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 }} name: E2E tests for ${{ matrix.module }}
steps: steps:
@ -301,9 +311,7 @@ jobs:
- name: Unzip assets before building (src/resources) - name: Unzip assets before building (src/resources)
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
# mempool
- name: Chrome browser tests (${{ matrix.module }}) - name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'mempool' }}
uses: cypress-io/github-action@v5 uses: cypress-io/github-action@v5
with: with:
tag: ${{ github.event_name }} tag: ${{ github.event_name }}
@ -314,9 +322,7 @@ jobs:
wait-on-timeout: 120 wait-on-timeout: 120
record: true record: true
parallel: true parallel: true
spec: | spec: ${{ matrix.spec }}
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
group: Tests on Chrome (${{ matrix.module }}) group: Tests on Chrome (${{ matrix.module }})
browser: "chrome" browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
@ -326,56 +332,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# liquid
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'liquid' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:local-staging
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# testnet
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'testnet4' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:mempool
start: npm run start:local-staging
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/testnet4/*.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
CYPRESS_REROUTE_TESTNET: true
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
validate_docker_json: validate_docker_json:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"

View File

@ -7,7 +7,7 @@ const config: Config.InitialOptions = {
automock: false, automock: false,
collectCoverage: true, collectCoverage: true,
collectCoverageFrom: ["./src/**/**.ts"], collectCoverageFrom: ["./src/**/**.ts"],
coverageProvider: "v8", coverageProvider: "babel",
coverageThreshold: { coverageThreshold: {
global: { global: {
lines: 1 lines: 1

View File

@ -155,10 +155,6 @@
"API": "https://mempool.space/api/v1/services", "API": "https://mempool.space/api/v1/services",
"ACCELERATIONS": false "ACCELERATIONS": false
}, },
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
},
"FIAT_PRICE": { "FIAT_PRICE": {
"ENABLED": true, "ENABLED": true,
"PAID": false, "PAID": false,

View File

@ -10,6 +10,7 @@
"hasInstallScript": true, "hasInstallScript": true,
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"dependencies": { "dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
"axios": "1.7.2", "axios": "1.7.2",
@ -17,7 +18,7 @@
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.21.1", "express": "~4.21.1",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.12.0", "mysql2": "~3.11.0",
"redis": "^4.7.0", "redis": "^4.7.0",
"rust-gbt": "file:./rust-gbt", "rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0", "socks-proxy-agent": "~7.0.0",
@ -25,6 +26,8 @@
"ws": "~8.18.0" "ws": "~8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.25.2",
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
@ -5997,21 +6000,6 @@
"yallist": "^3.0.2" "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": { "node_modules/make-dir": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -6173,17 +6161,16 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/mysql2": { "node_modules/mysql2": {
"version": "3.12.0", "version": "3.11.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz",
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==", "integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==",
"license": "MIT",
"dependencies": { "dependencies": {
"aws-ssl-profiles": "^1.1.1", "aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0", "denque": "^2.1.0",
"generate-function": "^2.3.1", "generate-function": "^2.3.1",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"long": "^5.2.1", "long": "^5.2.1",
"lru.min": "^1.0.0", "lru-cache": "^8.0.0",
"named-placeholders": "^1.1.3", "named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5", "seq-queue": "^0.0.5",
"sqlstring": "^2.3.2" "sqlstring": "^2.3.2"
@ -6203,6 +6190,14 @@
"node": ">=0.10.0" "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": { "node_modules/named-placeholders": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
@ -12218,11 +12213,6 @@
"yallist": "^3.0.2" "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": { "make-dir": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -12337,16 +12327,16 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"mysql2": { "mysql2": {
"version": "3.12.0", "version": "3.11.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz",
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==", "integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==",
"requires": { "requires": {
"aws-ssl-profiles": "^1.1.1", "aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0", "denque": "^2.1.0",
"generate-function": "^2.3.1", "generate-function": "^2.3.1",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"long": "^5.2.1", "long": "^5.2.1",
"lru.min": "^1.0.0", "lru-cache": "^8.0.0",
"named-placeholders": "^1.1.3", "named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5", "seq-queue": "^0.0.5",
"sqlstring": "^2.3.2" "sqlstring": "^2.3.2"
@ -12359,6 +12349,11 @@
"requires": { "requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0" "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=="
} }
} }
}, },

View File

@ -39,6 +39,7 @@
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
"axios": "1.7.2", "axios": "1.7.2",
@ -46,7 +47,7 @@
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.21.1", "express": "~4.21.1",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.12.0", "mysql2": "~3.11.0",
"rust-gbt": "file:./rust-gbt", "rust-gbt": "file:./rust-gbt",
"redis": "^4.7.0", "redis": "^4.7.0",
"socks-proxy-agent": "~7.0.0", "socks-proxy-agent": "~7.0.0",
@ -54,6 +55,8 @@
"ws": "~8.18.0" "ws": "~8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.25.2",
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",

View File

@ -151,9 +151,5 @@
"ENABLED": true, "ENABLED": true,
"PAID": false, "PAID": false,
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__" "API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
},
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
} }
} }

View File

@ -159,11 +159,6 @@ describe('Mempool Backend Config', () => {
PAID: false, PAID: false,
API_KEY: '', API_KEY: '',
}); });
expect(config.STRATUM).toStrictEqual({
ENABLED: false,
API: 'http://localhost:1234',
});
}); });
}); });

View File

@ -3,10 +3,6 @@ import logger from '../../logger';
import bitcoinClient from './bitcoin-client'; import bitcoinClient from './bitcoin-client';
import config from '../../config'; import config from '../../config';
const BLOCKHASH_REGEX = /^[a-f0-9]{64}$/i;
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
/** /**
* Define a set of routes used by the accelerator server * Define a set of routes used by the accelerator server
* Those routes are not designed to be public * Those routes are not designed to be public
@ -14,7 +10,7 @@ const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
class BitcoinBackendRoutes { class BitcoinBackendRoutes {
private static tag = 'BitcoinBackendRoutes'; private static tag = 'BitcoinBackendRoutes';
public initRoutes(app: Application): void { public initRoutes(app: Application) {
app app
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction) .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
@ -51,9 +47,9 @@ class BitcoinBackendRoutes {
*/ */
private static handleException(e: any, fnName: string, res: Response): void { private static handleException(e: any, fnName: string, res: Response): void {
if (typeof(e.code) === 'number') { if (typeof(e.code) === 'number') {
res.status(400).send(JSON.stringify(e, ['code'])); res.status(400).send(JSON.stringify(e, ['code', 'message']));
} else { } else {
const err = `unknown exception in ${fnName}`; const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`;
logger.err(err, BitcoinBackendRoutes.tag); logger.err(err, BitcoinBackendRoutes.tag);
res.status(500).send(err); res.status(500).send(err);
} }
@ -62,13 +58,13 @@ class BitcoinBackendRoutes {
private async $getMempoolEntry(req: Request, res: Response): Promise<void> { private async $getMempoolEntry(req: Request, res: Response): Promise<void> {
const txid = req.query.txid; const txid = req.query.txid;
try { try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return; return;
} }
const mempoolEntry = await bitcoinClient.getMempoolEntry(txid); const mempoolEntry = await bitcoinClient.getMempoolEntry(txid);
if (!mempoolEntry) { if (!mempoolEntry) {
res.status(404).send(); res.status(404).send(`no mempool entry found for txid ${txid}`);
return; return;
} }
res.status(200).send(mempoolEntry); res.status(200).send(mempoolEntry);
@ -80,13 +76,13 @@ class BitcoinBackendRoutes {
private async $decodeRawTransaction(req: Request, res: Response): Promise<void> { private async $decodeRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx; const rawTx = req.body.rawTx;
try { try {
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) { if (typeof(rawTx) !== 'string') {
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`); res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
return; return;
} }
const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx); const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx);
if (!decodedTx) { if (!decodedTx) {
res.status(400).send(`unable to decode rawTx`); res.status(400).send(`unable to decode rawTx ${rawTx}`);
return; return;
} }
res.status(200).send(decodedTx); res.status(200).send(decodedTx);
@ -99,23 +95,23 @@ class BitcoinBackendRoutes {
const txid = req.query.txid; const txid = req.query.txid;
const verbose = req.query.verbose; const verbose = req.query.verbose;
try { try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return; return;
} }
if (typeof(verbose) !== 'string') { if (typeof(verbose) !== 'string') {
res.status(400).send(`invalid param verbose. must be a string representing an integer`); res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`);
return; return;
} }
const verboseNumber = parseInt(verbose, 10); const verboseNumber = parseInt(verbose, 10);
if (typeof(verboseNumber) !== 'number') { if (typeof(verboseNumber) !== 'number') {
res.status(400).send(`invalid param verbose. must be a valid integer`); res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`);
return; return;
} }
const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber); const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber);
if (!decodedTx) { if (!decodedTx) {
res.status(400).send(`unable to get raw transaction`); res.status(400).send(`unable to get raw transaction for txid ${txid}`);
return; return;
} }
res.status(200).send(decodedTx); res.status(200).send(decodedTx);
@ -127,13 +123,13 @@ class BitcoinBackendRoutes {
private async $sendRawTransaction(req: Request, res: Response): Promise<void> { private async $sendRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx; const rawTx = req.body.rawTx;
try { try {
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) { if (typeof(rawTx) !== 'string') {
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`); res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
return; return;
} }
const txHex = await bitcoinClient.sendRawTransaction(rawTx); const txHex = await bitcoinClient.sendRawTransaction(rawTx);
if (!txHex) { if (!txHex) {
res.status(400).send(`unable to send rawTx`); res.status(400).send(`unable to send rawTx ${rawTx}`);
return; return;
} }
res.status(200).send(txHex); res.status(200).send(txHex);
@ -145,13 +141,13 @@ class BitcoinBackendRoutes {
private async $testMempoolAccept(req: Request, res: Response): Promise<void> { private async $testMempoolAccept(req: Request, res: Response): Promise<void> {
const rawTxs = req.body.rawTxs; const rawTxs = req.body.rawTxs;
try { try {
if (typeof(rawTxs) !== 'object' || !Array.isArray(rawTxs) || rawTxs.some((tx) => typeof(tx) !== 'string' || !RAW_TX_REGEX.test(tx))) { if (typeof(rawTxs) !== 'object') {
res.status(400).send(`invalid param rawTxs. must be an array of strings of hexadecimal characters`); res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`);
return; return;
} }
const txHex = await bitcoinClient.testMempoolAccept(rawTxs); const txHex = await bitcoinClient.testMempoolAccept(rawTxs);
if (typeof(txHex) !== 'object' || txHex.length === 0) { if (typeof(txHex) !== 'object' || txHex.length === 0) {
res.status(400).send(`testmempoolaccept failed for raw txs, got an empty result`); res.status(400).send(`testmempoolaccept failed for raw txs ${JSON.stringify(rawTxs)}, got an empty result`);
return; return;
} }
res.status(200).send(txHex); res.status(200).send(txHex);
@ -164,18 +160,18 @@ class BitcoinBackendRoutes {
const txid = req.query.txid; const txid = req.query.txid;
const verbose = req.query.verbose; const verbose = req.query.verbose;
try { try {
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { if (typeof(txid) !== 'string' || txid.length !== 64) {
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
return; return;
} }
if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) { if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) {
res.status(400).send(`invalid param verbose. must be a string ('true' | 'false')`); res.status(400).send(`invalid param verbose ${verbose}. must be a string ('true' | 'false')`);
return; return;
} }
const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false); const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false);
if (!ancestors) { if (!ancestors) {
res.status(400).send(`unable to get mempool ancestors`); res.status(400).send(`unable to get mempool ancestors for txid ${txid}`);
return; return;
} }
res.status(200).send(ancestors); res.status(200).send(ancestors);
@ -188,23 +184,23 @@ class BitcoinBackendRoutes {
const blockHash = req.query.hash; const blockHash = req.query.hash;
const verbosity = req.query.verbosity; const verbosity = req.query.verbosity;
try { try {
if (typeof(blockHash) !== 'string' || blockHash.length !== 64 || !BLOCKHASH_REGEX.test(blockHash)) { if (typeof(blockHash) !== 'string' || blockHash.length !== 64) {
res.status(400).send(`invalid param blockHash. must be 64 hexadecimal characters`); res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`);
return; return;
} }
if (typeof(verbosity) !== 'string') { if (typeof(verbosity) !== 'string') {
res.status(400).send(`invalid param verbosity. must be a string representing an integer`); res.status(400).send(`invalid param verbosity ${verbosity}. must be a string representing an integer`);
return; return;
} }
const verbosityNumber = parseInt(verbosity, 10); const verbosityNumber = parseInt(verbosity, 10);
if (typeof(verbosityNumber) !== 'number') { if (typeof(verbosityNumber) !== 'number') {
res.status(400).send(`invalid param verbosity. must be a valid integer`); res.status(400).send(`invalid param verbosity ${verbosity}. must be a valid integer`);
return; return;
} }
const block = await bitcoinClient.getBlock(blockHash, verbosityNumber); const block = await bitcoinClient.getBlock(blockHash, verbosityNumber);
if (!block) { if (!block) {
res.status(400).send(`unable to get block`); res.status(400).send(`unable to get block for block hash ${blockHash}`);
return; return;
} }
res.status(200).send(block); res.status(200).send(block);
@ -217,18 +213,18 @@ class BitcoinBackendRoutes {
const blockHeight = req.query.height; const blockHeight = req.query.height;
try { try {
if (typeof(blockHeight) !== 'string') { if (typeof(blockHeight) !== 'string') {
res.status(400).send(`invalid param blockHeight, must be a string representing an integer`); res.status(400).send(`invalid param blockHeight ${blockHeight}, must be a string representing an integer`);
return; return;
} }
const blockHeightNumber = parseInt(blockHeight, 10); const blockHeightNumber = parseInt(blockHeight, 10);
if (typeof(blockHeightNumber) !== 'number') { if (typeof(blockHeightNumber) !== 'number') {
res.status(400).send(`invalid param blockHeight. must be a valid integer`); res.status(400).send(`invalid param blockHeight ${blockHeight}. must be a valid integer`);
return; return;
} }
const block = await bitcoinClient.getBlockHash(blockHeightNumber); const block = await bitcoinClient.getBlockHash(blockHeightNumber);
if (!block) { if (!block) {
res.status(400).send(`unable to get block hash`); res.status(400).send(`unable to get block hash for block height ${blockHeightNumber}`);
return; return;
} }
res.status(200).send(block); res.status(200).send(block);
@ -251,4 +247,4 @@ class BitcoinBackendRoutes {
} }
} }
export default new BitcoinBackendRoutes; export default new BitcoinBackendRoutes

View File

@ -21,12 +21,6 @@ import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache'; import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp'; import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api'; import { handleError } from '../../utils/api';
import poolsUpdater from '../../tasks/pools-updater';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i;
const ADDRESS_REGEX = /^[a-z0-9]{2,120}$/i;
const SCRIPT_HASH_REGEX = /^([a-f0-9]{2})+$/i;
class BitcoinRoutes { class BitcoinRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@ -57,10 +51,6 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
// Temporarily add txs/package endpoint for all backends until esplora supports it // Temporarily add txs/package endpoint for all backends until esplora supports it
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage) .post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
// Internal routes
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/list', this.getBlockDefinitionHashes)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/current', this.getCurrentBlockDefinitionHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/:definitionHash', this.getBlocksByDefinitionHash)
; ;
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
@ -100,7 +90,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json'); res.set('Content-Type', 'application/json');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get init data'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -119,7 +109,7 @@ class BitcoinRoutes {
const result = mempoolBlocks.getMempoolBlocks(); const result = mempoolBlocks.getMempoolBlocks();
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get mempool blocks'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -131,10 +121,7 @@ class BitcoinRoutes {
const txIds: string[] = []; const txIds: string[] = [];
for (const _txId in req.query.txId) { for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') { if (typeof req.query.txId[_txId] === 'string') {
const txid = req.query.txId[_txId].toString(); txIds.push(req.query.txId[_txId].toString());
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
} }
} }
@ -153,22 +140,18 @@ class BitcoinRoutes {
handleError(req, res, 400, 'Too many txids requested'); handleError(req, res, 400, 'Too many txids requested');
return; return;
} }
if (txids.some((txid) => !TXID_REGEX.test(txid))) {
handleError(req, res, 400, 'Invalid txids format');
return;
}
try { try {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends); res.json(batchedOutspends);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get batched outspends'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async $getCpfpInfo(req: Request, res: Response) { private async $getCpfpInfo(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) { if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`); handleError(req, res, 501, `Invalid transaction ID.`);
return; return;
} }
@ -201,7 +184,7 @@ class BitcoinRoutes {
try { try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get CPFP info'); handleError(req, res, 500, 'failed to get CPFP info');
return; return;
} }
} }
@ -222,10 +205,6 @@ class BitcoinRoutes {
} }
private async getTransaction(req: Request, res: Response) { private async getTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true); const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true);
res.json(transaction); res.json(transaction);
@ -233,18 +212,12 @@ class BitcoinRoutes {
let statusCode = 500; let statusCode = 500;
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
} }
handleError(req, res, statusCode, 'Failed to get transaction'); handleError(req, res, statusCode, e instanceof Error ? e.message : e);
} }
} }
private async getRawTransaction(req: Request, res: Response) { private async getRawTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true); const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
@ -253,10 +226,8 @@ class BitcoinRoutes {
let statusCode = 500; let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
} }
handleError(req, res, statusCode, 'Failed to get raw transaction'); handleError(req, res, statusCode, e instanceof Error ? e.message : e);
} }
} }
@ -321,18 +292,14 @@ class BitcoinRoutes {
} }
} catch (e: any) { } catch (e: any) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
handleError(req, res, 404, notFoundError); handleError(req, res, 404, e.message);
} else { } else {
handleError(req, res, 500, 'Failed to process PSBT'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }
private async getTransactionStatus(req: Request, res: Response) { private async getTransactionStatus(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true); const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction.status); res.json(transaction.status);
@ -340,54 +307,36 @@ class BitcoinRoutes {
let statusCode = 500; let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
return;
} }
handleError(req, res, statusCode, 'Failed to get transaction status'); handleError(req, res, statusCode, e instanceof Error ? e.message : e);
} }
} }
private async getStrippedBlockTransactions(req: Request, res: Response) { private async getStrippedBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash); const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block summary'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getStrippedBlockTransaction(req: Request, res: Response) { private async getStrippedBlockTransaction(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
if (!TXID_REGEX.test(req.params.txid)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid); const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
if (!transaction) { if (!transaction) {
handleError(req, res, 404, `Transaction not found in summary`); handleError(req, res, 404, `transaction not found in summary`);
return; return;
} }
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transaction); res.json(transaction);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get transaction from summary'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
private async getBlock(req: Request, res: Response) { private async getBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const block = await blocks.$getBlock(req.params.hash); const block = await blocks.$getBlock(req.params.hash);
@ -399,69 +348,53 @@ class BitcoinRoutes {
} else if (blockAge > 30 * day) { } else if (blockAge > 30 * day) {
cacheDuration = 10 * day; cacheDuration = 10 * day;
} else { } else {
cacheDuration = 600; cacheDuration = 600
} }
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block); res.json(block);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getBlockHeader(req: Request, res: Response) { private async getBlockHeader(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash); const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash);
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(blockHeader); res.send(blockHeader);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block header'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getBlockAuditSummary(req: Request, res: Response) { private async getBlockAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash); const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
if (auditSummary) { if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
handleError(req, res, 404, `Audit not available`); handleError(req, res, 404, `audit not available`);
return; return;
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block audit summary'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async $getBlockTxAuditSummary(req: Request, res: Response) { private async $getBlockTxAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
if (!TXID_REGEX.test(req.params.txid)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid); const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
if (auditSummary) { if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
handleError(req, res, 404, `Transaction audit not available`); handleError(req, res, 404, `transaction audit not available`);
return; return;
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get transaction audit summary'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
@ -475,7 +408,7 @@ class BitcoinRoutes {
return await this.getLegacyBlocks(req, res); return await this.getLegacyBlocks(req, res);
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get blocks'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -517,7 +450,7 @@ class BitcoinRoutes {
res.json(await blocks.$getBlocksBetweenHeight(from, to)); res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get blocks'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -552,15 +485,11 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks); res.json(returnBlocks);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get blocks'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getBlockTransactions(req: Request, res: Response) { private async getBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@ -581,7 +510,7 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
handleError(req, res, 500, 'Failed to get block transactions'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -590,7 +519,7 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10)); const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash); res.send(blockHash);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block at height'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -599,20 +528,16 @@ class BitcoinRoutes {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try { try {
const addressData = await bitcoinApi.$getAddress(req.params.address); const addressData = await bitcoinApi.$getAddress(req.params.address);
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message); handleError(req, res, 413, e instanceof Error ? e.message : e);
return; return;
} }
handleError(req, res, 500, 'Failed to get address'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -621,10 +546,6 @@ class BitcoinRoutes {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try { try {
let lastTxId: string = ''; let lastTxId: string = '';
@ -635,10 +556,10 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message); handleError(req, res, 413, e instanceof Error ? e.message : e);
return; return;
} }
handleError(req, res, 500, 'Failed to get address transactions'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -654,10 +575,6 @@ class BitcoinRoutes {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
return;
}
try { try {
// electrum expects scripthashes in little-endian // electrum expects scripthashes in little-endian
@ -666,10 +583,10 @@ class BitcoinRoutes {
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message); handleError(req, res, 413, e instanceof Error ? e.message : e);
return; return;
} }
handleError(req, res, 500, 'Failed to get script hash'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -678,10 +595,6 @@ class BitcoinRoutes {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
return;
}
try { try {
// electrum expects scripthashes in little-endian // electrum expects scripthashes in little-endian
@ -694,10 +607,10 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e.message); handleError(req, res, 413, e instanceof Error ? e.message : e);
return; return;
} }
handleError(req, res, 500, 'Failed to get script hash transactions'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -710,10 +623,10 @@ class BitcoinRoutes {
private async getAddressPrefix(req: Request, res: Response) { private async getAddressPrefix(req: Request, res: Response) {
try { try {
const addressPrefix = await bitcoinApi.$getAddressPrefix(req.params.prefix); const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(addressPrefix); res.send(blockHash);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get address prefix'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -744,52 +657,6 @@ class BitcoinRoutes {
} }
} }
private async getBlockDefinitionHashes(req: Request, res: Response): Promise<void> {
try {
const result = await blocks.$getBlockDefinitionHashes();
if (!result) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'application/json');
res.send(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getCurrentBlockDefinitionHash(req: Request, res: Response): Promise<void> {
try {
const currentSha = await poolsUpdater.getShaFromDb();
if (!currentSha) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'text/plain');
res.send(currentSha);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getBlocksByDefinitionHash(req: Request, res: Response): Promise<void> {
try {
if (typeof(req.params.definitionHash) !== 'string') {
res.status(400).send('Parameter "hash" must be a valid string');
return;
}
const blocksHash = await blocks.$getBlocksByDefinitionHash(req.params.definitionHash as string);
if (!blocksHash) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'application/json');
res.send(blocksHash);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private getBlockTipHeight(req: Request, res: Response) { private getBlockTipHeight(req: Request, res: Response) {
try { try {
const result = blocks.getCurrentBlockHeight(); const result = blocks.getCurrentBlockHeight();
@ -800,7 +667,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(result.toString()); res.send(result.toString());
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get height at tip'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -810,55 +677,39 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get hash at tip'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getRawBlock(req: Request, res: Response) { private async getRawBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const result = await bitcoinApi.$getRawBlock(req.params.hash); const result = await bitcoinApi.$getRawBlock(req.params.hash);
res.setHeader('content-type', 'application/octet-stream'); res.setHeader('content-type', 'application/octet-stream');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get raw block'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getTxIdsForBlock(req: Request, res: Response) { private async getTxIdsForBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get txids for block'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async validateAddress(req: Request, res: Response) { private async validateAddress(req: Request, res: Response) {
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try { try {
const result = await bitcoinClient.validateAddress(req.params.address); const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to validate address'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getRbfHistory(req: Request, res: Response) { private async getRbfHistory(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const replacements = rbfCache.getRbfTree(req.params.txId) || null; const replacements = rbfCache.getRbfTree(req.params.txId) || null;
const replaces = rbfCache.getReplaces(req.params.txId) || null; const replaces = rbfCache.getReplaces(req.params.txId) || null;
@ -867,7 +718,7 @@ class BitcoinRoutes {
replaces replaces
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get rbf history'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -876,7 +727,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(false); const result = rbfCache.getRbfTrees(false);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get rbf trees'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -885,15 +736,11 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(true); const result = rbfCache.getRbfTrees(true);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get full rbf replacements'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getCachedTx(req: Request, res: Response) { private async getCachedTx(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const result = rbfCache.getTx(req.params.txId); const result = rbfCache.getTx(req.params.txId);
if (result) { if (result) {
@ -902,20 +749,16 @@ class BitcoinRoutes {
res.status(204).send(); res.status(204).send();
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get cached tx'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
private async getTransactionOutspends(req: Request, res: Response) { private async getTransactionOutspends(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const result = await bitcoinApi.$getOutspends(req.params.txId); const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get transaction outspends'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -928,7 +771,7 @@ class BitcoinRoutes {
handleError(req, res, 503, `Service Temporarily Unavailable`); handleError(req, res, 503, `Service Temporarily Unavailable`);
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get difficulty change'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -939,8 +782,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code }) handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: 'Failed to send raw transaction'); : (e.message || 'Error'));
} }
} }
@ -951,8 +794,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex); const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code }) handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: 'Failed to send raw transaction'); : (e.message || 'Error'));
} }
} }
@ -963,8 +806,8 @@ class BitcoinRoutes {
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result); res.send(result);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code }) handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: 'Failed to test transactions'); : (e.message || 'Error'));
} }
} }
@ -976,8 +819,8 @@ class BitcoinRoutes {
const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined); const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined);
res.send(result); res.send(result);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code }) handleError(req, res, 400, e.message && e.code ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: 'Failed to submit package'); : (e.message || 'Error'));
} }
} }

View File

@ -1,12 +1,12 @@
import config from '../../config'; import config from '../../config';
import axios, { isAxiosError } from 'axios'; import axios, { AxiosResponse, isAxiosError } from 'axios';
import http from 'http'; import http from 'http';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger'; import logger from '../../logger';
import { Common } from '../common'; import { Common } from '../common';
import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import os from 'os';
interface FailoverHost { interface FailoverHost {
host: string, host: string,
rtts: number[], rtts: number[],
@ -20,13 +20,6 @@ interface FailoverHost {
preferred?: boolean, preferred?: boolean,
checked: boolean, checked: boolean,
lastChecked?: number, lastChecked?: number,
publicDomain: string,
hashes: {
frontend?: string,
backend?: string,
electrs?: string,
lastUpdated: number,
}
} }
class FailoverRouter { class FailoverRouter {
@ -36,21 +29,14 @@ class FailoverRouter {
maxHeight: number = 0; maxHeight: number = 0;
hosts: FailoverHost[]; hosts: FailoverHost[];
multihost: boolean; multihost: boolean;
gitHashInterval: number = 600000; // 10 minutes pollInterval: number = 60000;
pollInterval: number = 60000; // 1 minute
pollTimer: NodeJS.Timeout | null = null; pollTimer: NodeJS.Timeout | null = null;
pollConnection = axios.create(); pollConnection = axios.create();
localHostname: string = 'localhost';
requestConnection = axios.create({ requestConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true }) httpAgent: new http.Agent({ keepAlive: true })
}); });
constructor() { constructor() {
try {
this.localHostname = os.hostname();
} catch (e) {
logger.warn('Failed to set local hostname, using "localhost"');
}
// setup list of hosts // setup list of hosts
this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => { this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
return { return {
@ -59,10 +45,6 @@ class FailoverRouter {
rtts: [], rtts: [],
rtt: Infinity, rtt: Infinity,
failures: 0, failures: 0,
publicDomain: 'https://' + this.extractPublicDomain(domain),
hashes: {
lastUpdated: 0,
},
}; };
}); });
this.activeHost = { this.activeHost = {
@ -73,10 +55,6 @@ class FailoverRouter {
socket: !!config.ESPLORA.UNIX_SOCKET_PATH, socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
preferred: true, preferred: true,
checked: false, checked: false,
publicDomain: `http://${this.localHostname}`,
hashes: {
lastUpdated: 0,
},
}; };
this.fallbackHost = this.activeHost; this.fallbackHost = this.activeHost;
this.hosts.unshift(this.activeHost); this.hosts.unshift(this.activeHost);
@ -128,24 +106,6 @@ class FailoverRouter {
host.outOfSync = false; host.outOfSync = false;
} }
host.unreachable = false; host.unreachable = false;
// update esplora git hash using the x-powered-by header from the height check
const poweredBy = result.headers['x-powered-by'];
if (poweredBy) {
const match = poweredBy.match(/([a-fA-F0-9]{5,40})/);
if (match && match[1]?.length) {
host.hashes.electrs = match[1];
}
}
// Check front and backend git hashes less often
if (Date.now() - host.hashes.lastUpdated > this.gitHashInterval) {
await Promise.all([
this.$updateFrontendGitHash(host),
this.$updateBackendGitHash(host)
]);
host.hashes.lastUpdated = Date.now();
}
} else { } else {
host.outOfSync = true; host.outOfSync = true;
host.unreachable = true; host.unreachable = true;
@ -242,47 +202,6 @@ class FailoverRouter {
} }
} }
// methods for retrieving git hashes by host
private async $updateFrontendGitHash(host: FailoverHost): Promise<void> {
try {
const url = `${host.publicDomain}/resources/config.js`;
const response = await this.pollConnection.get<string>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
const match = response.data.match(/GIT_COMMIT_HASH\s*=\s*['"](.*?)['"]/);
if (match && match[1]?.length) {
host.hashes.frontend = match[1];
}
} catch (e) {
// failed to get frontend build hash - do nothing
}
}
private async $updateBackendGitHash(host: FailoverHost): Promise<void> {
try {
const url = `${host.publicDomain}/api/v1/backend-info`;
const response = await this.pollConnection.get<any>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
if (response.data?.gitCommit) {
host.hashes.backend = response.data.gitCommit;
}
} catch (e) {
// failed to get backend build hash - do nothing
}
}
// returns the public mempool domain corresponding to an esplora server url
// (a bit of a hack to avoid manually specifying frontend & backend URLs for each esplora server)
private extractPublicDomain(url: string): string {
// force the url to start with a valid protocol
const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;
// parse as URL and extract the hostname
try {
const parsed = new URL(urlWithProtocol);
return parsed.hostname;
} catch (e) {
// fallback to the original url
return url;
}
}
private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> { private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> {
let axiosConfig; let axiosConfig;
let url; let url;
@ -462,7 +381,6 @@ class ElectrsApi implements AbstractBitcoinApi {
unreachable: !!host.unreachable, unreachable: !!host.unreachable,
checked: !!host.checked, checked: !!host.checked,
lastChecked: host.lastChecked || 0, lastChecked: host.lastChecked || 0,
hashes: host.hashes,
})); }));
} else { } else {
return []; return [];

View File

@ -33,8 +33,8 @@ import AccelerationRepository from '../repositories/AccelerationRepository';
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp'; import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool'; import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository'; import CpfpRepository from '../repositories/CpfpRepository';
import accelerationApi from './services/acceleration';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script'; import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
import database from '../database';
class Blocks { class Blocks {
private blocks: BlockExtended[] = []; private blocks: BlockExtended[] = [];
@ -1462,36 +1462,6 @@ class Blocks {
// not a fatal error, we'll try again next time the indexer runs // not a fatal error, we'll try again next time the indexer runs
} }
} }
public async $getBlockDefinitionHashes(): Promise<string[] | null> {
try {
const [rows]: any = await database.query(`SELECT DISTINCT(definition_hash) FROM blocks`);
if (rows && Array.isArray(rows)) {
return rows.map(r => r.definition_hash);
} else {
logger.debug(`Unable to retreive list of blocks.definition_hash from db (no result)`);
return null;
}
} catch (e) {
logger.debug(`Unable to retreive list of blocks.definition_hash from db (exception: ${e})`);
return null;
}
}
public async $getBlocksByDefinitionHash(definitionHash: string): Promise<string[] | null> {
try {
const [rows]: any = await database.query(`SELECT hash FROM blocks WHERE definition_hash = ?`, [definitionHash]);
if (rows && Array.isArray(rows)) {
return rows.map(r => r.hash);
} else {
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (no result)`);
return null;
}
} catch (e) {
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (exception: ${e})`);
return null;
}
}
} }
export default new Blocks(); export default new Blocks();

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 95; private static currentVersion = 93;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -801,335 +801,6 @@ class DatabaseMigration {
`); `);
await this.updateToSchemaVersion(93); await this.updateToSchemaVersion(93);
} }
// Unify database schema for all mempool netwoks
// versions above 94 should not use network-specific flags
if (databaseSchemaVersion < 94) {
if (!isBitcoin) {
// Apply all the bitcoin specific migrations to non-bitcoin networks: liquid, liquidtestnet and testnet4 (!)
// Version 5
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
// Version 6
await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`');
await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT');
await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL');
await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)');
await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
// Version 7
await this.$executeQuery('DROP table IF EXISTS hashrates;');
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
// Version 8
await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
// Version 9
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
// Version 10
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
// Version 11
await this.$executeQuery(`ALTER TABLE blocks
ADD avg_fee INT UNSIGNED NULL,
ADD avg_fee_rate INT UNSIGNED NULL
`);
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
// Version 12
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 13
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 14
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 17
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
// Version 18
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
// Version 20
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
// Version 22
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
// Version 24
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
// Version 25
await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats'));
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
// Version 26
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
// Version 27
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
// Version 28
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
// Version 29
await this.$executeQuery(this.getCreateGeoNamesTableQuery(), await this.$checkIfTableExists('geo_names'));
await this.$executeQuery('ALTER TABLE `nodes` ADD as_number int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD city_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD country_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD accuracy_radius int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
// Version 30
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
// Version 31
await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
// Version 32
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
// Version 33
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
// Version 34
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
// Version 35
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
// Version 36
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
// Version 37
await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
// Version 38
await this.$executeQuery(`TRUNCATE lightning_stats`);
await this.$executeQuery(`TRUNCATE node_stats`);
await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
await this.updateToSchemaVersion(38);
// Version 39
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
// Version 40
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
// Version 41
await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
// Version 42
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
// Version 43
await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
// Version 44
await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
// Version 45
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"');
// Version 48
await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"');
// Version 57
await this.$executeQuery(`ALTER TABLE nodes MODIFY updated_at datetime NULL`);
// Version 60
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"');
// Version 61
if (! await this.$checkIfTableExists('blocks_templates')) {
await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"');
}
await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"');
await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)');
await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template');
// Version 62
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_fees BIGINT UNSIGNED DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_weight BIGINT UNSIGNED DEFAULT NULL');
// Version 63
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
// Version 64
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
// Version 65
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
// Version 67
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)');
// Version 76
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
// Version 81
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
// Version 83
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
// Version 84
await this.$executeQuery(`
ALTER TABLE \`pools\`
ADD INDEX \`slug\` (\`slug\`),
ADD INDEX \`unique_id\` (\`unique_id\`)
`);
// Version 85
await this.$executeQuery(`
ALTER TABLE \`channels\`
ADD INDEX \`created\` (\`created\`),
ADD INDEX \`capacity\` (\`capacity\`),
ADD INDEX \`closing_reason\` (\`closing_reason\`),
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
`);
// Version 86
await this.$executeQuery(`
ALTER TABLE \`nodes\`
ADD INDEX \`status\` (\`status\`),
ADD INDEX \`channels\` (\`channels\`),
ADD INDEX \`country_id\` (\`country_id\`),
ADD INDEX \`as_number\` (\`as_number\`),
ADD INDEX \`first_seen\` (\`first_seen\`)
`);
// Version 87
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(87);
// Version 88
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
// Version 89
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
// Version 90
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
// Version 91
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
}
if (config.MEMPOOL.NETWORK !== 'liquid') {
// Apply all the liquid specific migrations to all other networks
// Version 68
await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);');
await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses'));
await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos'));
// Version 71
await this.$executeQuery('ALTER TABLE `federation_txos` ADD timelock INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `federation_txos` ADD expiredAt INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `federation_txos` ADD emergencyKey TINYINT NOT NULL DEFAULT 0');
// Version 92
await this.$executeQuery(`
ALTER TABLE \`elements_pegs\`
ADD INDEX \`block\` (\`block\`),
ADD INDEX \`datetime\` (\`datetime\`),
ADD INDEX \`amount\` (\`amount\`),
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
`);
// Version 93
await this.$executeQuery(`
ALTER TABLE \`federation_txos\`
ADD INDEX \`unspent\` (\`unspent\`),
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
ADD INDEX \`blocktime\` (\`blocktime\`),
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
ADD INDEX \`expiredAt\` (\`expiredAt\`)
`);
}
if (config.MEMPOOL.NETWORK !== 'mainnet') {
// Apply all the mainnet specific migrations to all other networks
// Version 69
await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations'));
// Version 70
await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;');
// Version 77
await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL');
}
await this.updateToSchemaVersion(94);
}
// blocks pools-v2.json hash
if (databaseSchemaVersion < 95) {
let poolJsonSha = 'f737d86571d190cf1a1a3cf5fd86b33ba9624254';
const [poolJsonShaDb]: any[] = await DB.query(`SELECT string FROM state WHERE name = 'pools_json_sha'`);
if (poolJsonShaDb?.length > 0) {
poolJsonSha = poolJsonShaDb[0].string;
}
await this.$executeQuery(`ALTER TABLE blocks ADD definition_hash varchar(255) NOT NULL DEFAULT "${poolJsonSha}"`);
await this.$executeQuery('ALTER TABLE blocks ADD INDEX `definition_hash` (`definition_hash`)');
await this.updateToSchemaVersion(95);
}
} }
/** /**

View File

@ -3,8 +3,6 @@ import { Application, Request, Response } from 'express';
import channelsApi from './channels.api'; import channelsApi from './channels.api';
import { handleError } from '../../utils/api'; import { handleError } from '../../utils/api';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
class ChannelsRoutes { class ChannelsRoutes {
constructor() { } constructor() { }
@ -25,7 +23,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search); const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to search channels by id'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -41,7 +39,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel); res.json(channel);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get channel'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -72,7 +70,7 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString()); res.header('X-Total-Count', channelsCount.toString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get channels for node'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -85,10 +83,7 @@ class ChannelsRoutes {
const txIds: string[] = []; const txIds: string[] = [];
for (const _txId in req.query.txId) { for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') { if (typeof req.query.txId[_txId] === 'string') {
const txid = req.query.txId[_txId].toString(); txIds.push(req.query.txId[_txId].toString());
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
} }
} }
const channels = await channelsApi.$getChannelsByTransactionId(txIds); const channels = await channelsApi.$getChannelsByTransactionId(txIds);
@ -113,7 +108,7 @@ class ChannelsRoutes {
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get channels by transaction ids'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -125,7 +120,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get penalty closed channels'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -138,7 +133,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get channel geodata'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }

View File

@ -29,7 +29,7 @@ class GeneralLightningRoutes {
channels: channels, channels: channels,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to search for nodes and channels'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -43,7 +43,7 @@ class GeneralLightningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics); res.json(statistics);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get lightning statistics'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -52,7 +52,7 @@ class GeneralLightningRoutes {
const statistics = await statisticsApi.$getLatestStatistics(); const statistics = await statisticsApi.$getLatestStatistics();
res.json(statistics); res.json(statistics);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get lightning statistics'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }

View File

@ -32,7 +32,7 @@ class NodesRoutes {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search); const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes); res.json(nodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to search for node'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -188,7 +188,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes); res.json(nodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get node group'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -204,7 +204,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node); res.json(node);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get node'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -216,7 +216,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics); res.json(statistics);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical node stats'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -232,7 +232,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node); res.json(node);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get fee histogram'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -248,7 +248,7 @@ class NodesRoutes {
topByChannels: topChannelsNodes, topByChannels: topChannelsNodes,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get nodes ranking'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -260,7 +260,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get top nodes by capacity'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -272,7 +272,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get top nodes by channels'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -284,7 +284,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get oldest nodes'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -296,7 +296,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs); res.json(nodesPerAs);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get ISP ranking'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -308,7 +308,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes); res.json(worldNodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get world nodes'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -336,7 +336,7 @@ class NodesRoutes {
nodes: nodes, nodes: nodes,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get nodes per country'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -363,7 +363,7 @@ class NodesRoutes {
nodes: nodes, nodes: nodes,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get nodes per ISP'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -375,7 +375,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs); res.json(nodesPerAs);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get nodes per country'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }

View File

@ -83,7 +83,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(pegs); res.json(pegs);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pegs by month'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -95,7 +95,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(reserves); res.json(reserves);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get reserves by month'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -107,7 +107,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentSupply); res.json(currentSupply);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pegs'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -119,7 +119,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentReserves); res.json(currentReserves);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get reserves'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -131,7 +131,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(auditStatus); res.json(auditStatus);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get federation audit status'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -143,7 +143,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses); res.json(federationAddresses);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get federation addresses'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -155,7 +155,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses); res.json(federationAddresses);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get federation addresses'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -167,7 +167,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos); res.json(federationUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get federation utxos'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -179,7 +179,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(expiredUtxos); res.json(expiredUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get expired utxos'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -191,7 +191,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos); res.json(federationUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get federation utxos number'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -203,7 +203,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos); res.json(emergencySpentUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get emergency spent utxos'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -215,7 +215,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos); res.json(emergencySpentUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get emergency spent utxos stats'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -227,7 +227,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(recentPegs); res.json(recentPegs);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pegs list'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -239,7 +239,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsVolume); res.json(pegsVolume);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pegs volume daily'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -251,7 +251,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsCount); res.json(pegsCount);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pegs count'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }

View File

@ -72,7 +72,7 @@ class MiningRoutes {
} }
res.status(200).send(response); res.status(200).send(response);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical prices'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -87,7 +87,7 @@ class MiningRoutes {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message); handleError(req, res, 404, e.message);
} else { } else {
handleError(req, res, 500, 'Failed to get pool'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }
@ -106,7 +106,7 @@ class MiningRoutes {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message); handleError(req, res, 404, e.message);
} else { } else {
handleError(req, res, 500, 'Failed to get blocks for pool'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }
@ -130,7 +130,7 @@ class MiningRoutes {
res.json(pools); res.json(pools);
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pools'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -144,7 +144,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats); res.json(stats);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pools'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -158,7 +158,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates); res.json(hashrates);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get pools historical hashrate'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -175,7 +175,7 @@ class MiningRoutes {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message); handleError(req, res, 404, e.message);
} else { } else {
handleError(req, res, 500, 'Failed to get pool historical hashrate'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }
@ -204,7 +204,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty, currentDifficulty: currentDifficulty,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical hashrate'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -218,7 +218,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical block fees'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -236,7 +236,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical block fees'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -250,7 +250,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards); res.json(blockRewards);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical block rewards'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -264,7 +264,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates); res.json(blockFeeRates);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical block fee rates'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -282,7 +282,7 @@ class MiningRoutes {
weights: blockWeights weights: blockWeights
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical block size and weight'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -294,7 +294,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical difficulty adjustments'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -304,7 +304,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(response); res.json(response);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get reward stats'); res.status(500).end();
} }
} }
@ -318,7 +318,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate])); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get historical blocks health'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -336,7 +336,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit); res.json(audit);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block audit'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -359,7 +359,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get height from timestamp'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -372,7 +372,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15)); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block audit scores'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -385,7 +385,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit || 'null'); res.json(audit || 'null');
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get block audit score'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -400,7 +400,7 @@ class MiningRoutes {
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get accelerations by pool'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -416,7 +416,7 @@ class MiningRoutes {
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get accelerations by height'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -431,7 +431,7 @@ class MiningRoutes {
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get recent accelerations'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -446,7 +446,7 @@ class MiningRoutes {
} }
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get acceleration totals'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -461,7 +461,7 @@ class MiningRoutes {
} }
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {})); res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get active accelerations'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
@ -473,7 +473,7 @@ class MiningRoutes {
accelerationApi.accelerationRequested(req.params.txid); accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send(); res.status(200).send();
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to request acceleration'); handleError(req, res, 500, e instanceof Error ? e.message : e);
} }
} }
} }

View File

@ -19,6 +19,15 @@ class PoolsParser {
'addresses': '[]', 'addresses': '[]',
'slug': 'unknown' 'slug': 'unknown'
}; };
private uniqueLogs: string[] = [];
private uniqueLog(loggerFunction: any, msg: string): void {
if (this.uniqueLogs.includes(msg)) {
return;
}
this.uniqueLogs.push(msg);
loggerFunction(msg);
}
public setMiningPools(pools): void { public setMiningPools(pools): void {
for (const pool of pools) { for (const pool of pools) {

View File

@ -119,11 +119,7 @@ class RbfCache {
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void { public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if ( !newTxExtended if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|| !replaced?.length
|| this.txs.has(newTxExtended.txid)
|| !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
) {
return; return;
} }

View File

@ -1,7 +1,6 @@
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import config from '../../config'; import config from '../../config';
import WalletApi from './wallets'; import WalletApi from './wallets';
import { handleError } from '../../utils/api';
class ServicesRoutes { class ServicesRoutes {
public initRoutes(app: Application): void { public initRoutes(app: Application): void {
@ -19,7 +18,7 @@ class ServicesRoutes {
const wallet = await WalletApi.getWallet(walletId); const wallet = await WalletApi.getWallet(walletId);
res.status(200).send(wallet); res.status(200).send(wallet);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get wallet'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
} }

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();

View File

@ -19,8 +19,13 @@ interface WalletAddress {
lastSync: number; lastSync: number;
} }
interface Wallet { interface WalletConfig {
url: string;
name: string; name: string;
apiKey: string;
}
interface Wallet extends WalletConfig {
addresses: Record<string, WalletAddress>; addresses: Record<string, WalletAddress>;
lastPoll: number; lastPoll: number;
} }
@ -32,10 +37,10 @@ class WalletApi {
private syncing = false; private syncing = false;
constructor() { constructor() {
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => { this.wallets = (config.WALLETS.WALLETS as WalletConfig[]).reduce((acc, wallet) => {
acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 }; acc[wallet.name] = { ...wallet, addresses: {}, lastPoll: 0 };
return acc; return acc;
}, {} as Record<string, Wallet>) : {}; }, {} as Record<string, Wallet>);
} }
public getWallet(wallet: string): Record<string, WalletAddress> { public getWallet(wallet: string): Record<string, WalletAddress> {
@ -52,16 +57,18 @@ class WalletApi {
const wallet = this.wallets[walletKey]; const wallet = this.wallets[walletKey];
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) { if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
try { try {
const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets/${wallet.name}`); const response = await axios.get(`${wallet.url}/${wallet.name}`, { headers: { 'Authorization': `${wallet.apiKey}` } });
const addresses: Record<string, WalletAddress> = response.data; const data: { walletBalances: WalletAddress[] } = response.data;
const addressList: WalletAddress[] = Object.values(addresses); const addresses = data.walletBalances;
const newAddresses: Record<string, boolean> = {};
// sync all current addresses // sync all current addresses
for (const address of addressList) { for (const address of addresses) {
await this.$syncWalletAddress(wallet, address); await this.$syncWalletAddress(wallet, address);
newAddresses[address.address] = true;
} }
// remove old addresses // remove old addresses
for (const address of Object.keys(wallet.addresses)) { for (const address of Object.keys(wallet.addresses)) {
if (!addresses[address]) { if (!newAddresses[address]) {
delete wallet.addresses[address]; delete wallet.addresses[address];
} }
} }
@ -86,10 +93,11 @@ class WalletApi {
const walletAddress: WalletAddress = { const walletAddress: WalletAddress = {
address: address.address, address: address.address,
active: address.active, active: address.active,
transactions: summary, transactions: await bitcoinApi.$getAddressTransactionSummary(address.address),
stats: addressInfo.chain_stats, stats: addressInfo.chain_stats,
lastSync: Date.now(), lastSync: Date.now(),
}; };
logger.debug(`Synced ${walletAddress.transactions?.length || 0} transactions for wallet ${wallet.name} address ${address.address}`);
wallet.addresses[address.address] = walletAddress; wallet.addresses[address.address] = walletAddress;
} catch (e) { } catch (e) {
logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`); logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`);
@ -142,7 +150,17 @@ class WalletApi {
wallet.addresses[address].transactions?.push(txSummary); wallet.addresses[address].transactions?.push(txSummary);
} }
if (anyMatch) { if (anyMatch) {
walletTransactions[walletKey].push(tx); for (const address of Object.keys({ ...funded, ...spent })) {
if (!walletTransactions[walletKey][address]) {
walletTransactions[walletKey][address] = [];
}
walletTransactions[walletKey][address].push({
txid: tx.txid,
value: (funded[address] ?? 0) - (spent[address] ?? 0),
height: block.height,
time: block.timestamp,
});
}
} }
} }
} }

View File

@ -1,7 +1,7 @@
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import config from '../../config'; import config from '../../config';
import statisticsApi from './statistics-api'; import statisticsApi from './statistics-api';
import { handleError } from '../../utils/api';
class StatisticsRoutes { class StatisticsRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
app app
@ -65,7 +65,7 @@ class StatisticsRoutes {
} }
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'Failed to get statistics'); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
} }

View File

@ -38,7 +38,6 @@ interface AddressTransactions {
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { calculateMempoolTxCpfp } from './cpfp'; import { calculateMempoolTxCpfp } from './cpfp';
import { getRecentFirstSeen } from '../utils/file-read'; import { getRecentFirstSeen } from '../utils/file-read';
import stratumApi, { StratumJob } from './services/stratum';
// valid 'want' subscriptions // valid 'want' subscriptions
const wantable = [ const wantable = [
@ -404,16 +403,6 @@ class WebsocketHandler {
delete client['track-mempool']; delete client['track-mempool'];
} }
if (parsedMessage && parsedMessage['track-stratum'] != null) {
if (parsedMessage['track-stratum']) {
const sub = parsedMessage['track-stratum'];
client['track-stratum'] = sub;
response['stratumJobs'] = this.socketData['stratumJobs'];
} else {
client['track-stratum'] = false;
}
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
client.send(this.serializeResponse(response)); client.send(this.serializeResponse(response));
} }
@ -1395,23 +1384,6 @@ class WebsocketHandler {
await statistics.runStatistics(); await statistics.runStatistics();
} }
public handleNewStratumJob(job: StratumJob): void {
this.updateSocketDataFields({ 'stratumJobs': stratumApi.getJobs() });
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
if (client['track-stratum'] && (client['track-stratum'] === 'all' || client['track-stratum'] === job.pool)) {
client.send(JSON.stringify({
'stratumJob': job
}));
}
});
}
}
// takes a dictionary of JSON serialized values // takes a dictionary of JSON serialized values
// and zips it together into a valid JSON object // and zips it together into a valid JSON object
private serializeResponse(response): string { private serializeResponse(response): string {

View File

@ -164,11 +164,11 @@ interface IConfig {
}, },
WALLETS: { WALLETS: {
ENABLED: boolean; ENABLED: boolean;
WALLETS: string[]; WALLETS: {
}, url: string;
STRATUM: { name: string;
ENABLED: boolean; apiKey: string;
API: string; }[];
} }
} }
@ -336,10 +336,6 @@ const defaults: IConfig = {
'ENABLED': false, 'ENABLED': false,
'WALLETS': [], 'WALLETS': [],
}, },
'STRATUM': {
'ENABLED': false,
'API': 'http://localhost:1234',
}
}; };
class Config implements IConfig { class Config implements IConfig {
@ -362,7 +358,6 @@ class Config implements IConfig {
REDIS: IConfig['REDIS']; REDIS: IConfig['REDIS'];
FIAT_PRICE: IConfig['FIAT_PRICE']; FIAT_PRICE: IConfig['FIAT_PRICE'];
WALLETS: IConfig['WALLETS']; WALLETS: IConfig['WALLETS'];
STRATUM: IConfig['STRATUM'];
constructor() { constructor() {
const configs = this.merge(configFromFile, defaults); const configs = this.merge(configFromFile, defaults);
@ -385,7 +380,6 @@ class Config implements IConfig {
this.REDIS = configs.REDIS; this.REDIS = configs.REDIS;
this.FIAT_PRICE = configs.FIAT_PRICE; this.FIAT_PRICE = configs.FIAT_PRICE;
this.WALLETS = configs.WALLETS; this.WALLETS = configs.WALLETS;
this.STRATUM = configs.STRATUM;
} }
merge = (...objects: object[]): IConfig => { merge = (...objects: object[]): IConfig => {

View File

@ -48,7 +48,6 @@ import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes'; import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks'; import mempoolBlocks from './api/mempool-blocks';
import walletApi from './api/services/wallets'; import walletApi from './api/services/wallets';
import stratumApi from './api/services/stratum';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@ -321,16 +320,11 @@ class Server {
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler)); loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
accelerationApi.connectWebsocket(); accelerationApi.connectWebsocket();
if (config.STRATUM.ENABLED) {
stratumApi.connectWebsocket();
}
} }
setUpHttpApiRoutes(): void { setUpHttpApiRoutes(): void {
bitcoinRoutes.initRoutes(this.app); bitcoinRoutes.initRoutes(this.app);
if (config.MEMPOOL.OFFICIAL) {
bitcoinCoreRoutes.initRoutes(this.app); bitcoinCoreRoutes.initRoutes(this.app);
}
pricesRoutes.initRoutes(this.app); pricesRoutes.initRoutes(this.app);
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) { if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
statisticsRoutes.initRoutes(this.app); statisticsRoutes.initRoutes(this.app);

View File

@ -325,8 +325,6 @@ export interface BlockExtension {
// Requires coinstatsindex, will be set to NULL otherwise // Requires coinstatsindex, will be set to NULL otherwise
utxoSetSize: number | null; utxoSetSize: number | null;
totalInputAmt: number | null; totalInputAmt: number | null;
// pools-v2.json git hash
definitionHash: string | undefined;
} }
/** /**

View File

@ -15,7 +15,6 @@ import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository'; import BlocksAuditsRepository from './BlocksAuditsRepository';
import transactionUtils from '../api/transaction-utils'; import transactionUtils from '../api/transaction-utils';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script'; import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
import poolsUpdater from '../tasks/pools-updater';
interface DatabaseBlock { interface DatabaseBlock {
id: string; id: string;
@ -124,7 +123,7 @@ class BlocksRepository {
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size, coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
total_inputs, total_outputs, total_input_amt, total_output_amt, total_inputs, total_outputs, total_input_amt, total_output_amt,
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight, fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
median_fee_amt, coinbase_signature_ascii, definition_hash median_fee_amt, coinbase_signature_ascii
) VALUE ( ) VALUE (
?, ?, FROM_UNIXTIME(?), ?, ?, ?, FROM_UNIXTIME(?), ?,
?, ?, ?, ?, ?, ?, ?, ?,
@ -135,7 +134,7 @@ class BlocksRepository {
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ? ?, ?
)`; )`;
const poolDbId = await PoolsRepository.$getPoolByUniqueId(block.extras.pool.id); const poolDbId = await PoolsRepository.$getPoolByUniqueId(block.extras.pool.id);
@ -182,7 +181,6 @@ class BlocksRepository {
block.extras.segwitTotalWeight, block.extras.segwitTotalWeight,
block.extras.medianFeeAmt, block.extras.medianFeeAmt,
truncatedCoinbaseSignatureAscii, truncatedCoinbaseSignatureAscii,
poolsUpdater.currentSha
]; ];
await DB.query(query, params); await DB.query(query, params);
@ -1015,9 +1013,9 @@ class BlocksRepository {
public async $savePool(id: string, poolId: number): Promise<void> { public async $savePool(id: string, poolId: number): Promise<void> {
try { try {
await DB.query(` await DB.query(`
UPDATE blocks SET pool_id = ?, definition_hash = ? UPDATE blocks SET pool_id = ?
WHERE hash = ?`, WHERE hash = ?`,
[poolId, poolsUpdater.currentSha, id] [poolId, id]
); );
} catch (e) { } catch (e) {
logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e));

View File

@ -88,8 +88,8 @@ class PoolsUpdater {
try { try {
await DB.query('START TRANSACTION;'); await DB.query('START TRANSACTION;');
await this.updateDBSha(githubSha);
await poolsParser.migratePoolsJson(); await poolsParser.migratePoolsJson();
await this.updateDBSha(githubSha);
await DB.query('COMMIT;'); await DB.query('COMMIT;');
} catch (e) { } catch (e) {
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag); logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag);
@ -121,7 +121,7 @@ class PoolsUpdater {
/** /**
* Fetch our latest pools-v2.json sha from the db * Fetch our latest pools-v2.json sha from the db
*/ */
public async getShaFromDb(): Promise<string | null> { private async getShaFromDb(): Promise<string | null> {
try { try {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : null); return (rows.length > 0 ? rows[0].string : null);

View File

@ -148,10 +148,6 @@
"API": "__MEMPOOL_SERVICES_API__", "API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__ "ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
}, },
"STRATUM": {
"ENABLED": __STRATUM_ENABLED__,
"API": "__STRATUM_API__"
},
"REDIS": { "REDIS": {
"ENABLED": __REDIS_ENABLED__, "ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__", "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",

View File

@ -149,10 +149,6 @@ __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"} __MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false} __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# STRATUM
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
# REDIS # REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false} __REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""} __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
@ -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_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
# STRATUM
sed -i "s!__STRATUM_ENABLED__!${__STRATUM_ENABLED__}!g" mempool-config.json
sed -i "s!__STRATUM_API__!${__STRATUM_API__}!g" mempool-config.json
# REDIS # REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json

View File

@ -11,14 +11,10 @@ services:
stop_grace_period: 1m stop_grace_period: 1m
command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'" command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'"
ports: ports:
- 8080:8080 - 80:8080
api: api:
environment: environment:
MEMPOOL_BACKEND: "electrum" MEMPOOL_BACKEND: "none"
ELECTRUM_HOST: "172.27.0.1"
ELECTRUM_PORT: "50001"
ELECTRUM_TLS_ENABLED: "false"
CORE_RPC_HOST: "172.27.0.1" CORE_RPC_HOST: "172.27.0.1"
CORE_RPC_PORT: "8332" CORE_RPC_PORT: "8332"
CORE_RPC_USERNAME: "mempool" CORE_RPC_USERNAME: "mempool"

View File

@ -45,7 +45,6 @@ __SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false} __PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
# Export as environment variables to be used by envsubst # Export as environment variables to be used by envsubst
export __MAINNET_ENABLED__ export __MAINNET_ENABLED__
@ -77,7 +76,6 @@ export __SERVICES_API__
export __PUBLIC_ACCELERATIONS__ export __PUBLIC_ACCELERATIONS__
export __HISTORICAL_PRICE__ export __HISTORICAL_PRICE__
export __ADDITIONAL_CURRENCIES__ export __ADDITIONAL_CURRENCIES__
export __STRATUM_ENABLED__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname) folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
echo ${folder} echo ${folder}

View File

@ -13,8 +13,8 @@ localhostIP="127.0.0.1"
cp ./docker/frontend/* ./frontend cp ./docker/frontend/* ./frontend
cp ./nginx.conf ./frontend/ cp ./nginx.conf ./frontend/
cp ./nginx-mempool.conf ./frontend/ cp ./nginx-mempool.conf ./frontend/
# sed -i"" -e "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf sed -i"" -e "s/${localhostIP}:80/0.0.0.0:__MEMPOOL_FRONTEND_HTTP_PORT__/g" ./frontend/nginx.conf
# sed -i"" -e "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf sed -i"" -e "s/${localhostIP}/0.0.0.0/g" ./frontend/nginx.conf
sed -i"" -e "s/user nobody;//g" ./frontend/nginx.conf sed -i"" -e "s/user nobody;//g" ./frontend/nginx.conf
sed -i"" -e "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf sed -i"" -e "s!/etc/nginx/nginx-mempool.conf!/etc/nginx/conf.d/nginx-mempool.conf!g" ./frontend/nginx.conf
sed -i"" -e "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf sed -i"" -e "s/${localhostIP}:8999/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/g" ./frontend/nginx-mempool.conf

View File

@ -344,9 +344,7 @@ describe('Mainnet', () => {
cy.visit('/'); cy.visit('/');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
//TODO(knorrium): add a check for the proxied server cy.changeNetwork('testnet4');
// cy.changeNetwork('testnet4');
cy.changeNetwork('signet'); cy.changeNetwork('signet');
cy.changeNetwork('mainnet'); cy.changeNetwork('mainnet');
}); });

View File

@ -27,6 +27,5 @@
"ACCELERATOR": false, "ACCELERATOR": false,
"ACCELERATOR_BUTTON": true, "ACCELERATOR_BUTTON": true,
"PUBLIC_ACCELERATIONS": false, "PUBLIC_ACCELERATIONS": false,
"STRATUM_ENABLED": false,
"SERVICES_API": "https://mempool.space/api/v1/services" "SERVICES_API": "https://mempool.space/api/v1/services"
} }

View File

@ -23,9 +23,9 @@
"@angular/router": "^17.3.1", "@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1", "@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1", "@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.7.2", "@fortawesome/fontawesome-common-types": "~6.6.0",
"@fortawesome/fontawesome-svg-core": "~6.7.2", "@fortawesome/fontawesome-svg-core": "~6.6.0",
"@fortawesome/free-solid-svg-icons": "~6.7.2", "@fortawesome/free-solid-svg-icons": "~6.6.0",
"@mempool/mempool.js": "2.3.0", "@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0", "@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0", "@types/qrcode": "~1.5.0",
@ -33,8 +33,9 @@
"browserify": "^17.0.0", "browserify": "^17.0.0",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"domino": "^2.1.6", "domino": "^2.1.6",
"echarts": "~5.6.0", "echarts": "~5.5.0",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~17.2.0", "ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0", "ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1", "qrcode": "1.5.1",
@ -61,7 +62,7 @@
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"cypress": "^13.17.0", "cypress": "^13.15.0",
"cypress-fail-on-console-error": "~5.1.0", "cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1", "cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1", "mock-socket": "~9.3.1",
@ -3112,10 +3113,9 @@
} }
}, },
"node_modules/@cypress/request": { "node_modules/@cypress/request": {
"version": "3.0.7", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz",
"integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==", "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==",
"license": "Apache-2.0",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"aws-sign2": "~0.7.0", "aws-sign2": "~0.7.0",
@ -3131,9 +3131,9 @@
"json-stringify-safe": "~5.0.1", "json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19", "mime-types": "~2.1.19",
"performance-now": "^2.1.0", "performance-now": "^2.1.0",
"qs": "6.13.1", "qs": "6.13.0",
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"tough-cookie": "^5.0.0", "tough-cookie": "^4.1.3",
"tunnel-agent": "^0.6.0", "tunnel-agent": "^0.6.0",
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
@ -3141,22 +3141,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/@cypress/request/node_modules/qs": {
"version": "6.13.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz",
"integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==",
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/@cypress/schematic": { "node_modules/@cypress/schematic": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-2.5.0.tgz",
@ -3690,33 +3674,30 @@
} }
}, },
"node_modules/@fortawesome/fontawesome-common-types": { "node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.7.2", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
"license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@fortawesome/fontawesome-svg-core": { "node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.7.2", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2" "@fortawesome/fontawesome-common-types": "6.6.0"
}, },
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@fortawesome/free-solid-svg-icons": { "node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.7.2", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2" "@fortawesome/fontawesome-common-types": "6.6.0"
}, },
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@ -5692,7 +5673,6 @@
"version": "0.2.6", "version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"safer-buffer": "~2.1.0" "safer-buffer": "~2.1.0"
@ -5727,7 +5707,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"license": "MIT",
"optional": true, "optional": true,
"engines": { "engines": {
"node": ">=0.8" "node": ">=0.8"
@ -5848,7 +5827,6 @@
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
"license": "Apache-2.0",
"optional": true, "optional": true,
"engines": { "engines": {
"node": "*" "node": "*"
@ -5858,7 +5836,6 @@
"version": "1.13.2", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
"integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
"license": "MIT",
"optional": true "optional": true
}, },
"node_modules/axios": { "node_modules/axios": {
@ -6016,7 +5993,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"license": "BSD-3-Clause",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"tweetnacl": "^0.14.3" "tweetnacl": "^0.14.3"
@ -7092,7 +7068,6 @@
"version": "0.12.0", "version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
"license": "Apache-2.0",
"optional": true "optional": true
}, },
"node_modules/chai": { "node_modules/chai": {
@ -7195,16 +7170,15 @@
} }
}, },
"node_modules/ci-info": { "node_modules/ci-info": {
"version": "4.1.0", "version": "3.8.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
"integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/sibiraj-s" "url": "https://github.com/sponsors/sibiraj-s"
} }
], ],
"license": "MIT",
"optional": true, "optional": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -7979,14 +7953,13 @@
"peer": true "peer": true
}, },
"node_modules/cypress": { "node_modules/cypress": {
"version": "13.17.0", "version": "13.15.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz",
"integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@cypress/request": "^3.0.6", "@cypress/request": "^3.0.4",
"@cypress/xvfb": "^1.2.4", "@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1", "@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2", "@types/sizzle": "^2.3.2",
@ -7997,7 +7970,6 @@
"cachedir": "^2.3.0", "cachedir": "^2.3.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"check-more-types": "^2.24.0", "check-more-types": "^2.24.0",
"ci-info": "^4.0.0",
"cli-cursor": "^3.1.0", "cli-cursor": "^3.1.0",
"cli-table3": "~0.6.1", "cli-table3": "~0.6.1",
"commander": "^6.2.1", "commander": "^6.2.1",
@ -8012,6 +7984,7 @@
"figures": "^3.2.0", "figures": "^3.2.0",
"fs-extra": "^9.1.0", "fs-extra": "^9.1.0",
"getos": "^3.2.1", "getos": "^3.2.1",
"is-ci": "^3.0.1",
"is-installed-globally": "~0.4.0", "is-installed-globally": "~0.4.0",
"lazy-ass": "^1.6.0", "lazy-ass": "^1.6.0",
"listr2": "^3.8.3", "listr2": "^3.8.3",
@ -8026,7 +7999,6 @@
"semver": "^7.5.3", "semver": "^7.5.3",
"supports-color": "^8.1.1", "supports-color": "^8.1.1",
"tmp": "~0.2.3", "tmp": "~0.2.3",
"tree-kill": "1.2.2",
"untildify": "^4.0.0", "untildify": "^4.0.0",
"yauzl": "^2.10.0" "yauzl": "^2.10.0"
}, },
@ -8229,7 +8201,6 @@
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
"license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"assert-plus": "^1.0.0" "assert-plus": "^1.0.0"
@ -8716,7 +8687,6 @@
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
"license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"jsbn": "~0.1.0", "jsbn": "~0.1.0",
@ -8724,12 +8694,12 @@
} }
}, },
"node_modules/echarts": { "node_modules/echarts": {
"version": "5.6.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
"dependencies": { "dependencies": {
"tslib": "2.3.0", "tslib": "2.3.0",
"zrender": "5.6.1" "zrender": "5.5.0"
} }
}, },
"node_modules/echarts/node_modules/tslib": { "node_modules/echarts/node_modules/tslib": {
@ -9935,7 +9905,6 @@
"engines": [ "engines": [
"node >=0.6.0" "node >=0.6.0"
], ],
"license": "MIT",
"optional": true "optional": true
}, },
"node_modules/falafel": { "node_modules/falafel": {
@ -9952,6 +9921,11 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/fancy-canvas": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-0.2.2.tgz",
"integrity": "sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA=="
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -10219,7 +10193,6 @@
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
"license": "Apache-2.0",
"optional": true, "optional": true,
"engines": { "engines": {
"node": "*" "node": "*"
@ -10427,7 +10400,6 @@
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
"license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"assert-plus": "^1.0.0" "assert-plus": "^1.0.0"
@ -10882,7 +10854,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz",
"integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==",
"license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"assert-plus": "^1.0.0", "assert-plus": "^1.0.0",
@ -11249,6 +11220,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-ci": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
"integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
"optional": true,
"dependencies": {
"ci-info": "^3.2.0"
},
"bin": {
"is-ci": "bin.js"
}
},
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.13.1", "version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
@ -11498,7 +11481,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
"license": "MIT",
"optional": true "optional": true
}, },
"node_modules/is-unicode-supported": { "node_modules/is-unicode-supported": {
@ -11563,7 +11545,6 @@
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
"license": "MIT",
"optional": true "optional": true
}, },
"node_modules/istanbul-lib-coverage": { "node_modules/istanbul-lib-coverage": {
@ -11697,7 +11678,6 @@
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"license": "MIT",
"optional": true "optional": true
}, },
"node_modules/jsesc": { "node_modules/jsesc": {
@ -11726,7 +11706,6 @@
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"license": "(AFL-2.1 OR BSD-3-Clause)",
"optional": true "optional": true
}, },
"node_modules/json-schema-traverse": { "node_modules/json-schema-traverse": {
@ -11744,7 +11723,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC",
"optional": true "optional": true
}, },
"node_modules/json5": { "node_modules/json5": {
@ -11805,7 +11783,6 @@
"engines": [ "engines": [
"node >=0.6.0" "node >=0.6.0"
], ],
"license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"assert-plus": "1.0.0", "assert-plus": "1.0.0",
@ -12129,6 +12106,14 @@
} }
} }
}, },
"node_modules/lightweight-charts": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-3.8.0.tgz",
"integrity": "sha512-7yFGnYuE1RjRJG9RwUTBz5wvF1QtjBOSW4FFlikr8Dh+/TDNt4ci+HsWSYmStgQUpawpvkCJ3j5/W25GppGj9Q==",
"dependencies": {
"fancy-canvas": "0.2.2"
}
},
"node_modules/limiter": { "node_modules/limiter": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
@ -14125,7 +14110,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true "optional": true
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
@ -14556,6 +14540,12 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
"optional": true
},
"node_modules/public-encrypt": { "node_modules/public-encrypt": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
@ -14671,6 +14661,12 @@
"node": ">=0.4.x" "node": ">=0.4.x"
} }
}, },
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"optional": true
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -16032,7 +16028,6 @@
"version": "1.18.0", "version": "1.18.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
"license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"asn1": "~0.2.3", "asn1": "~0.2.3",
@ -16582,26 +16577,6 @@
"readable-stream": "3" "readable-stream": "3"
} }
}, },
"node_modules/tldts": {
"version": "6.1.70",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz",
"integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tldts-core": "^6.1.70"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.70",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz",
"integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==",
"license": "MIT",
"optional": true
},
"node_modules/tlite": { "node_modules/tlite": {
"version": "0.1.9", "version": "0.1.9",
"resolved": "https://registry.npmjs.org/tlite/-/tlite-0.1.9.tgz", "resolved": "https://registry.npmjs.org/tlite/-/tlite-0.1.9.tgz",
@ -16646,16 +16621,27 @@
} }
}, },
"node_modules/tough-cookie": { "node_modules/tough-cookie": {
"version": "5.0.0", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"license": "BSD-3-Clause",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"tldts": "^6.1.32" "psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=6"
}
},
"node_modules/tough-cookie/node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"optional": true,
"engines": {
"node": ">= 4.0.0"
} }
}, },
"node_modules/transform-ast": { "node_modules/transform-ast": {
@ -16824,7 +16810,6 @@
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
@ -16837,7 +16822,6 @@
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
"license": "Unlicense",
"optional": true "optional": true
}, },
"node_modules/type": { "node_modules/type": {
@ -17146,6 +17130,16 @@
"querystring": "0.2.0" "querystring": "0.2.0"
} }
}, },
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"optional": true,
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/url/node_modules/punycode": { "node_modules/url/node_modules/punycode": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
@ -17213,7 +17207,6 @@
"engines": [ "engines": [
"node >=0.6.0" "node >=0.6.0"
], ],
"license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"assert-plus": "^1.0.0", "assert-plus": "^1.0.0",
@ -18366,9 +18359,9 @@
} }
}, },
"node_modules/zrender": { "node_modules/zrender": {
"version": "5.6.1", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
"dependencies": { "dependencies": {
"tslib": "2.3.0" "tslib": "2.3.0"
} }
@ -20355,9 +20348,9 @@
} }
}, },
"@cypress/request": { "@cypress/request": {
"version": "3.0.7", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz",
"integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==", "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==",
"optional": true, "optional": true,
"requires": { "requires": {
"aws-sign2": "~0.7.0", "aws-sign2": "~0.7.0",
@ -20373,22 +20366,11 @@
"json-stringify-safe": "~5.0.1", "json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19", "mime-types": "~2.1.19",
"performance-now": "^2.1.0", "performance-now": "^2.1.0",
"qs": "6.13.1", "qs": "6.13.0",
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"tough-cookie": "^5.0.0", "tough-cookie": "^4.1.3",
"tunnel-agent": "^0.6.0", "tunnel-agent": "^0.6.0",
"uuid": "^8.3.2" "uuid": "^8.3.2"
},
"dependencies": {
"qs": {
"version": "6.13.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz",
"integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==",
"optional": true,
"requires": {
"side-channel": "^1.0.6"
}
}
} }
}, },
"@cypress/schematic": { "@cypress/schematic": {
@ -20667,24 +20649,24 @@
} }
}, },
"@fortawesome/fontawesome-common-types": { "@fortawesome/fontawesome-common-types": {
"version": "6.7.2", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==" "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw=="
}, },
"@fortawesome/fontawesome-svg-core": { "@fortawesome/fontawesome-svg-core": {
"version": "6.7.2", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
"requires": { "requires": {
"@fortawesome/fontawesome-common-types": "6.7.2" "@fortawesome/fontawesome-common-types": "6.6.0"
} }
}, },
"@fortawesome/free-solid-svg-icons": { "@fortawesome/free-solid-svg-icons": {
"version": "6.7.2", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
"requires": { "requires": {
"@fortawesome/fontawesome-common-types": "6.7.2" "@fortawesome/fontawesome-common-types": "6.6.0"
} }
}, },
"@goto-bus-stop/common-shake": { "@goto-bus-stop/common-shake": {
@ -23316,9 +23298,9 @@
"integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="
}, },
"ci-info": { "ci-info": {
"version": "4.1.0", "version": "3.8.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
"integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
"optional": true "optional": true
}, },
"cipher-base": { "cipher-base": {
@ -23914,12 +23896,12 @@
"peer": true "peer": true
}, },
"cypress": { "cypress": {
"version": "13.17.0", "version": "13.15.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz",
"integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==",
"optional": true, "optional": true,
"requires": { "requires": {
"@cypress/request": "^3.0.6", "@cypress/request": "^3.0.4",
"@cypress/xvfb": "^1.2.4", "@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1", "@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2", "@types/sizzle": "^2.3.2",
@ -23930,7 +23912,6 @@
"cachedir": "^2.3.0", "cachedir": "^2.3.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"check-more-types": "^2.24.0", "check-more-types": "^2.24.0",
"ci-info": "^4.0.0",
"cli-cursor": "^3.1.0", "cli-cursor": "^3.1.0",
"cli-table3": "~0.6.1", "cli-table3": "~0.6.1",
"commander": "^6.2.1", "commander": "^6.2.1",
@ -23945,6 +23926,7 @@
"figures": "^3.2.0", "figures": "^3.2.0",
"fs-extra": "^9.1.0", "fs-extra": "^9.1.0",
"getos": "^3.2.1", "getos": "^3.2.1",
"is-ci": "^3.0.1",
"is-installed-globally": "~0.4.0", "is-installed-globally": "~0.4.0",
"lazy-ass": "^1.6.0", "lazy-ass": "^1.6.0",
"listr2": "^3.8.3", "listr2": "^3.8.3",
@ -23959,7 +23941,6 @@
"semver": "^7.5.3", "semver": "^7.5.3",
"supports-color": "^8.1.1", "supports-color": "^8.1.1",
"tmp": "~0.2.3", "tmp": "~0.2.3",
"tree-kill": "1.2.2",
"untildify": "^4.0.0", "untildify": "^4.0.0",
"yauzl": "^2.10.0" "yauzl": "^2.10.0"
}, },
@ -24485,12 +24466,12 @@
} }
}, },
"echarts": { "echarts": {
"version": "5.6.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
"requires": { "requires": {
"tslib": "2.3.0", "tslib": "2.3.0",
"zrender": "5.6.1" "zrender": "5.5.0"
}, },
"dependencies": { "dependencies": {
"tslib": { "tslib": {
@ -25452,6 +25433,11 @@
"object-keys": "^1.0.6" "object-keys": "^1.0.6"
} }
}, },
"fancy-canvas": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-0.2.2.tgz",
"integrity": "sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA=="
},
"fast-deep-equal": { "fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -26387,6 +26373,15 @@
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
"integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ=="
}, },
"is-ci": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
"integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
"optional": true,
"requires": {
"ci-info": "^3.2.0"
}
},
"is-core-module": { "is-core-module": {
"version": "2.13.1", "version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
@ -27020,6 +27015,14 @@
"webpack-sources": "^3.0.0" "webpack-sources": "^3.0.0"
} }
}, },
"lightweight-charts": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-3.8.0.tgz",
"integrity": "sha512-7yFGnYuE1RjRJG9RwUTBz5wvF1QtjBOSW4FFlikr8Dh+/TDNt4ci+HsWSYmStgQUpawpvkCJ3j5/W25GppGj9Q==",
"requires": {
"fancy-canvas": "0.2.2"
}
},
"limiter": { "limiter": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
@ -28803,6 +28806,12 @@
"event-stream": "=3.3.4" "event-stream": "=3.3.4"
} }
}, },
"psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
"optional": true
},
"public-encrypt": { "public-encrypt": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
@ -28894,6 +28903,12 @@
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
}, },
"querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"optional": true
},
"queue-microtask": { "queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -30358,21 +30373,6 @@
} }
} }
}, },
"tldts": {
"version": "6.1.70",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz",
"integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==",
"optional": true,
"requires": {
"tldts-core": "^6.1.70"
}
},
"tldts-core": {
"version": "6.1.70",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz",
"integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==",
"optional": true
},
"tlite": { "tlite": {
"version": "0.1.9", "version": "0.1.9",
"resolved": "https://registry.npmjs.org/tlite/-/tlite-0.1.9.tgz", "resolved": "https://registry.npmjs.org/tlite/-/tlite-0.1.9.tgz",
@ -30405,12 +30405,23 @@
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
}, },
"tough-cookie": { "tough-cookie": {
"version": "5.0.0", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"optional": true, "optional": true,
"requires": { "requires": {
"tldts": "^6.1.32" "psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"dependencies": {
"universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"optional": true
}
} }
}, },
"transform-ast": { "transform-ast": {
@ -30746,6 +30757,16 @@
} }
} }
}, },
"url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"optional": true,
"requires": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -31485,9 +31506,9 @@
} }
}, },
"zrender": { "zrender": {
"version": "5.6.1", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
"requires": { "requires": {
"tslib": "2.3.0" "tslib": "2.3.0"
}, },

View File

@ -76,9 +76,9 @@
"@angular/router": "^17.3.1", "@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1", "@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1", "@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.7.2", "@fortawesome/fontawesome-common-types": "~6.6.0",
"@fortawesome/fontawesome-svg-core": "~6.7.2", "@fortawesome/fontawesome-svg-core": "~6.6.0",
"@fortawesome/free-solid-svg-icons": "~6.7.2", "@fortawesome/free-solid-svg-icons": "~6.6.0",
"@mempool/mempool.js": "2.3.0", "@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0", "@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0", "@types/qrcode": "~1.5.0",
@ -86,7 +86,8 @@
"browserify": "^17.0.0", "browserify": "^17.0.0",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"domino": "^2.1.6", "domino": "^2.1.6",
"echarts": "~5.6.0", "echarts": "~5.5.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~17.2.0", "ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0", "ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1", "qrcode": "1.5.1",
@ -114,7 +115,7 @@
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.5.0", "@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"cypress": "^13.17.0", "cypress": "^13.15.0",
"cypress-fail-on-console-error": "~5.1.0", "cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1", "cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1", "mock-socket": "~9.3.1",

View File

@ -3,10 +3,8 @@ const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf'); let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => { PROXY_CONFIG.forEach(entry => {
const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space'; entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
console.log(`e2e tests running against ${hostname}`); entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
entry.target = entry.target.replace("mempool.space", hostname);
entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space");
}); });
module.exports = PROXY_CONFIG; module.exports = PROXY_CONFIG;

View File

@ -440,38 +440,3 @@ export const fiatCurrencies = {
indexed: true, indexed: true,
}, },
}; };
export interface Timezone {
offset: string;
name: string;
}
export const timezones: Timezone[] = [
{ offset: '-12', name: 'Anywhere on Earth (AoE)' },
{ offset: '-11', name: 'Samoa Standard Time (SST)' },
{ offset: '-10', name: 'Hawaii Standard Time (HST)' },
{ offset: '-9', name: 'Alaska Standard Time (AKST)' },
{ offset: '-8', name: 'Pacific Standard Time (PST)' },
{ offset: '-7', name: 'Mountain Standard Time (MST)' },
{ offset: '-6', name: 'Central Standard Time (CST)' },
{ offset: '-5', name: 'Eastern Standard Time (EST)' },
{ offset: '-4', name: 'Atlantic Standard Time (AST)' },
{ offset: '-3', name: 'Argentina Time (ART)' },
{ offset: '-2', name: 'Fernando de Noronha Time (FNT)' },
{ offset: '-1', name: 'Azores Time (AZOT)' },
{ offset: '+0', name: 'Greenwich Mean Time (GMT)' },
{ offset: '+1', name: 'Central European Time (CET)' },
{ offset: '+2', name: 'Eastern European Time (EET)' },
{ offset: '+3', name: 'Moscow Standard Time (MSK)' },
{ offset: '+4', name: 'Armenia Time (AMT)' },
{ offset: '+5', name: 'Pakistan Standard Time (PKT)' },
{ offset: '+6', name: 'Xinjiang Time (XJT)' },
{ offset: '+7', name: 'Indochina Time (ICT)' },
{ offset: '+8', name: 'Hong Kong Time (HKT)' },
{ offset: '+9', name: 'Japan Standard Time (JST)' },
{ offset: '+10', name: 'Australian Eastern Standard Time (AEST)' },
{ offset: '+11', name: 'Norfolk Time (NFT)' },
{ offset: '+12', name: 'New Zealand Standard Time (NZST)' },
{ offset: '+13', name: 'Tonga Time (TOT)' },
{ offset: '+14', name: 'Line Islands Time (LINT)' }
];

View File

@ -217,7 +217,7 @@
<ng-container> <ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales"> <ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> <a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/> <img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a> </a>
</ng-template> </ng-template>
</ng-container> </ng-container>
@ -229,7 +229,7 @@
<div class="wrapper"> <div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads"> <ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> <a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/> <img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a> </a>
</ng-template> </ng-template>
</div> </div>

View File

@ -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) {
@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>
} @else {
<div class="row mb-1 text-center"> <div class="row mb-1 text-center">
<div class="col-sm"> <div class="col-sm">
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1> <h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
</div> </div>
</div> </div>
}
<div class="row text-center mt-1"> <div class="row text-center mt-1">
<div class="col-sm"> <div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center"> <div class="d-flex flex-row justify-content-center align-items-center">
@ -365,11 +357,11 @@
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box> <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box>
</div> </div>
</div> </div>
<div class="payment-area" style="font-size: 14px;"> <div class="payment-area mt-2 p-2" style="font-size: 14px;">
<div class="row text-center justify-content-center mx-2"> <div class="row text-center justify-content-center mx-2">
<span 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></span> <p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p>
</div> </div>
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) { @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
<div class="row"> <div class="row">
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container>&nbsp;<small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></p> <p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container>&nbsp;<small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></p>
@ -386,12 +378,9 @@
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span></p> <p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span></p>
<app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice> <app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice>
} @else if (btcpayInvoiceFailed) { } @else if (btcpayInvoiceFailed) {
<div class="btcpay-invoice"> <p i18n="accelerator.failed-to-load-invoice">Failed to load invoice</p>
<fa-icon style="font-size: 20px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon> <div class="d-flex flex-column align-items-center justify-content-center" style="width: 100%; height: 292px;">
<span i18n="accelerator.failed-to-load-invoice">Failed to load invoice</span> <fa-icon style="font-size: 24px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
@if (!loadingBtcpayInvoice) {
<button class="btn btn-sm btn-secondary mt-0 mt-md-1" (click)="requestBTCPayInvoice()">Retry ↻</button>
}
</div> </div>
} @else { } @else {
<p i18n="accelerator.loading-invoice">Loading invoice...</p> <p i18n="accelerator.loading-invoice">Loading invoice...</p>
@ -400,13 +389,13 @@
</div> </div>
} }
</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"> <div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
<p class="text-nowrap">&mdash;&mdash;<span i18n="or"> OR </span>&mdash;&mdash;</p> <p class="text-nowrap">&mdash;<span i18n="or">OR</span>&mdash;</p>
</div> </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"> <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> <p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<app-fiat [value]="cost"></app-fiat> with</p>
@if (canPayWithCashapp) { @if (canPayWithCashapp) {
@ -424,17 +413,6 @@
<img src="/resources/google-pay.png" height=37> <img src="/resources/google-pay.png" height=37>
</div> </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')">
@if (['VISA', 'MASTERCARD', 'JCB', 'DISCOVER', 'DISCOVER_DINERS', 'AMERICAN_EXPRESS'].includes(estimate?.availablePaymentMethods?.cardOnFile?.card?.brand)) {
<app-svg-images [name]="estimate?.availablePaymentMethods?.cardOnFile?.card?.brand" height="33" class="mr-2"></app-svg-images>
} @else {
<app-svg-images name="OTHER_BRAND" height="33" class="mr-2"></app-svg-images>
}
<span style="font-size: 22px; padding-bottom: 3px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
</div>
}
</div> </div>
} }
</div> </div>
@ -457,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> <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>
</div> </div>
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') { } @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
<!-- Show checkout page --> <!-- Show checkout page -->
<div class="row mb-md-1 text-center" id="confirm-title"> <div class="row mb-md-1 text-center" id="confirm-title">
<div class="col-sm" id="confirm-payment-title"> <div class="col-sm" id="confirm-payment-title">
@ -473,7 +451,7 @@
</div> </div>
</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="row text-center mt-1">
<div class="col-sm"> <div class="col-sm">
<div class="form-group w-100"> <div class="form-group w-100">
@ -498,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> <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') { } @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> <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"> <div display="d-flex flex-row justify-content-center">
<span i18n="accelerator.loading-payment-method">Loading payment method...</span> <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 class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div> </div>
} }
</div> </div>
@if (isTokenizing > 0) {
<div class="d-flex flex-row justify-content-center">
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
}
</div> </div>
</div> </div>

View File

@ -8,13 +8,6 @@
color: var(--green) color: var(--green)
} }
.accelerate-checkout-inner {
&.input-disabled {
pointer-events: none;
opacity: 0.75;
}
}
.paymentMethod { .paymentMethod {
padding: 10px; padding: 10px;
background-color: var(--secondary); background-color: var(--secondary);
@ -153,11 +146,6 @@
.payment-area { .payment-area {
background: var(--bg); background: var(--bg);
margin-top: 0.5rem;
padding: 0.5rem;
@media (max-width: 575px) {
padding-bottom: 1.25rem;
}
} }
.col.pie { .col.pie {
@ -225,16 +213,3 @@
.apple-pay-button-white-with-line { .apple-pay-button-white-with-line {
-apple-pay-button-style: white-outline; -apple-pay-button-style: white-outline;
} }
.btcpay-invoice {
display: flex;
height: 292px;
flex-direction: column;
justify-content: center;
align-items: center;
@media (max-width: 575px) {
height: 75px;
flex-direction: row;
gap: 5px;
}
}

View File

@ -13,7 +13,7 @@ import { EnterpriseService } from '@app/services/enterprise.service';
import { ApiService } from '@app/services/api.service'; import { ApiService } from '@app/services/api.service';
import { isDevMode } from '@angular/core'; 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 = { export type AccelerationEstimate = {
hasAccess: boolean; hasAccess: boolean;
@ -26,7 +26,7 @@ export type AccelerationEstimate = {
mempoolBaseFee: number; mempoolBaseFee: number;
vsizeFee: number; vsizeFee: number;
pools: 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; unavailable?: boolean;
options: { // recommended bid options options: { // recommended bid options
fee: number; // recommended userBid in sats fee: number; // recommended userBid in sats
@ -49,7 +49,7 @@ export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2; export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4; 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({ @Component({
selector: 'app-accelerate-checkout', selector: 'app-accelerate-checkout',
@ -62,9 +62,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() miningStats: MiningStats; @Input() miningStats: MiningStats;
@Input() eta: ETA; @Input() eta: ETA;
@Input() scrollEvent: boolean; @Input() scrollEvent: boolean;
@Input() cashappEnabled: boolean = true;
@Input() applePayEnabled: boolean = false; @Input() applePayEnabled: boolean = false;
@Input() googlePayEnabled: boolean = true; @Input() googlePayEnabled: boolean = true;
@Input() cardOnFileEnabled: boolean = true;
@Input() advancedEnabled: boolean = false; @Input() advancedEnabled: boolean = false;
@Input() forceMobile: boolean = false; @Input() forceMobile: boolean = false;
@Input() showDetails: boolean = false; @Input() showDetails: boolean = false;
@ -76,8 +76,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
calculating = true; calculating = true;
processing = false; processing = false;
isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
isTokenizing = 0; // reference counter, 0 = false, >0 = true
selectedOption: 'wait' | 'accel'; selectedOption: 'wait' | 'accel';
cantPayReason = ''; cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data quoteError = ''; // error fetching estimate or initial data
@ -117,7 +115,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
loadingCashapp = false; loadingCashapp = false;
loadingApplePay = false; loadingApplePay = false;
loadingGooglePay = false; loadingGooglePay = false;
loadingCardOnFile = false;
payments: any; payments: any;
cashAppPay: any; cashAppPay: any;
applePay: any; applePay: any;
@ -157,7 +154,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerateError = null; this.accelerateError = null;
this.timePaid = 0; this.timePaid = 0;
this.btcpayInvoiceFailed = false; this.btcpayInvoiceFailed = false;
this.moveToStep('summary', true); this.moveToStep('summary');
} else { } else {
this.auth = auth; this.auth = auth;
} }
@ -166,11 +163,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) { // Redirected from cashapp if (urlParams.get('cash_request_id')) { // Redirected from cashapp
this.moveToStep('processing', true); this.moveToStep('processing');
this.insertSquare(); this.insertSquare();
this.setupSquare(); this.setupSquare();
} else { } else {
this.moveToStep('summary', true); this.moveToStep('summary');
} }
this.conversionsSubscription = this.stateService.conversions$.subscribe( this.conversionsSubscription = this.stateService.conversions$.subscribe(
@ -195,23 +192,20 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
if (changes.accelerating && this.accelerating) { if (changes.accelerating && this.accelerating) {
if (this.step === 'processing' || this.step === 'paid') { if (this.step === 'processing' || this.step === 'paid') {
this.moveToStep('success', true); this.moveToStep('success');
} else { // Edge case where the transaction gets accelerated by someone else or on another session } else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal(); this.closeModal();
} }
} }
} }
moveToStep(step: CheckoutStep, force: boolean = false): void { moveToStep(step: CheckoutStep): void {
if (this.isCheckoutLocked > 0 && !force) {
return;
}
this.processing = false; this.processing = false;
this._step = step; this._step = step;
if (this.timeoutTimer) { if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer); clearTimeout(this.timeoutTimer);
} }
if (!this.estimate && ['quote', 'summary', 'checkout', 'processing'].includes(this.step)) { if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) {
this.fetchEstimate(); this.fetchEstimate();
} }
if (this._step === 'checkout') { if (this._step === 'checkout') {
@ -220,9 +214,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
if (this._step === 'checkout' && this.canPayWithBitcoin) { if (this._step === 'checkout' && this.canPayWithBitcoin) {
this.btcpayInvoiceFailed = false; this.btcpayInvoiceFailed = false;
this.loadingBtcpayInvoice = true;
this.invoice = null; this.invoice = null;
this.requestBTCPayInvoice(); this.requestBTCPayInvoice();
} else if (this._step === 'cashapp') { } else if (this._step === 'cashapp' && this.cashappEnabled) {
this.loadingCashapp = true; this.loadingCashapp = true;
this.setupSquare(); this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100); this.scrollToElementWithTimeout('confirm-title', 'center', 100);
@ -234,10 +229,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = true; this.loadingGooglePay = true;
this.setupSquare(); this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100); 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') { } else if (this._step === 'paid') {
this.timePaid = Date.now(); this.timePaid = Date.now();
this.timeoutTimer = setTimeout(() => { this.timeoutTimer = setTimeout(() => {
@ -251,7 +242,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
closeModal(): void { closeModal(): void {
this.completed.emit(true); this.completed.emit(true);
this.moveToStep('summary', true); this.moveToStep('summary');
} }
/** /**
@ -332,6 +323,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) { if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) {
this.loadingBtcpayInvoice = true;
this.requestBTCPayInvoice(); this.requestBTCPayInvoice();
} }
@ -401,7 +393,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true; this.showSuccess = true;
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
this.moveToStep('paid', true); this.moveToStep('paid');
}, },
error: (response) => { error: (response) => {
this.processing = false; this.processing = false;
@ -457,8 +449,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
await this.requestApplePayPayment(); await this.requestApplePayPayment();
} else if (this._step === 'googlepay') { } else if (this._step === 'googlepay') {
await this.requestGooglePayPayment(); await this.requestGooglePayPayment();
} else if (this._step === 'cardonfile') {
this.loadingCardOnFile = false;
} }
}, },
error: () => { error: () => {
@ -513,14 +503,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
this.loadingApplePay = false; this.loadingApplePay = false;
applePayButton.addEventListener('click', async event => { applePayButton.addEventListener('click', async event => {
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
return;
}
event.preventDefault(); event.preventDefault();
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(); const tokenResult = await this.applePay.tokenize();
if (tokenResult?.status === 'OK') { if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card; const card = tokenResult.details?.card;
@ -531,9 +514,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return; return;
} }
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); 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.servicesApiService.accelerateWithApplePay$(
this.tx.txid, this.tx.txid,
tokenResult.token, tokenResult.token,
@ -549,9 +529,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.applePay.destroy(); this.applePay.destroy();
} }
setTimeout(() => { setTimeout(() => {
this.isTokenizing--; this.moveToStep('paid');
this.isCheckoutLocked--;
this.moveToStep('paid', true);
}, 1000); }, 1000);
}, },
error: (response) => { error: (response) => {
@ -559,12 +537,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerateError = response.error; this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) { if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => { setTimeout(() => {
this.isTokenizing--;
this.isCheckoutLocked--;
// Reset everything by reloading the page :D, can be improved // Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 10000); }, 3000);
} }
} }
}); });
@ -578,11 +554,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
throw new Error(errorMessage); throw new Error(errorMessage);
} }
} finally {
// always unlock the checkout once we're finished
this.isTokenizing--;
this.isCheckoutLocked--;
}
}); });
} catch (e) { } catch (e) {
this.processing = false; this.processing = false;
@ -631,14 +602,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = false; this.loadingGooglePay = false;
document.getElementById('google-pay-button').addEventListener('click', async event => { document.getElementById('google-pay-button').addEventListener('click', async event => {
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
return;
}
event.preventDefault(); event.preventDefault();
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(); const tokenResult = await this.googlePay.tokenize();
if (tokenResult?.status === 'OK') { if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card; const card = tokenResult.details?.card;
@ -648,25 +612,13 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.processing = false; this.processing = false;
return; 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()); 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.servicesApiService.accelerateWithGooglePay$(
this.tx.txid, this.tx.txid,
tokenResult.token, tokenResult.token,
verificationToken.token,
cardTag, cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD, costUSD
verificationToken.userChallenged
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false; this.processing = false;
@ -676,22 +628,18 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.googlePay.destroy(); this.googlePay.destroy();
} }
setTimeout(() => { setTimeout(() => {
this.isTokenizing--; this.moveToStep('paid');
this.isCheckoutLocked--;
this.moveToStep('paid', true);
}, 1000); }, 1000);
}, },
error: (response) => { error: (response) => {
this.processing = false; this.processing = false;
this.accelerateError = response.error; this.accelerateError = response.error;
this.isTokenizing--;
this.isCheckoutLocked--;
if (!(response.status === 403 && response.error === 'not_available')) { if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => { setTimeout(() => {
// Reset everything by reloading the page :D, can be improved // Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 10000); }, 3000);
} }
} }
}); });
@ -705,119 +653,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
throw new Error(errorMessage); throw new Error(errorMessage);
} }
} finally {
// always unlock the checkout once we're finished
this.isTokenizing--;
this.isCheckoutLocked--;
}
}); });
} }
); );
} }
/**
* 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 * CASHAPP
*/ */
@ -838,7 +678,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`; const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`;
const costUSD = this.cost / 100_000_000 * conversions.USD; const costUSD = this.step === 'processing' ? 69.69 : (this.cost / 100_000_000 * conversions.USD); // When we're redirected to this component, the payment data is already linked to the payment token, so does not matter what amonut we put in there, therefore it's 69.69
const paymentRequest = this.payments.paymentRequest({ const paymentRequest = this.payments.paymentRequest({
countryCode: 'US', countryCode: 'US',
currencyCode: 'USD', currencyCode: 'USD',
@ -878,7 +718,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.cashAppPay.destroy(); this.cashAppPay.destroy();
} }
setTimeout(() => { setTimeout(() => {
this.moveToStep('paid', true); this.moveToStep('paid');
if (window.history.replaceState) { if (window.history.replaceState) {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, '')); window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
@ -893,7 +733,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
// Reset everything by reloading the page :D, can be improved // Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``)); window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 10000); }, 3000);
} }
} }
}); });
@ -903,49 +743,20 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
); );
} }
/**
* https://developer.squareup.com/docs/sca-overview
*/
async $verifyBuyer(payments, token, details, amount): Promise<{token: string, userChallenged: boolean}> {
const verificationDetails = {
amount: amount,
currencyCode: 'USD',
intent: 'CHARGE',
billingContact: {
givenName: details.card?.billing?.givenName,
familyName: details.card?.billing?.familyName,
phone: details.card?.billing?.phone,
addressLines: details.card?.billing?.addressLines,
city: details.card?.billing?.city,
state: details.card?.billing?.state,
countryCode: details.card?.billing?.countryCode,
},
};
const verificationResults = await payments.verifyBuyer(
token,
verificationDetails,
);
return verificationResults;
}
/** /**
* BTCPay * BTCPay
*/ */
async requestBTCPayInvoice(): Promise<void> { async requestBTCPayInvoice(): Promise<void> {
this.loadingBtcpayInvoice = true;
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe( this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
switchMap(response => { switchMap(response => {
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId); return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
}), }),
catchError(error => { catchError(error => {
console.log(error); console.log(error);
this.loadingBtcpayInvoice = false;
this.btcpayInvoiceFailed = true; this.btcpayInvoiceFailed = true;
return of(null); return of(null);
}) })
).subscribe((invoice) => { ).subscribe((invoice) => {
this.loadingBtcpayInvoice = false;
this.invoice = invoice; this.invoice = invoice;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
@ -955,7 +766,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon'); this.audioService.playSound('ascend-chime-cartoon');
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
this.moveToStep('paid', true); this.moveToStep('paid');
} }
isLoggedIn(): boolean { isLoggedIn(): boolean {
@ -982,6 +793,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
get couldPayWithCashapp(): boolean { get couldPayWithCashapp(): boolean {
if (!this.cashappEnabled) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.cashapp; return !!this.estimate?.availablePaymentMethods?.cashapp;
} }
@ -1016,7 +830,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
get canPayWithCashapp(): boolean { get canPayWithCashapp(): boolean {
if (!this.conversions || (!this.isProdDomain && !isDevMode())) { if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false; return false;
} }
@ -1063,22 +877,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return false; 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 { get canPayWithBalance(): boolean {
if (!this.hasAccessToBalanceMode) { if (!this.hasAccessToBalanceMode) {
return false; return false;

View File

@ -46,8 +46,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
aggregatedHistory$: Observable<any>; aggregatedHistory$: Observable<any>;
statsSubscription: Subscription; statsSubscription: Subscription;
aggregatedHistorySubscription: Subscription;
fragmentSubscription: Subscription;
isLoading = true; isLoading = true;
formatNumber = formatNumber; formatNumber = formatNumber;
timespan = ''; timespan = '';
@ -82,7 +80,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(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) { if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
} }
@ -115,7 +113,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
share(), share(),
); );
this.aggregatedHistorySubscription = this.aggregatedHistory$.subscribe(); this.aggregatedHistory$.subscribe();
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
@ -337,8 +335,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.aggregatedHistorySubscription?.unsubscribe(); if (this.statsSubscription) {
this.fragmentSubscription?.unsubscribe(); this.statsSubscription.unsubscribe();
this.statsSubscription?.unsubscribe(); }
} }
} }

View File

@ -14,7 +14,7 @@
<th class="time text-right" i18n="accelerator.requested">Requested</th> <th class="time text-right" i18n="accelerator.requested">Requested</th>
</ng-container> </ng-container>
<ng-container *ngIf="!pending"> <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="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="pool text-right" i18n="mining.pool-name" *ngIf="!this.widget">Pool</th>
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</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 === '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="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('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')" 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>
</td> </td>
<td class="date text-right" *ngIf="!this.widget"> <td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time> <app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>

View File

@ -45,17 +45,14 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() left: number | string = 70; @Input() left: number | string = 70;
@Input() widget: boolean = false; @Input() widget: boolean = false;
@Input() defaultFiat: boolean = false; @Input() defaultFiat: boolean = false;
@Input() showLegend: boolean = true;
@Input() showYAxis: boolean = true;
adjustedLeft: number;
adjustedRight: number;
data: any[] = []; data: any[] = [];
fiatData: any[] = []; fiatData: any[] = [];
hoverData: any[] = []; hoverData: any[] = [];
conversions: any; conversions: any;
allowZoom: boolean = false; allowZoom: boolean = false;
initialRight = this.right;
initialLeft = this.left;
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false }; selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
subscription: Subscription; subscription: Subscription;
@ -123,7 +120,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} else if (this.conversions && this.conversions['USD']) { } else if (this.conversions && this.conversions['USD']) {
price = this.conversions['USD']; price = this.conversions['USD'];
} }
return { ...item, price: price }; return { ...item, price: price }
}); });
} }
}), }),
@ -184,8 +181,8 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0); const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue); const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue);
this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right; this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40; this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
this.chartOptions = { this.chartOptions = {
color: [ color: [
@ -202,10 +199,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
grid: { grid: {
top: 20, top: 20,
bottom: this.allowZoom ? 65 : 20, bottom: this.allowZoom ? 65 : 20,
right: this.adjustedRight, right: this.right,
left: this.adjustedLeft, left: this.left,
}, },
legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? { legend: !this.stateService.isAnyTestnet() ? {
data: [ data: [
{ {
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`, name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
@ -316,7 +313,6 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
type: 'value', type: 'value',
position: 'left', position: 'left',
axisLabel: { axisLabel: {
show: this.showYAxis,
color: 'rgb(110, 112, 121)', color: 'rgb(110, 112, 121)',
formatter: (val): string => { formatter: (val): string => {
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue); let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
@ -347,10 +343,9 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
{ {
type: 'value', type: 'value',
axisLabel: { axisLabel: {
show: this.showYAxis,
color: 'rgb(110, 112, 121)', color: 'rgb(110, 112, 121)',
formatter: function(val) { formatter: function(val) {
return `$${this.amountShortenerPipe.transform(val, 3, undefined, true, true)}`; return `$${this.amountShortenerPipe.transform(val, 0, undefined, true)}`;
}.bind(this) }.bind(this)
}, },
splitLine: { splitLine: {
@ -404,8 +399,8 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
type: 'slider', type: 'slider',
brushSelect: false, brushSelect: false,
realtime: true, realtime: true,
left: this.adjustedLeft, left: this.left,
right: this.adjustedRight, right: this.right,
selectedDataBackground: { selectedDataBackground: {
lineStyle: { lineStyle: {
color: '#fff', color: '#fff',
@ -435,23 +430,23 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
onLegendSelectChanged(e) { onLegendSelectChanged(e) {
this.selected = e.selected; this.selected = e.selected;
this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right; this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40; this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
this.chartOptions = { this.chartOptions = {
grid: { grid: {
right: this.adjustedRight, right: this.right,
left: this.adjustedLeft, left: this.left,
}, },
legend: { legend: {
selected: this.selected, selected: this.selected,
}, },
dataZoom: this.allowZoom ? [{ dataZoom: this.allowZoom ? [{
left: this.adjustedLeft, left: this.left,
right: this.adjustedRight, right: this.right,
}, { }, {
left: this.adjustedLeft, left: this.left,
right: this.adjustedRight, right: this.right,
}] : undefined }] : undefined
}; };
@ -478,30 +473,25 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} }
extendSummary(summary) { 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 // Add a point at today's date to make the graph end at the current time
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 }); extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
extendedSummary.reverse();
let maxTime = Date.now() / 1000; let oneHour = 60 * 60;
const oneHour = 60 * 60;
// Fill gaps longer than interval // Fill gaps longer than interval
for (let i = 0; i < extendedSummary.length - 1; i++) { for (let i = 0; i < extendedSummary.length - 1; i++) {
if (extendedSummary[i].time > maxTime) { let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);
extendedSummary[i].time = maxTime - 30;
}
maxTime = extendedSummary[i].time;
const hours = Math.floor((extendedSummary[i].time - extendedSummary[i + 1].time) / oneHour);
if (hours > 1) { if (hours > 1) {
for (let j = 1; j < hours; j++) { for (let j = 1; j < hours; j++) {
const newTime = extendedSummary[i].time - oneHour * j; let newTime = extendedSummary[i].time + oneHour * j;
extendedSummary.splice(i + j, 0, { time: newTime, value: 0 }); extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
} }
i += hours - 1; i += hours - 1;
} }
} }
return extendedSummary; return extendedSummary.reverse();
} }
} }

View File

@ -41,7 +41,7 @@ export class AppComponent implements OnInit {
@HostListener('document:keydown', ['$event']) @HostListener('document:keydown', ['$event'])
handleKeyboardEvents(event: KeyboardEvent) { handleKeyboardEvents(event: KeyboardEvent) {
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) { if (event.target instanceof HTMLInputElement) {
return; return;
} }
// prevent arrow key horizontal scrolling // prevent arrow key horizontal scrolling

View File

@ -10,10 +10,6 @@
</span> </span>
} }
<div class="d-flex justify-content-center">
<app-mempool-error *ngIf="paymentErrorMessage" [error]="paymentErrorMessage"></app-mempool-error>
</div>
<div *ngIf="paymentStatus === 2"> <div *ngIf="paymentStatus === 2">
<form [formGroup]="paymentForm"> <form [formGroup]="paymentForm">

View File

@ -1,8 +1,9 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { Subscription, of, catchError } from 'rxjs'; import { ActivatedRoute } from '@angular/router';
import { retry, tap } from 'rxjs/operators'; import { Subscription, of, timer } from 'rxjs';
import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators';
import { ServicesApiServices } from '@app/services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
@Component({ @Component({
@ -17,17 +18,30 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy {
@Output() completed = new EventEmitter(); @Output() completed = new EventEmitter();
paymentForm: FormGroup; paymentForm: FormGroup;
requestSubscription: Subscription | undefined;
paymentStatusSubscription: Subscription | undefined; paymentStatusSubscription: Subscription | undefined;
paymentStatus = 1; // 1 - Waiting for invoice | 2 - Pending payment | 3 - Payment completed paymentStatus = 1; // 1 - Waiting for invoice | 2 - Pending payment | 3 - Payment completed
paymentErrorMessage: string = ''; paramMapSubscription: Subscription | undefined;
invoiceSubscription: Subscription | undefined;
invoiceTimeout; // Wait for angular to load all the things before making a request
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private apiService: ServicesApiServices, private apiService: ServicesApiServices,
private sanitizer: DomSanitizer private sanitizer: DomSanitizer,
private activatedRoute: ActivatedRoute
) { } ) { }
ngOnDestroy() { ngOnDestroy() {
if (this.requestSubscription) {
this.requestSubscription.unsubscribe();
}
if (this.paramMapSubscription) {
this.paramMapSubscription.unsubscribe();
}
if (this.invoiceSubscription) {
this.invoiceSubscription.unsubscribe();
}
if (this.paymentStatusSubscription) { if (this.paymentStatusSubscription) {
this.paymentStatusSubscription.unsubscribe(); this.paymentStatusSubscription.unsubscribe();
} }
@ -58,39 +72,15 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy {
} else { } else {
this.paymentStatus = 4; this.paymentStatus = 4;
} }
this.monitorPendingInvoice();
}
monitorPendingInvoice(): void {
if (!this.invoice) {
return;
}
if (this.paymentStatusSubscription) {
this.paymentStatusSubscription.unsubscribe();
}
this.paymentStatusSubscription = this.apiService.getPaymentStatus$(this.invoice.btcpayInvoiceId).pipe( this.paymentStatusSubscription = this.apiService.getPaymentStatus$(this.invoice.btcpayInvoiceId).pipe(
tap(result => { retry({ delay: () => timer(2000)}),
if (result.status === 204) { // Manually trigger an error in that case so we can retry repeat({delay: 2000}),
throw result; filter((response) => response.status !== 204 && response.status !== 404),
} else if (result.status === 200) { // Invoice settled take(1),
).subscribe(() => {
this.paymentStatus = 3; this.paymentStatus = 3;
this.completed.emit(); this.completed.emit();
} });
}),
catchError(err => {
if (err.status === 204 || err.status === 504) {
throw err; // Will trigger the retry
} else if (err.status === 400) {
this.paymentErrorMessage = 'Invoice has expired';
} else if (err.status === 404) {
this.paymentErrorMessage = 'Invoice is no longer valid';
}
this.paymentStatus = -1;
return of(null);
}),
retry({ delay: 1000 }),
).subscribe();
} }
get availableMethods(): string[] { get availableMethods(): string[] {

View File

@ -172,19 +172,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.animationFrameRequest) { if (this.animationFrameRequest) {
cancelAnimationFrame(this.animationFrameRequest); cancelAnimationFrame(this.animationFrameRequest);
}
clearTimeout(this.animationHeartBeat); clearTimeout(this.animationHeartBeat);
}
if (this.canvas) { if (this.canvas) {
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost); this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored); this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
}
if (this.scene) {
this.scene.destroy();
}
this.vertexArray.destroy();
this.vertexArray = null;
this.themeChangedSubscription?.unsubscribe(); this.themeChangedSubscription?.unsubscribe();
this.searchSubscription?.unsubscribe(); }
} }
clear(direction): void { clear(direction): void {
@ -453,7 +447,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
this.applyQueuedUpdates(); this.applyQueuedUpdates();
// skip re-render if there's no change to the scene // 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 */ /* SET UP SHADER UNIFORMS */
// screen dimensions // screen dimensions
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight); 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)) { if (this.running && this.scene && now <= (this.scene.animateUntil + 500)) {
this.doRun(); this.doRun();
} else { } else {
if (this.animationHeartBeat) {
clearTimeout(this.animationHeartBeat); clearTimeout(this.animationHeartBeat);
}
this.animationHeartBeat = window.setTimeout(() => { this.animationHeartBeat = window.setTimeout(() => {
this.start(); this.start();
}, 1000); }, 1000);

View File

@ -19,7 +19,6 @@ export class FastVertexArray {
freeSlots: number[]; freeSlots: number[];
lastSlot: number; lastSlot: number;
dirty = false; dirty = false;
destroyed = false;
constructor(length, stride) { constructor(length, stride) {
this.length = length; this.length = length;
@ -33,9 +32,6 @@ export class FastVertexArray {
} }
insert(sprite: TxSprite): number { insert(sprite: TxSprite): number {
if (this.destroyed) {
return;
}
this.count++; this.count++;
let position; let position;
@ -49,14 +45,11 @@ export class FastVertexArray {
} }
} }
this.sprites[position] = sprite; this.sprites[position] = sprite;
this.dirty = true;
return position; return position;
this.dirty = true;
} }
remove(index: number): void { remove(index: number): void {
if (this.destroyed) {
return;
}
this.count--; this.count--;
this.clearData(index); this.clearData(index);
this.freeSlots.push(index); this.freeSlots.push(index);
@ -68,26 +61,20 @@ export class FastVertexArray {
} }
setData(index: number, dataChunk: number[]): void { setData(index: number, dataChunk: number[]): void {
if (this.destroyed) {
return;
}
this.data.set(dataChunk, (index * this.stride)); this.data.set(dataChunk, (index * this.stride));
this.dirty = true; this.dirty = true;
} }
private clearData(index: number): void { clearData(index: number): void {
this.data.fill(0, (index * this.stride), ((index + 1) * this.stride)); this.data.fill(0, (index * this.stride), ((index + 1) * this.stride));
this.dirty = true; this.dirty = true;
} }
getData(index: number): Float32Array { getData(index: number): Float32Array {
if (this.destroyed) {
return;
}
return this.data.subarray(index, this.stride); return this.data.subarray(index, this.stride);
} }
private expand(): void { expand(): void {
this.length *= 2; this.length *= 2;
const newData = new Float32Array(this.length * this.stride); const newData = new Float32Array(this.length * this.stride);
newData.set(this.data); newData.set(this.data);
@ -95,7 +82,7 @@ export class FastVertexArray {
this.dirty = true; 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) // 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)))); const newLength = Math.max(512, Math.pow(2, Math.ceil(Math.log2(this.count))));
if (newLength !== this.length) { if (newLength !== this.length) {
@ -123,13 +110,4 @@ export class FastVertexArray {
getVertexData(): Float32Array { getVertexData(): Float32Array {
return this.data; return this.data;
} }
destroy(): void {
this.data = null;
this.sprites = null;
this.freeSlots = null;
this.lastSlot = 0;
this.dirty = false;
this.destroyed = true;
}
} }

View File

@ -116,7 +116,7 @@ export class BlockViewComponent implements OnInit, OnDestroy {
this.isLoadingBlock = false; this.isLoadingBlock = false;
this.isLoadingOverview = true; this.isLoadingOverview = true;
}), }),
shareReplay({ bufferSize: 1, refCount: true }) shareReplay(1)
); );
this.overviewSubscription = block$.pipe( this.overviewSubscription = block$.pipe(
@ -176,8 +176,5 @@ export class BlockViewComponent implements OnInit, OnDestroy {
if (this.queryParamsSubscription) { if (this.queryParamsSubscription) {
this.queryParamsSubscription.unsubscribe(); this.queryParamsSubscription.unsubscribe();
} }
if (this.blockGraph) {
this.blockGraph.destroy();
}
} }
} }

View File

@ -117,7 +117,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
this.openGraphService.waitOver('block-data-' + this.rawId); this.openGraphService.waitOver('block-data-' + this.rawId);
}), }),
throttleTime(50, asyncScheduler, { leading: true, trailing: true }), throttleTime(50, asyncScheduler, { leading: true, trailing: true }),
shareReplay({ bufferSize: 1, refCount: true }) shareReplay(1)
); );
this.overviewSubscription = block$.pipe( this.overviewSubscription = block$.pipe(

View File

@ -1,8 +1,8 @@
import { Component, OnInit, OnDestroy, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core';
import { Location } from '@angular/common'; 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 { 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 { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs';
import { StateService } from '@app/services/state.service'; import { StateService } from '@app/services/state.service';
import { SeoService } from '@app/services/seo.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; paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
numUnexpected: number = 0; numUnexpected: number = 0;
mode: 'projected' | 'actual' = 'projected'; mode: 'projected' | 'actual' = 'projected';
currentQueryParams: Params;
overviewSubscription: Subscription; overviewSubscription: Subscription;
accelerationsSubscription: Subscription; accelerationsSubscription: Subscription;
@ -81,8 +80,8 @@ export class BlockComponent implements OnInit, OnDestroy {
timeLtr: boolean; timeLtr: boolean;
childChangeSubscription: Subscription; childChangeSubscription: Subscription;
auditPrefSubscription: Subscription; auditPrefSubscription: Subscription;
isAuditEnabledSubscription: Subscription;
oobSubscription: Subscription; oobSubscription: Subscription;
priceSubscription: Subscription; priceSubscription: Subscription;
blockConversion: Price; blockConversion: Price;
@ -119,7 +118,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.setAuditAvailable(this.auditSupported); this.setAuditAvailable(this.auditSupported);
if (this.auditSupported) { if (this.auditSupported) {
this.isAuditEnabledSubscription = this.isAuditEnabledFromParam().subscribe(auditParam => { this.isAuditEnabledFromParam().subscribe(auditParam => {
if (this.auditParamEnabled) { if (this.auditParamEnabled) {
this.auditModeEnabled = auditParam; this.auditModeEnabled = auditParam;
} else { } else {
@ -282,7 +281,7 @@ export class BlockComponent implements OnInit, OnDestroy {
} }
}), }),
throttleTime(300, asyncScheduler, { leading: true, trailing: true }), throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
shareReplay({ bufferSize: 1, refCount: true }) shareReplay(1)
); );
this.overviewSubscription = this.block$.pipe( this.overviewSubscription = this.block$.pipe(
@ -364,7 +363,6 @@ export class BlockComponent implements OnInit, OnDestroy {
.subscribe((network) => this.network = network); .subscribe((network) => this.network = network);
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.currentQueryParams = params;
if (params.showDetails === 'true') { if (params.showDetails === 'true') {
this.showDetails = true; this.showDetails = true;
} else { } else {
@ -416,7 +414,6 @@ export class BlockComponent implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.stateService.markBlock$.next({}); this.stateService.markBlock$.next({});
this.overviewSubscription?.unsubscribe(); this.overviewSubscription?.unsubscribe();
this.accelerationsSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe(); this.keyNavigationSubscription?.unsubscribe();
this.blocksSubscription?.unsubscribe(); this.blocksSubscription?.unsubscribe();
this.cacheBlocksSubscription?.unsubscribe(); this.cacheBlocksSubscription?.unsubscribe();
@ -424,16 +421,8 @@ export class BlockComponent implements OnInit, OnDestroy {
this.queryParamsSubscription?.unsubscribe(); this.queryParamsSubscription?.unsubscribe();
this.timeLtrSubscription?.unsubscribe(); this.timeLtrSubscription?.unsubscribe();
this.childChangeSubscription?.unsubscribe(); this.childChangeSubscription?.unsubscribe();
this.auditPrefSubscription?.unsubscribe();
this.isAuditEnabledSubscription?.unsubscribe();
this.oobSubscription?.unsubscribe();
this.priceSubscription?.unsubscribe(); this.priceSubscription?.unsubscribe();
this.blockGraphProjected.forEach(graph => { this.oobSubscription?.unsubscribe();
graph.destroy();
});
this.blockGraphActual.forEach(graph => {
graph.destroy();
});
} }
// TODO - Refactor this.fees/this.reward for liquid because it is not // TODO - Refactor this.fees/this.reward for liquid because it is not
@ -744,7 +733,8 @@ export class BlockComponent implements OnInit, OnDestroy {
toggleAuditMode(): void { toggleAuditMode(): void {
this.stateService.hideAudit.next(this.auditModeEnabled); this.stateService.hideAudit.next(this.auditModeEnabled);
const queryParams = { ...this.currentQueryParams }; this.route.queryParams.subscribe(params => {
const queryParams = { ...params };
delete queryParams['audit']; delete queryParams['audit'];
let newUrl = this.router.url.split('?')[0]; let newUrl = this.router.url.split('?')[0];
@ -752,10 +742,10 @@ export class BlockComponent implements OnInit, OnDestroy {
if (queryString) { if (queryString) {
newUrl += '?' + queryString; newUrl += '?' + queryString;
} }
this.location.replaceState(newUrl);
// avoid duplicate subscriptions this.location.replaceState(newUrl);
this.auditPrefSubscription?.unsubscribe(); });
this.auditPrefSubscription = this.stateService.hideAudit.subscribe((hide) => { this.auditPrefSubscription = this.stateService.hideAudit.subscribe((hide) => {
this.auditModeEnabled = !hide; this.auditModeEnabled = !hide;
this.showAudit = this.auditAvailable && this.auditModeEnabled; this.showAudit = this.auditAvailable && this.auditModeEnabled;

View File

@ -49,7 +49,7 @@
</div> </div>
</td> </td>
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"> <td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [hideTimeSince]="true"></app-timestamp> &lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
</td> </td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"> <td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<a <a

View File

@ -281,11 +281,9 @@
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8"> <div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card"> <div class="card">
<div class="card-body"> <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> <h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
<span>&nbsp;</span> </span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<app-address-transactions-widget [addressSummary$]="walletSummary$"></app-address-transactions-widget> <app-address-transactions-widget [addressSummary$]="walletSummary$"></app-address-transactions-widget>
</div> </div>
</div> </div>

View File

@ -162,9 +162,6 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
this.cacheBlocksSubscription?.unsubscribe(); this.cacheBlocksSubscription?.unsubscribe();
this.networkChangedSubscription?.unsubscribe(); this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe(); this.queryParamsSubscription?.unsubscribe();
this.blockGraphs.forEach(graph => {
graph.destroy();
});
} }
shiftTestBlocks(): void { shiftTestBlocks(): void {

View File

@ -19,10 +19,12 @@
} @else if (!user) { } @else if (!user) {
<!-- User not logged in --> <!-- User not logged in -->
<div class="alert alert-mempool d-block text-center w-100"> <div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle pr-2"> <div class="d-inline align-middle">
<span>To use the faucet, please</span> <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> </div>
<app-github-login customClass="btn btn-sm" width="150px" redirectTo="/testnet4/faucet" buttonString="Sign up with"></app-github-login> <app-twitter-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Twitter"></app-twitter-login>
</div> </div>
} }
@else if (user && user.status === 'pending' && !user.email && user.snsId) { @else if (user && user.status === 'pending' && !user.email && user.snsId) {
@ -34,18 +36,18 @@
</div> </div>
} }
@else if (error === 'not_available') { @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="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle"> <div class="d-inline align-middle">
<span class="mb-2 mr-2">To use the faucet, please</span> <span class="mb-2 mr-2">To use the faucet, please</span>
</div> </div>
<app-github-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your"></app-github-login> <app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
</div> </div>
} }
@else if (error === 'account_limited') { @else if (error === 'account_limited') {
<div class="alert alert-mempool d-block text-center w-100"> <div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle"> <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>
</div> </div>
} }

View File

@ -24,7 +24,7 @@ export class FaucetComponent implements OnInit, OnDestroy {
min: number; // minimum amount to request at once (in sats) min: number; // minimum amount to request at once (in sats)
max: number; // maximum amount to request at once max: number; // maximum amount to request at once
address?: string; // faucet address address?: string; // faucet address
code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon' | 'faucet_not_available_no_utxo'; code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon';
} | null = null; } | null = null;
faucetForm: FormGroup; faucetForm: FormGroup;

View File

@ -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 : ''">
<span class="ml-2 text-light align-middle">{{ buttonString }}</span>
<svg height="32" viewBox="0 0 18 16" width="32" style="fill: white; padding-left: 5px">
<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>
</a>

View File

@ -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;
}
}

View File

@ -56,7 +56,8 @@
</ng-template> </ng-template>
</td> </td>
<td class="timestamp text-left"> <td class="timestamp text-left">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="utxo.blocktime"></app-timestamp> &lrm;{{ utxo.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="utxo.blocktime"></app-time>)</i></div>
</td> </td>
<td class="expires-in text-left" [ngStyle]="{ 'color': getGradientColor(utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate) }"> <td class="expires-in text-left" [ngStyle]="{ 'color': getGradientColor(utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate) }">
{{ utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate < 0 ? -(utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate) : utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate }} <span i18n="shared.blocks" class="symbol">blocks</span> {{ utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate < 0 ? -(utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate) : utxo.blocknumber + utxo.timelock - lastReservesBlockUpdate }} <span i18n="shared.blocks" class="symbol">blocks</span>

View File

@ -53,7 +53,8 @@
</ng-container> </ng-container>
</td> </td>
<td class="timestamp text-left"> <td class="timestamp text-left">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="peg.blocktime"></app-timestamp> &lrm;{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="peg.blocktime"></app-time>)</i></div>
</td> </td>
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}"> <td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}">
<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount> <app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>

View File

@ -4,8 +4,9 @@
<nav class="navbar navbar-expand-md navbar-dark"> <nav class="navbar navbar-expand-md navbar-dark">
<!-- Hamburger --> <!-- Hamburger -->
<ng-container *ngIf="servicesEnabled"> <ng-container *ngIf="servicesEnabled">
<div *ngIf="user" class="profile_image_container" (click)="hamburgerClick($event)"> <div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)">
<img [src]="'/api/v1/services/account/images/' + user.username" class="profile_image" onError="this.src = '/resources/anon.svg'; this.className = 'anon'" /> <img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/images/' + user.username + '/md5=' + user.imageMd5" class="profile_image">
<app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images>
</div> </div>
<div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)"> <div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)">
<app-svg-images name="hamburger" height="40"></app-svg-images> <app-svg-images name="hamburger" height="40"></app-svg-images>
@ -22,7 +23,7 @@
} @else { } @else {
<ng-template [ngIf]="subdomain && enterpriseInfo"> <ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container"> <div class="subdomain_container">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</div> </div>
<div class="vertical-line"></div> <div class="vertical-line"></div>
</ng-template> </ng-template>
@ -42,7 +43,7 @@
} @else { } @else {
<ng-template [ngIf]="subdomain && enterpriseInfo"> <ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container"> <div class="subdomain_container">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</div> </div>
<div class="vertical-line"></div> <div class="vertical-line"></div>
</ng-template> </ng-template>

View File

@ -269,7 +269,7 @@ nav {
text-align: center; text-align: center;
align-self: center; align-self: center;
cursor: pointer; cursor: pointer;
.anon { &.anon {
border: 1.5px solid lightgrey; border: 1.5px solid lightgrey;
color: lightgrey; color: lightgrey;
border-radius: 5px; border-radius: 5px;

View File

@ -120,7 +120,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.blockGraph?.destroy();
this.blockSub.unsubscribe(); this.blockSub.unsubscribe();
this.timeLtrSubscription.unsubscribe(); this.timeLtrSubscription.unsubscribe();
this.websocketService.stopTrackMempoolBlock(); this.websocketService.stopTrackMempoolBlock();

View File

@ -10,7 +10,7 @@
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1> <h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
</div> </div>
<div class="box pool-details"> <div class="box">
<div class="row"> <div class="row">
<div class="col-lg-6"> <div class="col-lg-6">
@ -173,125 +173,7 @@
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</div> </div>
<!-- Stratum Job -->
<ng-container *ngIf="(job$ | async) as job;">
<h2 i18n="pool.next_block">Next block</h2>
<div class="box mb-3">
<div class="row" >
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>
<table class="job-table table table-xs table-borderless table-fixed table-data">
<thead>
<tr>
<th class="data-title clip text-center height" i18n="latest-blocks.height">Height</th>
<th class="data-title clip text-center expected" i18n="next-block.expected-time">Expected</th>
<th class="data-title clip text-center reward" i18n="latest-blocks.reward">Reward</th>
<th class="data-title clip text-center timestamp" i18n="next-block.timestamp">Timestamp</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center height">
{{ job.height }}
</td>
<td class="text-center expected">
<ng-container *ngIf="(expectedBlockTime$ | async) as expectedBlockTime; else expectedPlaceholder">
<app-time kind="until" [time]="expectedBlockTime" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
</ng-container>
<ng-template #expectedPlaceholder>~</ng-template>
</td>
<td class="text-center reward">
<app-amount [satoshis]="job.reward"></app-amount>
</td>
<td class="text-center timestamp">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.timestamp" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td>
<table class="job-table table table-xs table-borderless table-fixed table-data">
<thead>
<tr>
<th class="data-title clip text-center coinbase" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
<th class="data-title clip text-center clean" i18n="next-block.clean">Clean</th>
<th class="data-title clip text-center prevhash" i18n="next-block.prevhash">Prevhash</th>
<th class="data-title clip text-center job-received" i18n="next-block.job-received">Job Received</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center coinbase">
{{ job.scriptsig | hex2ascii }}
</td>
<td class="text-center clean">
@if (job.cleanJobs) {
<fa-icon [icon]="['fas', 'check-circle']" [fixedWidth]="true"></fa-icon>
} @else {
<fa-icon [icon]="['fas', 'times-circle']" [fixedWidth]="true"></fa-icon>
}
</td>
<td class="text-center prevhash">
<a [routerLink]="['/block' | relativeUrl, job.prevHash]">
<app-truncate [text]="job.prevHash" [lastChars]="8"></app-truncate>
</a>
</td>
<td class="text-center job-received">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.received / 1000" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td>
<table class="stratum-table">
<thead>
<tr>
<th class="data-title clip text-center" [attr.colspan]="Math.max(job.merkleBranches.length, 12)">
<a class="title-link" href="" [routerLink]="['/stratum' | relativeUrl]">
Merkle Branches
<span>&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) {
<td class="merkle" [style.background-color]="branch ? '#' + branch.slice(0, 6) : ''">
@if ($index === 0 && branch) {
<a [routerLink]="['/tx' | relativeUrl, reverseHash(branch)]" class="cell-link">
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 14px; color: white"></fa-icon>
</a>
}
</td>
}
@for (_ of [].constructor(Math.max(0, 12 - job.merkleBranches.length)); track $index) {
<td class="merkle empty-branch"></td>
}
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</ng-container>
<!-- Blocks list --> <!-- Blocks list -->
<h2 i18n="master-page.blocks">Blocks</h2>
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" <table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5"
[infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()"> [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
<ng-container *ngIf="blocks$ | async as blocks; else skeleton"> <ng-container *ngIf="blocks$ | async as blocks; else skeleton">
@ -312,7 +194,7 @@
<a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a> <a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a>
</td> </td>
<td class="timestamp"> <td class="timestamp">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [hideTimeSince]="true"></app-timestamp> &lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
</td> </td>
<td class="mined"> <td class="mined">
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [showTooltip]="true"></app-time> <app-time kind="since" [time]="block.timestamp" [fastRender]="true" [showTooltip]="true"></app-time>

View File

@ -49,10 +49,12 @@ div.scrollable {
max-height: 75px; max-height: 75px;
} }
.pool-details { .box {
padding-bottom: 5px;
@media (min-width: 767.98px) { @media (min-width: 767.98px) {
min-height: 187px; min-height: 187px;
} }
}
.label { .label {
width: 25%; width: 25%;
@ -153,7 +155,6 @@ div.scrollable {
width: auto; width: auto;
text-align: left; text-align: left;
} }
}
.skeleton-loader { .skeleton-loader {
max-width: 200px; max-width: 200px;
@ -214,68 +215,3 @@ div.scrollable {
.taller-row { .taller-row {
height: 75px; 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;
.cell-link {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
color: inherit;
text-decoration: none;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.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;
}

View File

@ -10,9 +10,6 @@ import { selectPowerOfTen } from '@app/bitcoin.utils';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { SeoService } from '@app/services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { StratumJob } from '../../interfaces/websocket.interface';
import { WebsocketService } from '../../services/websocket.service';
import { MiningService } from '../../services/mining.service';
interface AccelerationTotal { interface AccelerationTotal {
cost: number, cost: number,
@ -30,16 +27,12 @@ export class PoolComponent implements OnInit {
@Input() left: number | string = 75; @Input() left: number | string = 75;
gfg = true; gfg = true;
stratumEnabled = this.stateService.env.STRATUM_ENABLED;
formatNumber = formatNumber; formatNumber = formatNumber;
Math = Math;
slugSubscription: Subscription; slugSubscription: Subscription;
poolStats$: Observable<PoolStat>; poolStats$: Observable<PoolStat>;
blocks$: Observable<BlockExtended[]>; blocks$: Observable<BlockExtended[]>;
oobFees$: Observable<AccelerationTotal[]>; oobFees$: Observable<AccelerationTotal[]>;
job$: Observable<StratumJob | null>;
expectedBlockTime$: Observable<number>;
isLoading = true; isLoading = true;
error: HttpErrorResponse | null = null; error: HttpErrorResponse | null = null;
@ -60,8 +53,6 @@ export class PoolComponent implements OnInit {
private apiService: ApiService, private apiService: ApiService,
private route: ActivatedRoute, private route: ActivatedRoute,
public stateService: StateService, public stateService: StateService,
private websocketService: WebsocketService,
private miningService: MiningService,
private seoService: SeoService, private seoService: SeoService,
) { ) {
this.auditAvailable = this.stateService.env.AUDIT; this.auditAvailable = this.stateService.env.AUDIT;
@ -138,31 +129,6 @@ export class PoolComponent implements OnInit {
}), }),
filter(oob => oob.length === 3 && oob[2].count > 0) filter(oob => oob.length === 3 && oob[2].count > 0)
); );
if (this.stratumEnabled) {
this.job$ = combineLatest([
this.poolStats$.pipe(
tap((poolStats) => {
this.websocketService.startTrackStratum(poolStats.pool.unique_id);
})
),
this.stateService.stratumJobs$
]).pipe(
map(([poolStats, jobs]) => {
return jobs[poolStats.pool.unique_id];
})
);
this.expectedBlockTime$ = combineLatest([
this.miningService.getMiningStats('1w'),
this.poolStats$,
this.stateService.difficultyAdjustment$
]).pipe(
map(([miningStats, poolStat, da]) => {
return (da.timeAvg / ((poolStat.estimatedHashrate || 0) / (miningStats.lastEstimatedHashrate * 1_000_000_000_000_000_000))) + Date.now() + da.timeOffset;
})
);
}
} }
prepareChartOptions(hashrate, share) { prepareChartOptions(hashrate, share) {
@ -361,10 +327,6 @@ export class PoolComponent implements OnInit {
return block.height; return block.height;
} }
reverseHash(hash: string) {
return hash.match(/../g).reverse().join('');
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.slugSubscription.unsubscribe(); this.slugSubscription.unsubscribe();
} }

View File

@ -1,34 +0,0 @@
.accept-results {
td, th {
&.allowed {
width: 10%;
text-align: center;
}
&.txid {
width: 50%;
}
&.rate {
width: 20%;
text-align: right;
white-space: wrap;
}
&.reason {
width: 20%;
text-align: right;
white-space: wrap;
}
}
@media (max-width: 950px) {
table-layout: auto;
td, th {
&.allowed {
width: 100px;
}
&.txid {
max-width: 200px;
}
}
}
}

View File

@ -19,9 +19,6 @@
<th class="rtt only-small">RTT</th> <th class="rtt only-small">RTT</th>
<th class="rtt only-large">RTT</th> <th class="rtt only-large">RTT</th>
<th class="height">Height</th> <th class="height">Height</th>
<th class="frontend only-large">Front</th>
<th class="backend only-large">Back</th>
<th class="electrs only-large">Electrs</th>
</tr> </tr>
<tr *ngFor="let host of hosts; let i = index; trackBy: trackByFn"> <tr *ngFor="let host of hosts; let i = index; trackBy: trackByFn">
<td class="rank">{{ i + 1 }}</td> <td class="rank">{{ i + 1 }}</td>
@ -31,15 +28,6 @@
<td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td> <td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td> <td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '')) }}</td> <td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '')) }}</td>
<ng-container *ngFor="let type of ['frontend', 'backend', 'electrs']">
<td class="{{type}} only-large" [style.background-color]="host.hashes?.[type] ? '#' + host.hashes[type].slice(0, 6) : ''">
@if (host.hashes?.[type]) {
<a [style.color]="'white'" href="https://github.com/mempool/{{type === 'electrs' ? 'electrs' : 'mempool'}}/commit/{{ host.hashes[type] }}" target="_blank">{{ host.hashes[type].slice(0, 8) || '?' }}</a>
} @else {
<span>?</span>
}
</td>
</ng-container>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -9,7 +9,7 @@
} }
.status-panel { .status-panel {
max-width: 1000px; max-width: 720px;
margin: auto; margin: auto;
padding: 1em; padding: 1em;
background: var(--box-bg); background: var(--box-bg);

View File

@ -82,10 +82,6 @@ export class ServerHealthComponent implements OnInit {
return '🇺🇸'; return '🇺🇸';
} else if (host.includes('.va1.')) { } else if (host.includes('.va1.')) {
return '🇺🇸'; return '🇺🇸';
} else if (host.includes('.sg1.')) {
return '🇸🇬';
} else if (host.includes('.hnl.')) {
return '🤙';
} else { } else {
return ''; return '';
} }

View File

@ -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>

View File

@ -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;
}

View File

@ -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 '│';
}
}
}

View File

@ -1,47 +1,4 @@
<ng-container [ngSwitch]="name"> <ng-container [ngSwitch]="name">
<ng-container *ngSwitchCase="'VISA'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
<path d="M470.1 231.3s7.6 37.2 9.3 45H446c3.3-8.9 16-43.5 16-43.5-.2 .3 3.3-9.1 5.3-14.9l2.8 13.4zM576 80v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h480c26.5 0 48 21.5 48 48zM152.5 331.2L215.7 176h-42.5l-39.3 106-4.3-21.5-14-71.4c-2.3-9.9-9.4-12.7-18.2-13.1H32.7l-.7 3.1c15.8 4 29.9 9.8 42.2 17.1l35.8 135h42.5zm94.4 .2L272.1 176h-40.2l-25.1 155.4h40.1zm139.9-50.8c.2-17.7-10.6-31.2-33.7-42.3-14.1-7.1-22.7-11.9-22.7-19.2 .2-6.6 7.3-13.4 23.1-13.4 13.1-.3 22.7 2.8 29.9 5.9l3.6 1.7 5.5-33.6c-7.9-3.1-20.5-6.6-36-6.6-39.7 0-67.6 21.2-67.8 51.4-.3 22.3 20 34.7 35.2 42.2 15.5 7.6 20.8 12.6 20.8 19.3-.2 10.4-12.6 15.2-24.1 15.2-16 0-24.6-2.5-37.7-8.3l-5.3-2.5-5.6 34.9c9.4 4.3 26.8 8.1 44.8 8.3 42.2 .1 69.7-20.8 70-53zM528 331.4L495.6 176h-31.1c-9.6 0-16.9 2.8-21 12.9l-59.7 142.5H426s6.9-19.2 8.4-23.3H486c1.2 5.5 4.8 23.3 4.8 23.3H528z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'MASTERCARD'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M482.9 410.3c0 6.8-4.6 11.7-11.2 11.7-6.8 0-11.2-5.2-11.2-11.7 0-6.5 4.4-11.7 11.2-11.7 6.6 0 11.2 5.2 11.2 11.7zm-310.8-11.7c-7.1 0-11.2 5.2-11.2 11.7 0 6.5 4.1 11.7 11.2 11.7 6.5 0 10.9-4.9 10.9-11.7-.1-6.5-4.4-11.7-10.9-11.7zm117.5-.3c-5.4 0-8.7 3.5-9.5 8.7h19.1c-.9-5.7-4.4-8.7-9.6-8.7zm107.8 .3c-6.8 0-10.9 5.2-10.9 11.7 0 6.5 4.1 11.7 10.9 11.7 6.8 0 11.2-4.9 11.2-11.7 0-6.5-4.4-11.7-11.2-11.7zm105.9 26.1c0 .3 .3 .5 .3 1.1 0 .3-.3 .5-.3 1.1-.3 .3-.3 .5-.5 .8-.3 .3-.5 .5-1.1 .5-.3 .3-.5 .3-1.1 .3-.3 0-.5 0-1.1-.3-.3 0-.5-.3-.8-.5-.3-.3-.5-.5-.5-.8-.3-.5-.3-.8-.3-1.1 0-.5 0-.8 .3-1.1 0-.5 .3-.8 .5-1.1 .3-.3 .5-.3 .8-.5 .5-.3 .8-.3 1.1-.3 .5 0 .8 0 1.1 .3 .5 .3 .8 .3 1.1 .5s.2 .6 .5 1.1zm-2.2 1.4c.5 0 .5-.3 .8-.3 .3-.3 .3-.5 .3-.8 0-.3 0-.5-.3-.8-.3 0-.5-.3-1.1-.3h-1.6v3.5h.8V426h.3l1.1 1.4h.8l-1.1-1.3zM576 81v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V81c0-26.5 21.5-48 48-48h480c26.5 0 48 21.5 48 48zM64 220.6c0 76.5 62.1 138.5 138.5 138.5 27.2 0 53.9-8.2 76.5-23.1-72.9-59.3-72.4-171.2 0-230.5-22.6-15-49.3-23.1-76.5-23.1-76.4-.1-138.5 62-138.5 138.2zm224 108.8c70.5-55 70.2-162.2 0-217.5-70.2 55.3-70.5 162.6 0 217.5zm-142.3 76.3c0-8.7-5.7-14.4-14.7-14.7-4.6 0-9.5 1.4-12.8 6.5-2.4-4.1-6.5-6.5-12.2-6.5-3.8 0-7.6 1.4-10.6 5.4V392h-8.2v36.7h8.2c0-18.9-2.5-30.2 9-30.2 10.2 0 8.2 10.2 8.2 30.2h7.9c0-18.3-2.5-30.2 9-30.2 10.2 0 8.2 10 8.2 30.2h8.2v-23zm44.9-13.7h-7.9v4.4c-2.7-3.3-6.5-5.4-11.7-5.4-10.3 0-18.2 8.2-18.2 19.3 0 11.2 7.9 19.3 18.2 19.3 5.2 0 9-1.9 11.7-5.4v4.6h7.9V392zm40.5 25.6c0-15-22.9-8.2-22.9-15.2 0-5.7 11.9-4.8 18.5-1.1l3.3-6.5c-9.4-6.1-30.2-6-30.2 8.2 0 14.3 22.9 8.3 22.9 15 0 6.3-13.5 5.8-20.7 .8l-3.5 6.3c11.2 7.6 32.6 6 32.6-7.5zm35.4 9.3l-2.2-6.8c-3.8 2.1-12.2 4.4-12.2-4.1v-16.6h13.1V392h-13.1v-11.2h-8.2V392h-7.6v7.3h7.6V416c0 17.6 17.3 14.4 22.6 10.9zm13.3-13.4h27.5c0-16.2-7.4-22.6-17.4-22.6-10.6 0-18.2 7.9-18.2 19.3 0 20.5 22.6 23.9 33.8 14.2l-3.8-6c-7.8 6.4-19.6 5.8-21.9-4.9zm59.1-21.5c-4.6-2-11.6-1.8-15.2 4.4V392h-8.2v36.7h8.2V408c0-11.6 9.5-10.1 12.8-8.4l2.4-7.6zm10.6 18.3c0-11.4 11.6-15.1 20.7-8.4l3.8-6.5c-11.6-9.1-32.7-4.1-32.7 15 0 19.8 22.4 23.8 32.7 15l-3.8-6.5c-9.2 6.5-20.7 2.6-20.7-8.6zm66.7-18.3H408v4.4c-8.3-11-29.9-4.8-29.9 13.9 0 19.2 22.4 24.7 29.9 13.9v4.6h8.2V392zm33.7 0c-2.4-1.2-11-2.9-15.2 4.4V392h-7.9v36.7h7.9V408c0-11 9-10.3 12.8-8.4l2.4-7.6zm40.3-14.9h-7.9v19.3c-8.2-10.9-29.9-5.1-29.9 13.9 0 19.4 22.5 24.6 29.9 13.9v4.6h7.9v-51.7zm7.6-75.1v4.6h.8V302h1.9v-.8h-4.6v.8h1.9zm6.6 123.8c0-.5 0-1.1-.3-1.6-.3-.3-.5-.8-.8-1.1-.3-.3-.8-.5-1.1-.8-.5 0-1.1-.3-1.6-.3-.3 0-.8 .3-1.4 .3-.5 .3-.8 .5-1.1 .8-.5 .3-.8 .8-.8 1.1-.3 .5-.3 1.1-.3 1.6 0 .3 0 .8 .3 1.4 0 .3 .3 .8 .8 1.1 .3 .3 .5 .5 1.1 .8 .5 .3 1.1 .3 1.4 .3 .5 0 1.1 0 1.6-.3 .3-.3 .8-.5 1.1-.8 .3-.3 .5-.8 .8-1.1 .3-.6 .3-1.1 .3-1.4zm3.2-124.7h-1.4l-1.6 3.5-1.6-3.5h-1.4v5.4h.8v-4.1l1.6 3.5h1.1l1.4-3.5v4.1h1.1v-5.4zm4.4-80.5c0-76.2-62.1-138.3-138.5-138.3-27.2 0-53.9 8.2-76.5 23.1 72.1 59.3 73.2 171.5 0 230.5 22.6 15 49.5 23.1 76.5 23.1 76.4 .1 138.5-61.9 138.5-138.4z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'JCB'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M431.5 244.3V212c41.2 0 38.5 .2 38.5 .2 7.3 1.3 13.3 7.3 13.3 16 0 8.8-6 14.5-13.3 15.8-1.2 .4-3.3 .3-38.5 .3zm42.8 20.2c-2.8-.7-3.3-.5-42.8-.5v35c39.6 0 40 .2 42.8-.5 7.5-1.5 13.5-8 13.5-17 0-8.7-6-15.5-13.5-17zM576 80v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h480c26.5 0 48 21.5 48 48zM182 192.3h-57c0 67.1 10.7 109.7-35.8 109.7-19.5 0-38.8-5.7-57.2-14.8v28c30 8.3 68 8.3 68 8.3 97.9 0 82-47.7 82-131.2zm178.5 4.5c-63.4-16-165-14.9-165 59.3 0 77.1 108.2 73.6 165 59.2V287C312.9 311.7 253 309 253 256s59.8-55.6 107.5-31.2v-28zM544 286.5c0-18.5-16.5-30.5-38-32v-.8c19.5-2.7 30.3-15.5 30.3-30.2 0-19-15.7-30-37-31 0 0 6.3-.3-120.3-.3v127.5h122.7c24.3 .1 42.3-12.9 42.3-33.2z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'DISCOVER'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M520.4 196.1c0-7.9-5.5-12.1-15.6-12.1h-4.9v24.9h4.7c10.3 0 15.8-4.4 15.8-12.8zM528 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h480c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-44.1 138.9c22.6 0 52.9-4.1 52.9 24.4 0 12.6-6.6 20.7-18.7 23.2l25.8 34.4h-19.6l-22.2-32.8h-2.2v32.8h-16zm-55.9 .1h45.3v14H444v18.2h28.3V217H444v22.2h29.3V253H428zm-68.7 0l21.9 55.2 22.2-55.2h17.5l-35.5 84.2h-8.6l-35-84.2zm-55.9-3c24.7 0 44.6 20 44.6 44.6 0 24.7-20 44.6-44.6 44.6-24.7 0-44.6-20-44.6-44.6 0-24.7 20-44.6 44.6-44.6zm-49.3 6.1v19c-20.1-20.1-46.8-4.7-46.8 19 0 25 27.5 38.5 46.8 19.2v19c-29.7 14.3-63.3-5.7-63.3-38.2 0-31.2 33.1-53 63.3-38zm-97.2 66.3c11.4 0 22.4-15.3-3.3-24.4-15-5.5-20.2-11.4-20.2-22.7 0-23.2 30.6-31.4 49.7-14.3l-8.4 10.8c-10.4-11.6-24.9-6.2-24.9 2.5 0 4.4 2.7 6.9 12.3 10.3 18.2 6.6 23.6 12.5 23.6 25.6 0 29.5-38.8 37.4-56.6 11.3l10.3-9.9c3.7 7.1 9.9 10.8 17.5 10.8zM55.4 253H32v-82h23.4c26.1 0 44.1 17 44.1 41.1 0 18.5-13.2 40.9-44.1 40.9zm67.5 0h-16v-82h16zM544 433c0 8.2-6.8 15-15 15H128c189.6-35.6 382.7-139.2 416-160zM74.1 191.6c-5.2-4.9-11.6-6.6-21.9-6.6H48v54.2h4.2c10.3 0 17-2 21.9-6.4 5.7-5.2 8.9-12.8 8.9-20.7s-3.2-15.5-8.9-20.5z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'DISCOVER_DINERS'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M239.7 79.9c-96.9 0-175.8 78.6-175.8 175.8 0 96.9 78.9 175.8 175.8 175.8 97.2 0 175.8-78.9 175.8-175.8 0-97.2-78.6-175.8-175.8-175.8zm-39.9 279.6c-41.7-15.9-71.4-56.4-71.4-103.8s29.7-87.9 71.4-104.1v207.9zm79.8 .3V151.6c41.7 16.2 71.4 56.7 71.4 104.1s-29.7 87.9-71.4 104.1zM528 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h480c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM329.7 448h-90.3c-106.2 0-193.8-85.5-193.8-190.2C45.6 143.2 133.2 64 239.4 64h90.3c105 0 200.7 79.2 200.7 193.8 0 104.7-95.7 190.2-200.7 190.2z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'AMERICAN_EXPRESS'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M0 432c0 26.5 21.5 48 48 48H528c26.5 0 48-21.5 48-48v-1.1H514.3l-31.9-35.1-31.9 35.1H246.8V267.1H181L262.7 82.4h78.6l28.1 63.2V82.4h97.2L483.5 130l17-47.6H576V80c0-26.5-21.5-48-48-48H48C21.5 32 0 53.5 0 80V432zm440.4-21.7L482.6 364l42 46.3H576l-68-72.1 68-72.1H525.4l-42 46.7-41.5-46.7H390.5L458 338.6l-67.4 71.6V377.1h-83V354.9h80.9V322.6H307.6V300.2h83V267.1h-122V410.3H440.4zm96.3-72L576 380.2V296.9l-39.3 41.4zm-36.3-92l36.9-100.6V246.3H576V103H515.8l-32.2 89.3L451.7 103H390.5V246.1L327.3 103H276.1L213.7 246.3h43l11.9-28.7h65.9l12 28.7h82.7V146L466 246.3h34.4zM282 185.4l19.5-46.9 19.4 46.9H282z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'OTHER_BRAND'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M512 80c8.8 0 16 7.2 16 16l0 32L48 128l0-32c0-8.8 7.2-16 16-16l448 0zm16 144l0 192c0 8.8-7.2 16-16 16L64 432c-8.8 0-16-7.2-16-16l0-192 480 0zM64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l448 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm56 304c-13.3 0-24 10.7-24 24s10.7 24 24 24l48 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-48 0zm128 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l112 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-112 0z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'officialMempoolSpace'"> <ng-container *ngSwitchCase="'officialMempoolSpace'">
<svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M163.658 113.263C161.089 113.263 158.992 111.146 158.992 108.535C158.992 105.966 161.048 103.951 163.658 103.951C166.269 103.951 168.325 105.966 168.325 108.535C168.325 111.125 166.228 113.263 163.658 113.263Z" fill="#9857FF"/> <path d="M163.658 113.263C161.089 113.263 158.992 111.146 158.992 108.535C158.992 105.966 161.048 103.951 163.658 103.951C166.269 103.951 168.325 105.966 168.325 108.535C168.325 111.125 166.228 113.263 163.658 113.263Z" fill="#9857FF"/>

View File

@ -1,8 +0,0 @@
<div [formGroup]="timezoneForm" class="text-small text-center">
<select formControlName="mode" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 110px;" (change)="changeMode()">
<option value="local">UTC{{ localTimezoneOffset !== '+0' ? localTimezoneOffset : '' }} {{ localTimezoneName ? '- ' + localTimezoneName : '' }}</option>
<option value="+0" *ngIf="localTimezoneOffset !== '+0'">UTC - Greenwich Mean Time (GMT)</option>
<option disabled>────</option>
<option *ngFor="let timezone of timezones" [value]="timezone.offset">UTC{{ timezone.offset !== '+0' ? timezone.offset : '' }} - {{ timezone.name }}</option>
</select>
</div>

View File

@ -1,58 +0,0 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { StorageService } from '@app/services/storage.service';
import { StateService } from '@app/services/state.service';
import { timezones } from '@app/app.constants';
@Component({
selector: 'app-timezone-selector',
templateUrl: './timezone-selector.component.html',
styleUrls: ['./timezone-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimezoneSelectorComponent implements OnInit {
timezoneForm: UntypedFormGroup;
timezones = timezones;
localTimezoneOffset: string = '';
localTimezoneName: string;
constructor(
private formBuilder: UntypedFormBuilder,
private stateService: StateService,
private storageService: StorageService,
) { }
ngOnInit() {
this.setLocalTimezone();
this.timezoneForm = this.formBuilder.group({
mode: ['local'],
});
this.stateService.timezone$.subscribe((mode) => {
this.timezoneForm.get('mode')?.setValue(mode);
});
}
changeMode() {
const newMode = this.timezoneForm.get('mode')?.value;
this.storageService.setValue('timezone-preference', newMode);
this.stateService.timezone$.next(newMode);
}
setLocalTimezone() {
const offset = new Date().getTimezoneOffset();
const sign = offset <= 0 ? "+" : "-";
const absOffset = Math.abs(offset);
const hours = String(Math.floor(absOffset / 60));
const minutes = String(absOffset % 60).padStart(2, '0');
if (minutes === '00') {
this.localTimezoneOffset = `${sign}${hours}`;
} else {
this.localTimezoneOffset = `${sign}${hours.padStart(2, '0')}:${minutes}`;
}
const timezone = this.timezones.find(tz => tz.offset === this.localTimezoneOffset);
this.timezones = this.timezones.filter(tz => tz.offset !== this.localTimezoneOffset && tz.offset !== '+0');
this.localTimezoneName = timezone ? timezone.name : '';
}
}

View File

@ -8,7 +8,7 @@
</a> </a>
} @else if (enterpriseInfo?.img || enterpriseInfo?.imageMd5) { } @else if (enterpriseInfo?.img || enterpriseInfo?.imageMd5) {
<a [routerLink]="['/' | relativeUrl]"> <a [routerLink]="['/' | relativeUrl]">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + enterpriseInfo.name + '/logo'" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}"> <img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + enterpriseInfo.name + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</a> </a>
<div class="vertical-line"></div> <div class="vertical-line"></div>
} }
@ -88,7 +88,7 @@
<div class="field narrower mt-2"> <div class="field narrower mt-2">
<div class="label" i18n="transaction.confirmed-at">Confirmed at</div> <div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
<div class="value"> <div class="value">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="tx.status.block_time" [hideTimeSince]="true"></app-timestamp> &lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline"> <div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true" [showTooltip]="true"></app-time>)</i> <i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true" [showTooltip]="true"></app-time>)</i>
</div> </div>
@ -124,6 +124,7 @@
<ng-container *ngIf="(ETA$ | async) as eta;"> <ng-container *ngIf="(ETA$ | async) as eta;">
<app-accelerate-checkout <app-accelerate-checkout
*ngIf="(da$ | async) as da;" *ngIf="(da$ | async) as da;"
[cashappEnabled]="cashappEligible"
[advancedEnabled]="false" [advancedEnabled]="false"
[forceMobile]="true" [forceMobile]="true"
[tx]="tx" [tx]="tx"

View File

@ -756,6 +756,10 @@ export class TrackerComponent implements OnInit, OnDestroy {
} }
} }
get cashappEligible(): boolean {
return this.mempoolPosition?.block > 0 && this.tx.weight < 4000;
}
get showAccelerationSummary(): boolean { get showAccelerationSummary(): boolean {
return ( return (
this.tx this.tx

View File

@ -61,7 +61,10 @@
<tr> <tr>
<td i18n="block.timestamp">Timestamp</td> <td i18n="block.timestamp">Timestamp</td>
<td> <td>
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="tx.status.block_time"></app-timestamp> &lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
<div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
</div>
</td> </td>
</tr> </tr>
} @else { } @else {

View File

@ -24,7 +24,6 @@
[height]="tx?.status?.block_height" [height]="tx?.status?.block_height"
[replaced]="replaced" [replaced]="replaced"
[removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed" [removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed"
[cached]="isCached"
></app-confirmations> ></app-confirmations>
</div> </div>
</ng-container> </ng-container>
@ -139,6 +138,7 @@
<app-accelerate-checkout <app-accelerate-checkout
*ngIf="(da$ | async) as da;" *ngIf="(da$ | async) as da;"
[cashappEnabled]="cashappEligible"
[advancedEnabled]="true" [advancedEnabled]="true"
[tx]="tx" [tx]="tx"
[accelerating]="isAcceleration" [accelerating]="isAcceleration"

View File

@ -156,6 +156,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
showAccelerationDetails = false; showAccelerationDetails = false;
hasAccelerationDetails = false; hasAccelerationDetails = false;
scrollIntoAccelPreview = false; scrollIntoAccelPreview = false;
cashappEligible = false;
auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true; auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true;
isMempoolSpaceBuild = this.stateService.isMempoolSpaceBuild; isMempoolSpaceBuild = this.stateService.isMempoolSpaceBuild;
@ -239,7 +240,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
retry({ count: 2, delay: 2000 }), retry({ count: 2, delay: 2000 }),
// Try again until we either get a valid response, or the transaction is confirmed // Try again until we either get a valid response, or the transaction is confirmed
repeat({ delay: 2000 }), 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), take(1),
)), )),
) )
@ -527,6 +528,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.miningStats = stats; this.miningStats = stats;
}); });
} }
if (txPosition.position?.block > 0 && this.tx.weight < 4000) {
this.cashappEligible = true;
}
if (!this.gotInitialPosition && txPosition.position?.block === 0 && txPosition.position?.vsize < 750_000) { if (!this.gotInitialPosition && txPosition.position?.block === 0 && txPosition.position?.vsize < 750_000) {
this.accelerationFlowCompleted = true; this.accelerationFlowCompleted = true;
} }
@ -1032,6 +1036,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.showAccelerationDetails = false; this.showAccelerationDetails = false;
this.accelerationFlowCompleted = false; this.accelerationFlowCompleted = false;
this.accelerationInfo = null; this.accelerationInfo = null;
this.cashappEligible = false;
this.txInBlockIndex = null; this.txInBlockIndex = null;
this.mempoolPosition = null; this.mempoolPosition = null;
this.pool = null; this.pool = null;

View File

@ -6,7 +6,7 @@
<app-truncate [text]="tx.txid"></app-truncate> <app-truncate [text]="tx.txid"></app-truncate>
</a> </a>
<div> <div>
<ng-template [ngIf]="tx.status.confirmed"><app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="tx.status.block_time" [hideTimeSince]="true"></app-timestamp></ng-template> <ng-template [ngIf]="tx.status.confirmed">&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}</ng-template>
<ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen"> <ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
<i><app-time kind="since" [time]="tx.firstSeen" [fastRender]="true" [showTooltip]="true"></app-time></i> <i><app-time kind="since" [time]="tx.firstSeen" [fastRender]="true" [showTooltip]="true"></app-time></i>
</ng-template> </ng-template>
@ -81,7 +81,7 @@
</ng-container> </ng-container>
</div> </div>
</td> </td>
<td class="text-right nowrap amount" [class]="{large: tx.largeInput}"> <td class="text-right nowrap amount" [class]="{large: vin?.prevout?.value > 1000000000 || vin.isInscription}">
<button *ngIf="vin.isInscription" (click)="toggleOrdData(tx.txid, 'vin', vindex)" type="button" class="btn btn-sm badge badge-ord primary" style="margin-right: 10px;">Inscription</button> <button *ngIf="vin.isInscription" (click)="toggleOrdData(tx.txid, 'vin', vindex)" type="button" class="btn btn-sm badge badge-ord primary" style="margin-right: 10px;">Inscription</button>
<ng-template [ngIf]="vin.prevout && vin.prevout.asset && vin.prevout.asset !== nativeAssetId" [ngIfElse]="defaultOutput"> <ng-template [ngIf]="vin.prevout && vin.prevout.asset && vin.prevout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
<div *ngIf="assetsMinimal && assetsMinimal[vin.prevout.asset] else assetVinNotFound"> <div *ngIf="assetsMinimal && assetsMinimal[vin.prevout.asset] else assetVinNotFound">
@ -257,7 +257,7 @@
</ng-template> </ng-template>
</ng-template> </ng-template>
</td> </td>
<td class="text-right nowrap amount" [class]="{large: tx.largeOutput}"> <td class="text-right nowrap amount" [class]="{large: vout?.value > 1000000000}">
<ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput"> <ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
<div *ngIf="assetsMinimal && assetsMinimal[vout.asset] else assetNotFound"> <div *ngIf="assetsMinimal && assetsMinimal[vout.asset] else assetNotFound">
<ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container> <ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container>

View File

@ -202,12 +202,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
for (const address of this.addresses) { for (const address of this.addresses) {
switch (address.length) { switch (address.length) {
case 130: { case 130: {
if (v.scriptpubkey === '41' + address + 'ac') { if (v.scriptpubkey === '21' + address + 'ac') {
return v.value; return v.value;
} }
} break; } break;
case 66: { case 66: {
if (v.scriptpubkey === '21' + address + 'ac') { if (v.scriptpubkey === '41' + address + 'ac') {
return v.value; return v.value;
} }
} break; } break;
@ -224,12 +224,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
for (const address of this.addresses) { for (const address of this.addresses) {
switch (address.length) { switch (address.length) {
case 130: { case 130: {
if (v.prevout?.scriptpubkey === '41' + address + 'ac') { if (v.prevout?.scriptpubkey === '21' + address + 'ac') {
return v.prevout?.value; return v.prevout?.value;
} }
} break; } break;
case 66: { case 66: {
if (v.prevout?.scriptpubkey === '21' + address + 'ac') { if (v.prevout?.scriptpubkey === '41' + address + 'ac') {
return v.prevout?.value; return v.prevout?.value;
} }
} break; } break;
@ -258,7 +258,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
const hasAnnex = tx.vin[i].witness?.[tx.vin[i].witness.length - 1].startsWith('50'); const hasAnnex = tx.vin[i].witness?.[tx.vin[i].witness.length - 1].startsWith('50');
if (tx.vin[i].witness.length > (hasAnnex ? 2 : 1) && tx.vin[i].witness[tx.vin[i].witness.length - (hasAnnex ? 3 : 2)].includes('0063036f7264')) { if (tx.vin[i].witness.length > (hasAnnex ? 2 : 1) && tx.vin[i].witness[tx.vin[i].witness.length - (hasAnnex ? 3 : 2)].includes('0063036f7264')) {
tx.vin[i].isInscription = true; tx.vin[i].isInscription = true;
tx.largeInput = true;
} }
} }
} }
@ -269,9 +268,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
} }
} }
} }
tx.largeInput = tx.largeInput || tx.vin.some(vin => (vin?.prevout?.value > 1000000000));
tx.largeOutput = tx.vout.some(vout => (vout?.value > 1000000000));
}); });
if (this.blockTime && this.transactions?.length && this.currency) { if (this.blockTime && this.transactions?.length && this.currency) {
@ -355,12 +351,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
this.electrsApiService.getTransaction$(tx.txid) this.electrsApiService.getTransaction$(tx.txid)
.subscribe((newTx) => { .subscribe((newTx) => {
tx['@vinLoaded'] = true; tx['@vinLoaded'] = true;
let temp = tx.vin;
tx.vin = newTx.vin; tx.vin = newTx.vin;
tx.fee = newTx.fee; tx.fee = newTx.fee;
for (const [index, vin] of temp.entries()) {
newTx.vin[index].isInscription = vin.isInscription;
}
this.ref.markForCheck(); this.ref.markForCheck();
}); });
} }

View File

@ -1,6 +1,6 @@
<a href="#" (click)="twitterLogin()" <a href="#" (click)="twitterLogin()"
[class]="(disabled ? 'disabled': '') + (customClass ? customClass : 'w-100 btn mt-1 d-flex justify-content-center align-items-center')" [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 : ''"> style="background-color: #1DA1F2" [style]="width ? 'width: ' + width : ''">
<img src="./resources/twitter.svg" height="25" style="padding: 2px" [alt]="buttonString + ' with Twitter'" />
<span class="ml-2 text-light align-middle">{{ buttonString }}</span> <span class="ml-2 text-light align-middle">{{ buttonString }}</span>
<img src="./resources/x.svg" height="25" style="padding: 2px; padding-left: 5px" [alt]="buttonString + ' with Twitter'" />
</a> </a>

View File

@ -1,31 +0,0 @@
<div class="box preview-box" *ngIf="(walletAddresses$ | async) as walletAddresses">
<app-preview-title>
<span i18n="shared.wallet">Wallet</span>
</app-preview-title>
<div>
<div class="table-col">
<table class="table table-borderless dual-col-striped table-fixed wallet-table" *ngIf="(walletStats$ | async) as walletStats">
<tbody>
<tr>
<td i18n="address.number-addresses">Addresses</td>
<td class="wrap-cell">{{ addressStrings.length }}</td>
<td class="spacer"></td>
<td i18n="address.utxos">UTXOs</td>
<td class="wrap-cell">{{ walletStats.utxos }}</td>
</tr>
<tr>
<td i18n="wallet.balance-btc">Balance (BTC)</td>
<td class="wrap-cell"><app-amount [satoshis]="walletStats.balance" [noFiat]="true" [digitsInfo]="walletStats.balance > 1_000_000_000 ? '1.4-4' : '1.8-8'"></app-amount></td>
<td class="spacer"></td>
<td i18n="wallet.balance-usd">Balance (USD)</td>
<td class="wrap-cell"><span class="fiat"><app-fiat [value]="walletStats.balance"></app-fiat></span></td>
</tr>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col-md graph-col">
<app-address-graph [addressSummary$]="walletSummary$" period="all" [widget]="true" [defaultFiat]="true" [height]="330" [left]="-40" [right]="-40" [showLegend]="false" [showYAxis]="false"/>
</div>
</div>
</div>

View File

@ -1,31 +0,0 @@
.title-wrapper {
padding: 0 15px;
}
.graph-col {
height: 350px;
text-align: center;
padding: 0;
margin-left: 2px;
margin-right: 15px;
}
.table-col {
overflow: hidden;
}
.table {
font-size: 32px;
::ng-deep .symbol {
font-size: 24px;
}
.spacer {
background: none;
}
}
.fiat {
display: block;
}

View File

@ -1,245 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap, catchError, map, tap, shareReplay, startWith, scan } from 'rxjs/operators';
import { Address, AddressTxSummary, ChainStats, Transaction } from '@interfaces/electrs.interface';
import { StateService } from '@app/services/state.service';
import { ApiService } from '@app/services/api.service';
import { of, Observable, Subscription } from 'rxjs';
import { SeoService } from '@app/services/seo.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { WalletAddress } from '@interfaces/node-api.interface';
import { OpenGraphService } from '../../services/opengraph.service';
import { WebsocketService } from '../../services/websocket.service';
class WalletStats implements ChainStats {
addresses: string[];
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
constructor (stats: ChainStats[], addresses: string[]) {
Object.assign(this, stats.reduce((acc, stat) => {
acc.funded_txo_count += stat.funded_txo_count;
acc.funded_txo_sum += stat.funded_txo_sum;
acc.spent_txo_count += stat.spent_txo_count;
acc.spent_txo_sum += stat.spent_txo_sum;
return acc;
}, {
funded_txo_count: 0,
funded_txo_sum: 0,
spent_txo_count: 0,
spent_txo_sum: 0,
tx_count: 0,
})
);
this.addresses = addresses;
}
public addTx(tx: Transaction): void {
for (const vin of tx.vin) {
if (this.addresses.includes(vin.prevout?.scriptpubkey_address)) {
this.spendTxo(vin.prevout.value);
}
}
for (const vout of tx.vout) {
if (this.addresses.includes(vout.scriptpubkey_address)) {
this.fundTxo(vout.value);
}
}
this.tx_count++;
}
public removeTx(tx: Transaction): void {
for (const vin of tx.vin) {
if (this.addresses.includes(vin.prevout?.scriptpubkey_address)) {
this.unspendTxo(vin.prevout.value);
}
}
for (const vout of tx.vout) {
if (this.addresses.includes(vout.scriptpubkey_address)) {
this.unfundTxo(vout.value);
}
}
this.tx_count--;
}
private fundTxo(value: number): void {
this.funded_txo_sum += value;
this.funded_txo_count++;
}
private unfundTxo(value: number): void {
this.funded_txo_sum -= value;
this.funded_txo_count--;
}
private spendTxo(value: number): void {
this.spent_txo_sum += value;
this.spent_txo_count++;
}
private unspendTxo(value: number): void {
this.spent_txo_sum -= value;
this.spent_txo_count--;
}
get balance(): number {
return this.funded_txo_sum - this.spent_txo_sum;
}
get totalReceived(): number {
return this.funded_txo_sum;
}
get utxos(): number {
return this.funded_txo_count - this.spent_txo_count;
}
}
@Component({
selector: 'app-wallet-preview',
templateUrl: './wallet-preview.component.html',
styleUrls: ['./wallet-preview.component.scss']
})
export class WalletPreviewComponent implements OnInit, OnDestroy {
network = '';
addresses: Address[] = [];
addressStrings: string[] = [];
walletName: string;
isLoadingWallet = true;
wallet$: Observable<Record<string, WalletAddress>>;
walletAddresses$: Observable<Record<string, Address>>;
walletSummary$: Observable<AddressTxSummary[]>;
walletStats$: Observable<WalletStats>;
error: any;
walletSubscription: Subscription;
collapseAddresses: boolean = true;
fullyLoaded = false;
txCount = 0;
received = 0;
sent = 0;
chainBalance = 0;
constructor(
private route: ActivatedRoute,
private stateService: StateService,
private apiService: ApiService,
private seoService: SeoService,
private websocketService: WebsocketService,
private openGraphService: OpenGraphService,
) { }
ngOnInit(): void {
this.websocketService.want(['blocks', 'stats']);
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.wallet$ = this.route.paramMap.pipe(
map((params: ParamMap) => params.get('wallet') as string),
tap((walletName: string) => {
this.walletName = walletName;
this.openGraphService.waitFor('wallet-addresses-' + this.walletName);
this.openGraphService.waitFor('wallet-data-' + this.walletName);
this.openGraphService.waitFor('wallet-txs-' + this.walletName);
this.seoService.setTitle($localize`:@@wallet.component.browser-title:Wallet: ${walletName}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.wallet:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} wallet ${walletName}:INTERPOLATION:.`);
}),
switchMap((walletName: string) => this.apiService.getWallet$(walletName).pipe(
catchError((err) => {
this.error = err;
this.seoService.logSoft404();
console.log(err);
this.openGraphService.fail('wallet-addresses-' + this.walletName);
this.openGraphService.fail('wallet-data-' + this.walletName);
this.openGraphService.fail('wallet-txs-' + this.walletName);
return of({});
})
)),
shareReplay(1),
);
this.walletAddresses$ = this.wallet$.pipe(
map(wallet => {
const walletInfo: Record<string, Address> = {};
for (const address of Object.keys(wallet)) {
walletInfo[address] = {
address,
chain_stats: wallet[address].stats,
mempool_stats: {
funded_txo_count: 0,
funded_txo_sum: 0,
spent_txo_count: 0, spent_txo_sum: 0, tx_count: 0
},
};
}
return walletInfo;
}),
tap(() => {
this.isLoadingWallet = false;
})
);
this.walletSubscription = this.walletAddresses$.subscribe(wallet => {
this.addressStrings = Object.keys(wallet);
this.addresses = Object.values(wallet);
this.openGraphService.waitOver('wallet-addresses-' + this.walletName);
});
this.walletSummary$ = this.wallet$.pipe(
map(wallet => this.deduplicateWalletTransactions(Object.values(wallet).flatMap(address => address.transactions))),
tap(() => {
this.openGraphService.waitOver('wallet-txs-' + this.walletName);
})
);
this.walletStats$ = this.wallet$.pipe(
switchMap(wallet => {
const walletStats = new WalletStats(Object.values(wallet).map(w => w.stats), Object.keys(wallet));
return this.stateService.walletTransactions$.pipe(
startWith([]),
scan((stats, newTransactions) => {
for (const tx of newTransactions) {
stats.addTx(tx);
}
return stats;
}, walletStats),
);
}),
tap(() => {
this.openGraphService.waitOver('wallet-data-' + this.walletName);
})
);
}
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
const transactions = new Map<string, AddressTxSummary>();
for (const tx of walletTransactions) {
if (transactions.has(tx.txid)) {
transactions.get(tx.txid).value += tx.value;
} else {
transactions.set(tx.txid, tx);
}
}
return Array.from(transactions.values()).sort((a, b) => {
if (a.height === b.height) {
return b.tx_position - a.tx_position;
}
return b.height - a.height;
});
}
normalizeAddress(address: string): string {
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) {
return address.toLowerCase();
} else {
return address;
}
}
ngOnDestroy(): void {
this.walletSubscription.unsubscribe();
}
}

View File

@ -1,6 +1,6 @@
<div class="container-xl" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'"> <div class="container-xl" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
<div class="title-address"> <div class="title-address">
<h1>{{ walletName }}</h1> <h1 i18n="shared.wallet">Wallet</h1>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
@ -74,36 +74,6 @@
</ng-container> </ng-container>
</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> <ng-template #loadingTemplate>
<div class="box" *ngIf="!error; else errorTemplate"> <div class="box" *ngIf="!error; else errorTemplate">

View File

@ -9,8 +9,6 @@ import { of, Observable, Subscription } from 'rxjs';
import { SeoService } from '@app/services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils'; import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { WalletAddress } from '@interfaces/node-api.interface'; 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 { class WalletStats implements ChainStats {
addresses: string[]; addresses: string[];
@ -26,7 +24,6 @@ class WalletStats implements ChainStats {
acc.funded_txo_sum += stat.funded_txo_sum; acc.funded_txo_sum += stat.funded_txo_sum;
acc.spent_txo_count += stat.spent_txo_count; acc.spent_txo_count += stat.spent_txo_count;
acc.spent_txo_sum += stat.spent_txo_sum; acc.spent_txo_sum += stat.spent_txo_sum;
acc.tx_count += stat.tx_count;
return acc; return acc;
}, { }, {
funded_txo_count: 0, funded_txo_count: 0,
@ -112,17 +109,12 @@ export class WalletComponent implements OnInit, OnDestroy {
addressStrings: string[] = []; addressStrings: string[] = [];
walletName: string; walletName: string;
isLoadingWallet = true; isLoadingWallet = true;
isLoadingTransactions = true;
transactions: Transaction[];
totalTransactionCount: number;
retryLoadMore = false;
wallet$: Observable<Record<string, WalletAddress>>; wallet$: Observable<Record<string, WalletAddress>>;
walletAddresses$: Observable<Record<string, Address>>; walletAddresses$: Observable<Record<string, Address>>;
walletSummary$: Observable<AddressTxSummary[]>; walletSummary$: Observable<AddressTxSummary[]>;
walletStats$: Observable<WalletStats>; walletStats$: Observable<WalletStats>;
error: any; error: any;
walletSubscription: Subscription; walletSubscription: Subscription;
transactionSubscription: Subscription;
collapseAddresses: boolean = true; collapseAddresses: boolean = true;
@ -137,8 +129,6 @@ export class WalletComponent implements OnInit, OnDestroy {
private websocketService: WebsocketService, private websocketService: WebsocketService,
private stateService: StateService, private stateService: StateService,
private apiService: ApiService, private apiService: ApiService,
private electrsApiService: ElectrsApiService,
private audioService: AudioService,
private seoService: SeoService, private seoService: SeoService,
) { } ) { }
@ -182,21 +172,6 @@ export class WalletComponent implements OnInit, OnDestroy {
}), }),
switchMap(initial => this.stateService.walletTransactions$.pipe( switchMap(initial => this.stateService.walletTransactions$.pipe(
startWith(null), 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) => { scan((wallet, walletTransactions) => {
for (const tx of (walletTransactions || [])) { for (const tx of (walletTransactions || [])) {
const funded: Record<string, number> = {}; const funded: Record<string, number> = {};
@ -292,57 +267,8 @@ export class WalletComponent implements OnInit, OnDestroy {
return stats; return stats;
}, walletStats), }, 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[] { deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
@ -373,6 +299,5 @@ export class WalletComponent implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.websocketService.stopTrackingWallet(); this.websocketService.stopTrackingWallet();
this.walletSubscription.unsubscribe(); this.walletSubscription.unsubscribe();
this.transactionSubscription.unsubscribe();
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Env, StateService } from '@app/services/state.service'; 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'; import { faqData } from '@app/docs/api-docs/api-docs-data';
@Component({ @Component({
@ -28,8 +28,6 @@ export class ApiDocsNavComponent implements OnInit {
this.auditEnabled = this.env.AUDIT; this.auditEnabled = this.env.AUDIT;
if (this.whichTab === 'rest') { if (this.whichTab === 'rest') {
this.tabData = restApiDocsData; this.tabData = restApiDocsData;
} else if (this.whichTab === 'websocket') {
this.tabData = wsApiDocsData;
} else if (this.whichTab === 'faq') { } else if (this.whichTab === 'faq') {
this.tabData = faqData; this.tabData = faqData;
} }

View File

@ -108,43 +108,18 @@
</div> </div>
</div> </div>
<div id="websocketAPI" *ngIf="whichTab === 'websocket'"> <div id="websocketAPI" *ngIf="( whichTab === 'websocket' )">
<div class="api-category">
<div id="doc-nav-desktop" class="hide-on-mobile" [ngClass]="desktopDocsNavPosition"> <div class="websocket">
<app-api-docs-nav (navLinkClickEvent)="anchorLinkClick( $event )" [network]="{ val: network$ | async }" [whichTab]="whichTab"></app-api-docs-nav> <div class="endpoint">
<div class="subtitle" i18n="Api docs endpoint">Endpoint</div>
{{ wrapUrl(network.val, wsDocs, true) }}
</div> </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>
</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="description">
<div class="subtitle" i18n>Description</div> <div class="subtitle" i18n>Description</div>
<div [innerHTML]="item.description.default" i18n></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>
<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> </div>
<app-code-template [method]="'websocket'" [hostname]="hostname" [code]="wsDocs" [network]="network.val" [showCodeExample]="wsDocs.showJsExamples"></app-code-template>
</div> </div>
</div> </div>
</div> </div>

View File

@ -470,21 +470,3 @@ dd {
margin-left: 1em; 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;
}
}

View File

@ -145,7 +145,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
if (document.getElementById( targetId + "-tab-header" )) { if (document.getElementById( targetId + "-tab-header" )) {
tabHeaderHeight = document.getElementById( targetId + "-tab-header" ).scrollHeight; 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 endpointContainerEl = document.querySelector<HTMLElement>( "#" + targetId );
const endpointContentEl = document.querySelector<HTMLElement>( "#" + targetId + " .endpoint-content" ); const endpointContentEl = document.querySelector<HTMLElement>( "#" + targetId + " .endpoint-content" );
const endPointContentElHeight = endpointContentEl.clientHeight; const endPointContentElHeight = endpointContentEl.clientHeight;
@ -207,29 +207,13 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
text = text.replace('%{' + indexNumber + '}', curlText); 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}`; 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`;
}
} }

View File

@ -36,7 +36,6 @@ import { HashrateChartPoolsComponent } from '@components/hashrates-chart-pools/h
import { BlockHealthGraphComponent } from '@components/block-health-graph/block-health-graph.component'; import { BlockHealthGraphComponent } from '@components/block-health-graph/block-health-graph.component';
import { AddressComponent } from '@components/address/address.component'; import { AddressComponent } from '@components/address/address.component';
import { WalletComponent } from '@components/wallet/wallet.component'; import { WalletComponent } from '@components/wallet/wallet.component';
import { WalletPreviewComponent } from '@components/wallet/wallet-preview.component';
import { AddressGraphComponent } from '@components/address-graph/address-graph.component'; import { AddressGraphComponent } from '@components/address-graph/address-graph.component';
import { UtxoGraphComponent } from '@components/utxo-graph/utxo-graph.component'; import { UtxoGraphComponent } from '@components/utxo-graph/utxo-graph.component';
import { ActiveAccelerationBox } from '@components/acceleration/active-acceleration-box/active-acceleration-box.component'; import { ActiveAccelerationBox } from '@components/acceleration/active-acceleration-box/active-acceleration-box.component';
@ -50,7 +49,6 @@ import { CommonModule } from '@angular/common';
MempoolBlockComponent, MempoolBlockComponent,
AddressComponent, AddressComponent,
WalletComponent, WalletComponent,
WalletPreviewComponent,
MiningDashboardComponent, MiningDashboardComponent,
AcceleratorDashboardComponent, AcceleratorDashboardComponent,

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