Merge branch 'master' into nymkappa/fix-bitcoin-payment
This commit is contained in:
commit
9eb85200e0
72
.github/workflows/ci.yml
vendored
72
.github/workflows/ci.yml
vendored
@ -251,17 +251,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
module: ["mempool", "liquid"]
|
module: ["mempool", "liquid", "testnet4"]
|
||||||
include:
|
|
||||||
- module: "mempool"
|
|
||||||
spec: |
|
|
||||||
cypress/e2e/mainnet/*.spec.ts
|
|
||||||
cypress/e2e/signet/*.spec.ts
|
|
||||||
cypress/e2e/testnet4/*.spec.ts
|
|
||||||
- module: "liquid"
|
|
||||||
spec: |
|
|
||||||
cypress/e2e/liquid/liquid.spec.ts
|
|
||||||
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
|
|
||||||
|
|
||||||
name: E2E tests for ${{ matrix.module }}
|
name: E2E tests for ${{ matrix.module }}
|
||||||
steps:
|
steps:
|
||||||
@ -310,8 +300,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Unzip assets before building (src/resources)
|
- name: Unzip assets before building (src/resources)
|
||||||
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
|
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
|
||||||
|
|
||||||
|
# mempool
|
||||||
- name: Chrome browser tests (${{ matrix.module }})
|
- name: Chrome browser tests (${{ matrix.module }})
|
||||||
|
if: ${{ matrix.module == 'mempool' }}
|
||||||
uses: cypress-io/github-action@v5
|
uses: cypress-io/github-action@v5
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.event_name }}
|
tag: ${{ github.event_name }}
|
||||||
@ -322,7 +314,9 @@ jobs:
|
|||||||
wait-on-timeout: 120
|
wait-on-timeout: 120
|
||||||
record: true
|
record: true
|
||||||
parallel: true
|
parallel: true
|
||||||
spec: ${{ matrix.spec }}
|
spec: |
|
||||||
|
cypress/e2e/mainnet/*.spec.ts
|
||||||
|
cypress/e2e/signet/*.spec.ts
|
||||||
group: Tests on Chrome (${{ matrix.module }})
|
group: Tests on Chrome (${{ matrix.module }})
|
||||||
browser: "chrome"
|
browser: "chrome"
|
||||||
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
|
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
|
||||||
@ -332,6 +326,56 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
|
# liquid
|
||||||
|
- name: Chrome browser tests (${{ matrix.module }})
|
||||||
|
if: ${{ matrix.module == 'liquid' }}
|
||||||
|
uses: cypress-io/github-action@v5
|
||||||
|
with:
|
||||||
|
tag: ${{ github.event_name }}
|
||||||
|
working-directory: ${{ matrix.module }}/frontend
|
||||||
|
build: npm run config:defaults:${{ matrix.module }}
|
||||||
|
start: npm run start:local-staging
|
||||||
|
wait-on: "http://localhost:4200"
|
||||||
|
wait-on-timeout: 120
|
||||||
|
record: true
|
||||||
|
parallel: true
|
||||||
|
spec: |
|
||||||
|
cypress/e2e/liquid/liquid.spec.ts
|
||||||
|
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
|
||||||
|
group: Tests on Chrome (${{ matrix.module }})
|
||||||
|
browser: "chrome"
|
||||||
|
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
|
||||||
|
env:
|
||||||
|
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
|
# testnet
|
||||||
|
- name: Chrome browser tests (${{ matrix.module }})
|
||||||
|
if: ${{ matrix.module == 'testnet4' }}
|
||||||
|
uses: cypress-io/github-action@v5
|
||||||
|
with:
|
||||||
|
tag: ${{ github.event_name }}
|
||||||
|
working-directory: ${{ matrix.module }}/frontend
|
||||||
|
build: npm run config:defaults:mempool
|
||||||
|
start: npm run start:local-staging
|
||||||
|
wait-on: "http://localhost:4200"
|
||||||
|
wait-on-timeout: 120
|
||||||
|
record: true
|
||||||
|
parallel: true
|
||||||
|
spec: |
|
||||||
|
cypress/e2e/testnet4/*.spec.ts
|
||||||
|
group: Tests on Chrome (${{ matrix.module }})
|
||||||
|
browser: "chrome"
|
||||||
|
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
|
||||||
|
env:
|
||||||
|
CYPRESS_REROUTE_TESTNET: true
|
||||||
|
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
validate_docker_json:
|
validate_docker_json:
|
||||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
@ -359,4 +403,4 @@ jobs:
|
|||||||
- name: Validate JSON syntax
|
- name: Validate JSON syntax
|
||||||
run: |
|
run: |
|
||||||
cat mempool-config.json | jq
|
cat mempool-config.json | jq
|
||||||
working-directory: docker/docker/backend
|
working-directory: docker/docker/backend
|
||||||
@ -7,7 +7,7 @@ const config: Config.InitialOptions = {
|
|||||||
automock: false,
|
automock: false,
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
collectCoverageFrom: ["./src/**/**.ts"],
|
collectCoverageFrom: ["./src/**/**.ts"],
|
||||||
coverageProvider: "babel",
|
coverageProvider: "v8",
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
lines: 1
|
lines: 1
|
||||||
|
|||||||
@ -155,6 +155,10 @@
|
|||||||
"API": "https://mempool.space/api/v1/services",
|
"API": "https://mempool.space/api/v1/services",
|
||||||
"ACCELERATIONS": false
|
"ACCELERATIONS": false
|
||||||
},
|
},
|
||||||
|
"STRATUM": {
|
||||||
|
"ENABLED": false,
|
||||||
|
"API": "http://localhost:1234"
|
||||||
|
},
|
||||||
"FIAT_PRICE": {
|
"FIAT_PRICE": {
|
||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
"PAID": false,
|
"PAID": false,
|
||||||
|
|||||||
55
backend/package-lock.json
generated
55
backend/package-lock.json
generated
@ -10,7 +10,6 @@
|
|||||||
"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",
|
||||||
@ -18,7 +17,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.11.0",
|
"mysql2": "~3.12.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",
|
||||||
@ -26,8 +25,6 @@
|
|||||||
"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",
|
||||||
@ -6000,6 +5997,21 @@
|
|||||||
"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",
|
||||||
@ -6161,16 +6173,17 @@
|
|||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
"node_modules/mysql2": {
|
"node_modules/mysql2": {
|
||||||
"version": "3.11.0",
|
"version": "3.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
|
||||||
"integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==",
|
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
|
||||||
|
"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-cache": "^8.0.0",
|
"lru.min": "^1.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"
|
||||||
@ -6190,14 +6203,6 @@
|
|||||||
"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",
|
||||||
@ -12213,6 +12218,11 @@
|
|||||||
"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",
|
||||||
@ -12327,16 +12337,16 @@
|
|||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
"mysql2": {
|
"mysql2": {
|
||||||
"version": "3.11.0",
|
"version": "3.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
|
||||||
"integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==",
|
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
|
||||||
"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-cache": "^8.0.0",
|
"lru.min": "^1.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"
|
||||||
@ -12349,11 +12359,6 @@
|
|||||||
"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=="
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -39,7 +39,6 @@
|
|||||||
"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",
|
||||||
@ -47,7 +46,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.11.0",
|
"mysql2": "~3.12.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",
|
||||||
@ -55,8 +54,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@ -151,5 +151,9 @@
|
|||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
"PAID": false,
|
"PAID": false,
|
||||||
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
|
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
|
||||||
|
},
|
||||||
|
"STRATUM": {
|
||||||
|
"ENABLED": false,
|
||||||
|
"API": "http://localhost:1234"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -159,6 +159,11 @@ describe('Mempool Backend Config', () => {
|
|||||||
PAID: false,
|
PAID: false,
|
||||||
API_KEY: '',
|
API_KEY: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(config.STRATUM).toStrictEqual({
|
||||||
|
ENABLED: false,
|
||||||
|
API: 'http://localhost:1234',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { Application, NextFunction, Request, Response } from 'express';
|
import { Application, NextFunction, Request, Response } from 'express';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import bitcoinClient from './bitcoin-client';
|
import bitcoinClient from './bitcoin-client';
|
||||||
|
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
|
||||||
@ -9,26 +14,26 @@ import bitcoinClient from './bitcoin-client';
|
|||||||
class BitcoinBackendRoutes {
|
class BitcoinBackendRoutes {
|
||||||
private static tag = 'BitcoinBackendRoutes';
|
private static tag = 'BitcoinBackendRoutes';
|
||||||
|
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application): void {
|
||||||
app
|
app
|
||||||
.get('/api/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('/api/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)
|
||||||
.get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
|
||||||
.post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
|
||||||
.post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
|
||||||
.get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
|
||||||
.get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
|
||||||
.get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
|
||||||
.get('/api/internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable caching for bitcoin core routes
|
* Disable caching for bitcoin core routes
|
||||||
*
|
*
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @param next
|
* @param next
|
||||||
*/
|
*/
|
||||||
private disableCache(req: Request, res: Response, next: NextFunction): void {
|
private disableCache(req: Request, res: Response, next: NextFunction): void {
|
||||||
res.setHeader('Pragma', 'no-cache');
|
res.setHeader('Pragma', 'no-cache');
|
||||||
@ -39,16 +44,16 @@ class BitcoinBackendRoutes {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Exeption handler to return proper details to the accelerator server
|
* Exeption handler to return proper details to the accelerator server
|
||||||
*
|
*
|
||||||
* @param e
|
* @param e
|
||||||
* @param fnName
|
* @param fnName
|
||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
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', 'message']));
|
res.status(400).send(JSON.stringify(e, ['code']));
|
||||||
} else {
|
} else {
|
||||||
const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`;
|
const err = `unknown exception in ${fnName}`;
|
||||||
logger.err(err, BitcoinBackendRoutes.tag);
|
logger.err(err, BitcoinBackendRoutes.tag);
|
||||||
res.status(500).send(err);
|
res.status(500).send(err);
|
||||||
}
|
}
|
||||||
@ -57,13 +62,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) {
|
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
|
||||||
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
|
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const mempoolEntry = await bitcoinClient.getMempoolEntry(txid);
|
const mempoolEntry = await bitcoinClient.getMempoolEntry(txid);
|
||||||
if (!mempoolEntry) {
|
if (!mempoolEntry) {
|
||||||
res.status(404).send(`no mempool entry found for txid ${txid}`);
|
res.status(404).send();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(mempoolEntry);
|
res.status(200).send(mempoolEntry);
|
||||||
@ -75,13 +80,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') {
|
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) {
|
||||||
res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
|
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`);
|
||||||
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 ${rawTx}`);
|
res.status(400).send(`unable to decode rawTx`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(decodedTx);
|
res.status(200).send(decodedTx);
|
||||||
@ -94,23 +99,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) {
|
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
|
||||||
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
|
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof(verbose) !== 'string') {
|
if (typeof(verbose) !== 'string') {
|
||||||
res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`);
|
res.status(400).send(`invalid param 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 ${verbose}. must be a valid integer`);
|
res.status(400).send(`invalid param 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 for txid ${txid}`);
|
res.status(400).send(`unable to get raw transaction`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(decodedTx);
|
res.status(200).send(decodedTx);
|
||||||
@ -122,13 +127,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') {
|
if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) {
|
||||||
res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`);
|
res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`);
|
||||||
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 ${rawTx}`);
|
res.status(400).send(`unable to send rawTx`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(txHex);
|
res.status(200).send(txHex);
|
||||||
@ -140,13 +145,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') {
|
if (typeof(rawTxs) !== 'object' || !Array.isArray(rawTxs) || rawTxs.some((tx) => typeof(tx) !== 'string' || !RAW_TX_REGEX.test(tx))) {
|
||||||
res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`);
|
res.status(400).send(`invalid param rawTxs. must be an array of strings of hexadecimal characters`);
|
||||||
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 ${JSON.stringify(rawTxs)}, got an empty result`);
|
res.status(400).send(`testmempoolaccept failed for raw txs, got an empty result`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(txHex);
|
res.status(200).send(txHex);
|
||||||
@ -159,18 +164,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) {
|
if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
|
||||||
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`);
|
res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
|
||||||
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 ${verbose}. must be a string ('true' | 'false')`);
|
res.status(400).send(`invalid param 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 for txid ${txid}`);
|
res.status(400).send(`unable to get mempool ancestors`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(ancestors);
|
res.status(200).send(ancestors);
|
||||||
@ -183,23 +188,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) {
|
if (typeof(blockHash) !== 'string' || blockHash.length !== 64 || !BLOCKHASH_REGEX.test(blockHash)) {
|
||||||
res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`);
|
res.status(400).send(`invalid param blockHash. must be 64 hexadecimal characters`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof(verbosity) !== 'string') {
|
if (typeof(verbosity) !== 'string') {
|
||||||
res.status(400).send(`invalid param verbosity ${verbosity}. must be a string representing an integer`);
|
res.status(400).send(`invalid param 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 ${verbosity}. must be a valid integer`);
|
res.status(400).send(`invalid param 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 for block hash ${blockHash}`);
|
res.status(400).send(`unable to get block`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(block);
|
res.status(200).send(block);
|
||||||
@ -212,18 +217,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 ${blockHeight}, must be a string representing an integer`);
|
res.status(400).send(`invalid param 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 ${blockHeight}. must be a valid integer`);
|
res.status(400).send(`invalid param 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 for block height ${blockHeightNumber}`);
|
res.status(400).send(`unable to get block hash`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(block);
|
res.status(200).send(block);
|
||||||
@ -246,4 +251,4 @@ class BitcoinBackendRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new BitcoinBackendRoutes
|
export default new BitcoinBackendRoutes;
|
||||||
@ -22,6 +22,11 @@ import rbfCache from '../rbf-cache';
|
|||||||
import { calculateMempoolTxCpfp } from '../cpfp';
|
import { calculateMempoolTxCpfp } from '../cpfp';
|
||||||
import { handleError } from '../../utils/api';
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
|
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) {
|
||||||
app
|
app
|
||||||
@ -42,6 +47,7 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
||||||
@ -89,7 +95,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get init data');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +114,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get mempool blocks');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +126,10 @@ 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') {
|
||||||
txIds.push(req.query.txId[_txId].toString());
|
const txid = req.query.txId[_txId].toString();
|
||||||
|
if (TXID_REGEX.test(txid)) {
|
||||||
|
txIds.push(txid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,18 +148,22 @@ 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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get batched outspends');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $getCpfpInfo(req: Request, res: Response) {
|
private async $getCpfpInfo(req: Request, res: Response) {
|
||||||
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
if (!TXID_REGEX.test(req.params.txId)) {
|
||||||
handleError(req, res, 501, `Invalid transaction ID.`);
|
handleError(req, res, 501, `Invalid transaction ID`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +196,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,6 +217,10 @@ 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);
|
||||||
@ -211,12 +228,18 @@ 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, e instanceof Error ? e.message : e);
|
handleError(req, res, statusCode, 'Failed to get transaction');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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');
|
||||||
@ -225,8 +248,10 @@ 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, e instanceof Error ? e.message : e);
|
handleError(req, res, statusCode, 'Failed to get raw transaction');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,14 +316,18 @@ 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, e.message);
|
handleError(req, res, 404, notFoundError);
|
||||||
} else {
|
} else {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to process PSBT');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
@ -306,22 +335,54 @@ 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, e instanceof Error ? e.message : e);
|
handleError(req, res, statusCode, 'Failed to get transaction status');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get block summary');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
|
||||||
|
if (!transaction) {
|
||||||
|
handleError(req, res, 404, `Transaction not found in summary`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
|
res.json(transaction);
|
||||||
|
} catch (e) {
|
||||||
|
handleError(req, res, 500, 'Failed to get transaction from summary');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@ -333,53 +394,69 @@ 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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get block');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get block header');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get block audit summary');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get transaction audit summary');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,7 +470,7 @@ class BitcoinRoutes {
|
|||||||
return await this.getLegacyBlocks(req, res);
|
return await this.getLegacyBlocks(req, res);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get blocks');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,7 +512,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get blocks');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,11 +547,15 @@ 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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get blocks');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@ -495,7 +576,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get block transactions');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -504,7 +585,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get block at height');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,16 +594,20 @@ 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 instanceof Error ? e.message : e);
|
handleError(req, res, 413, e.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get address');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -531,6 +616,10 @@ 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 = '';
|
||||||
@ -541,10 +630,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 instanceof Error ? e.message : e);
|
handleError(req, res, 413, e.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get address transactions');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,6 +649,10 @@ 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
|
||||||
@ -568,10 +661,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 instanceof Error ? e.message : e);
|
handleError(req, res, 413, e.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get script hash');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -580,6 +673,10 @@ 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
|
||||||
@ -592,10 +689,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 instanceof Error ? e.message : e);
|
handleError(req, res, 413, e.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get script hash transactions');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -608,10 +705,10 @@ class BitcoinRoutes {
|
|||||||
|
|
||||||
private async getAddressPrefix(req: Request, res: Response) {
|
private async getAddressPrefix(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
const addressPrefix = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||||
res.send(blockHash);
|
res.send(addressPrefix);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get address prefix');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -652,7 +749,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get height at tip');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -662,39 +759,55 @@ 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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get hash at tip');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get raw block');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get txids for block');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to validate address');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@ -703,7 +816,7 @@ class BitcoinRoutes {
|
|||||||
replaces
|
replaces
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get rbf history');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -712,7 +825,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get rbf trees');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -721,11 +834,15 @@ 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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get full rbf replacements');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@ -734,16 +851,20 @@ class BitcoinRoutes {
|
|||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get cached tx');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get transaction outspends');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -756,7 +877,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get difficulty change');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -767,8 +888,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, message: e.message })
|
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
|
||||||
: (e.message || 'Error'));
|
: 'Failed to send raw transaction');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -779,8 +900,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, message: e.message })
|
handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
|
||||||
: (e.message || 'Error'));
|
: 'Failed to send raw transaction');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -791,8 +912,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, message: e.message })
|
handleError(req, res, 400, (e.message && e.code) ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code })
|
||||||
: (e.message || 'Error'));
|
: 'Failed to test transactions');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -804,8 +925,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, message: e.message })
|
handleError(req, res, 400, (e.message && e.code) ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code })
|
||||||
: (e.message || 'Error'));
|
: 'Failed to submit package');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import axios, { AxiosResponse, isAxiosError } from 'axios';
|
import axios, { 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,6 +20,13 @@ 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 {
|
||||||
@ -29,14 +36,21 @@ class FailoverRouter {
|
|||||||
maxHeight: number = 0;
|
maxHeight: number = 0;
|
||||||
hosts: FailoverHost[];
|
hosts: FailoverHost[];
|
||||||
multihost: boolean;
|
multihost: boolean;
|
||||||
pollInterval: number = 60000;
|
gitHashInterval: number = 600000; // 10 minutes
|
||||||
|
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 {
|
||||||
@ -45,6 +59,10 @@ class FailoverRouter {
|
|||||||
rtts: [],
|
rtts: [],
|
||||||
rtt: Infinity,
|
rtt: Infinity,
|
||||||
failures: 0,
|
failures: 0,
|
||||||
|
publicDomain: 'https://' + this.extractPublicDomain(domain),
|
||||||
|
hashes: {
|
||||||
|
lastUpdated: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
this.activeHost = {
|
this.activeHost = {
|
||||||
@ -55,6 +73,10 @@ 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);
|
||||||
@ -106,6 +128,24 @@ 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;
|
||||||
@ -202,6 +242,47 @@ 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;
|
||||||
@ -381,6 +462,7 @@ 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 [];
|
||||||
|
|||||||
@ -412,8 +412,16 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||||
|
const currentBlockHeight = blockchainInfo.blocks;
|
||||||
|
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight);
|
||||||
|
if (indexingBlockAmount <= -1) {
|
||||||
|
indexingBlockAmount = currentBlockHeight + 1;
|
||||||
|
}
|
||||||
|
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
|
||||||
|
|
||||||
// Get all indexed block hash
|
// Get all indexed block hash
|
||||||
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
|
const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex);
|
||||||
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
|
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
|
||||||
|
|
||||||
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
|
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
|
||||||
@ -1216,6 +1224,11 @@ class Blocks {
|
|||||||
return summary.transactions;
|
return summary.transactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
|
||||||
|
const txs = await this.$getStrippedBlockTransactions(hash);
|
||||||
|
return txs.find(tx => tx.txid === txid) || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get 15 blocks
|
* Get 15 blocks
|
||||||
*
|
*
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 83;
|
private static currentVersion = 94;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -710,6 +710,414 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
|
||||||
await this.updateToSchemaVersion(83);
|
await this.updateToSchemaVersion(83);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add new pools indexes
|
||||||
|
if (databaseSchemaVersion < 84 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery(`
|
||||||
|
ALTER TABLE \`pools\`
|
||||||
|
ADD INDEX \`slug\` (\`slug\`),
|
||||||
|
ADD INDEX \`unique_id\` (\`unique_id\`)
|
||||||
|
`);
|
||||||
|
await this.updateToSchemaVersion(84);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lightning channels indexes
|
||||||
|
if (databaseSchemaVersion < 85 && isBitcoin === true) {
|
||||||
|
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\`)
|
||||||
|
`);
|
||||||
|
await this.updateToSchemaVersion(85);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lightning nodes indexes
|
||||||
|
if (databaseSchemaVersion < 86 && isBitcoin === true) {
|
||||||
|
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\`)
|
||||||
|
`);
|
||||||
|
await this.updateToSchemaVersion(86);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lightning node sockets indexes
|
||||||
|
if (databaseSchemaVersion < 87 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
|
||||||
|
await this.updateToSchemaVersion(87);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lightning stats indexes
|
||||||
|
if (databaseSchemaVersion < 88 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
|
||||||
|
await this.updateToSchemaVersion(88);
|
||||||
|
}
|
||||||
|
|
||||||
|
// geo names indexes
|
||||||
|
if (databaseSchemaVersion < 89 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
|
||||||
|
await this.updateToSchemaVersion(89);
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashrates indexes
|
||||||
|
if (databaseSchemaVersion < 90 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
|
||||||
|
await this.updateToSchemaVersion(90);
|
||||||
|
}
|
||||||
|
|
||||||
|
// block audits indexes
|
||||||
|
if (databaseSchemaVersion < 91 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
|
||||||
|
await this.updateToSchemaVersion(91);
|
||||||
|
}
|
||||||
|
|
||||||
|
// elements_pegs indexes
|
||||||
|
if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') {
|
||||||
|
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\`)
|
||||||
|
`);
|
||||||
|
await this.updateToSchemaVersion(92);
|
||||||
|
}
|
||||||
|
|
||||||
|
// federation_txos indexes
|
||||||
|
if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') {
|
||||||
|
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\`)
|
||||||
|
`);
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -3,6 +3,8 @@ 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() { }
|
||||||
|
|
||||||
@ -23,7 +25,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to search channels by id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +41,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get channel');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +72,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get channels for node');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +85,10 @@ 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') {
|
||||||
txIds.push(req.query.txId[_txId].toString());
|
const txid = 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);
|
||||||
@ -108,7 +113,7 @@ class ChannelsRoutes {
|
|||||||
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get channels by transaction ids');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +125,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get penalty closed channels');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +138,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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get channel geodata');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class GeneralLightningRoutes {
|
|||||||
channels: channels,
|
channels: channels,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to search for nodes and channels');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get lightning statistics');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get lightning statistics');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to search for node');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get node group');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get node');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get historical node stats');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get fee histogram');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,7 +248,7 @@ class NodesRoutes {
|
|||||||
topByChannels: topChannelsNodes,
|
topByChannels: topChannelsNodes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get nodes ranking');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get top nodes by capacity');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get top nodes by channels');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get oldest nodes');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get ISP ranking');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get world nodes');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,7 +336,7 @@ class NodesRoutes {
|
|||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get nodes per country');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,7 +363,7 @@ class NodesRoutes {
|
|||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get nodes per ISP');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get nodes per country');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get pegs by month');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get reserves by month');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get pegs');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get reserves');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get federation audit status');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get federation addresses');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get federation addresses');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get federation utxos');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get expired utxos');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get federation utxos number');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get emergency spent utxos');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get emergency spent utxos stats');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get pegs list');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get pegs volume daily');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get pegs count');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -382,7 +382,7 @@ class MempoolBlocks {
|
|||||||
|
|
||||||
const ancestors: Ancestor[] = [];
|
const ancestors: Ancestor[] = [];
|
||||||
const descendants: Ancestor[] = [];
|
const descendants: Ancestor[] = [];
|
||||||
let ancestor: MempoolTransactionExtended
|
let ancestor: MempoolTransactionExtended;
|
||||||
for (const cluster of clusters) {
|
for (const cluster of clusters) {
|
||||||
for (const memberTxid of cluster) {
|
for (const memberTxid of cluster) {
|
||||||
const mempoolTx = mempool[memberTxid];
|
const mempoolTx = mempool[memberTxid];
|
||||||
@ -462,7 +462,7 @@ class MempoolBlocks {
|
|||||||
|
|
||||||
for (let i = 0; i < block.length; i++) {
|
for (let i = 0; i < block.length; i++) {
|
||||||
const txid = block[i];
|
const txid = block[i];
|
||||||
if (txid) {
|
if (txid in mempool) {
|
||||||
mempoolTx = mempool[txid];
|
mempoolTx = mempool[txid];
|
||||||
// save position in projected blocks
|
// save position in projected blocks
|
||||||
mempoolTx.position = {
|
mempoolTx.position = {
|
||||||
@ -481,6 +481,9 @@ class MempoolBlocks {
|
|||||||
mempoolTx.acceleratedAt = acceleration?.added;
|
mempoolTx.acceleratedAt = acceleration?.added;
|
||||||
mempoolTx.feeDelta = acceleration?.feeDelta;
|
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||||
for (const ancestor of mempoolTx.ancestors || []) {
|
for (const ancestor of mempoolTx.ancestors || []) {
|
||||||
|
if (!(ancestor.txid in mempool)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!mempool[ancestor.txid].acceleration) {
|
if (!mempool[ancestor.txid].acceleration) {
|
||||||
mempool[ancestor.txid].cpfpDirty = true;
|
mempool[ancestor.txid].cpfpDirty = true;
|
||||||
}
|
}
|
||||||
@ -688,7 +691,7 @@ class MempoolBlocks {
|
|||||||
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
|
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
|
||||||
} = {};
|
} = {};
|
||||||
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
|
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
|
||||||
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => {
|
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).filter(acc => acc.txid in mempoolCache).map(acc => {
|
||||||
let vsize = mempoolCache[acc.txid].vsize;
|
let vsize = mempoolCache[acc.txid].vsize;
|
||||||
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
|
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
|
||||||
vsize += (ancestor.weight / 4);
|
vsize += (ancestor.weight / 4);
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client';
|
|||||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||||
import rbfCache from './rbf-cache';
|
import rbfCache from './rbf-cache';
|
||||||
import { Acceleration } from './services/acceleration';
|
import { Acceleration } from './services/acceleration';
|
||||||
|
import accelerationApi from './services/acceleration';
|
||||||
import redisCache from './redis-cache';
|
import redisCache from './redis-cache';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
|
|
||||||
@ -207,7 +208,7 @@ class Mempool {
|
|||||||
return txTimes;
|
return txTimes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
|
public async $updateMempool(transactions: string[], accelerations: Record<string, Acceleration> | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
|
||||||
logger.debug(`Updating mempool...`);
|
logger.debug(`Updating mempool...`);
|
||||||
|
|
||||||
// warn if this run stalls the main loop for more than 2 minutes
|
// warn if this run stalls the main loop for more than 2 minutes
|
||||||
@ -354,7 +355,7 @@ class Mempool {
|
|||||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||||
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
||||||
|
|
||||||
const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : [];
|
const accelerationDelta = accelerations != null ? await this.updateAccelerations(accelerations) : [];
|
||||||
if (accelerationDelta.length) {
|
if (accelerationDelta.length) {
|
||||||
hasChange = true;
|
hasChange = true;
|
||||||
}
|
}
|
||||||
@ -399,58 +400,11 @@ class Mempool {
|
|||||||
return this.accelerations;
|
return this.accelerations;
|
||||||
}
|
}
|
||||||
|
|
||||||
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
|
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] {
|
||||||
try {
|
try {
|
||||||
const changed: string[] = [];
|
const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap);
|
||||||
|
|
||||||
const newAccelerationMap: { [txid: string]: Acceleration } = {};
|
|
||||||
for (const acceleration of newAccelerations) {
|
|
||||||
// skip transactions we don't know about
|
|
||||||
if (!this.mempoolCache[acceleration.txid]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
newAccelerationMap[acceleration.txid] = acceleration;
|
|
||||||
if (this.accelerations[acceleration.txid] == null) {
|
|
||||||
// new acceleration
|
|
||||||
changed.push(acceleration.txid);
|
|
||||||
} else {
|
|
||||||
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
|
|
||||||
// feeDelta changed
|
|
||||||
changed.push(acceleration.txid);
|
|
||||||
} else if (this.accelerations[acceleration.txid].pools?.length) {
|
|
||||||
let poolsChanged = false;
|
|
||||||
const pools = new Set();
|
|
||||||
this.accelerations[acceleration.txid].pools.forEach(pool => {
|
|
||||||
pools.add(pool);
|
|
||||||
});
|
|
||||||
acceleration.pools.forEach(pool => {
|
|
||||||
if (!pools.has(pool)) {
|
|
||||||
poolsChanged = true;
|
|
||||||
} else {
|
|
||||||
pools.delete(pool);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (pools.size > 0) {
|
|
||||||
poolsChanged = true;
|
|
||||||
}
|
|
||||||
if (poolsChanged) {
|
|
||||||
// pools changed
|
|
||||||
changed.push(acceleration.txid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const oldTxid of Object.keys(this.accelerations)) {
|
|
||||||
if (!newAccelerationMap[oldTxid]) {
|
|
||||||
// removed
|
|
||||||
changed.push(oldTxid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.accelerations = newAccelerationMap;
|
this.accelerations = newAccelerationMap;
|
||||||
|
return accelerationDelta;
|
||||||
return changed;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
|
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@ -72,7 +72,7 @@ class MiningRoutes {
|
|||||||
}
|
}
|
||||||
res.status(200).send(response);
|
res.status(200).send(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get historical prices');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get pool');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get blocks for pool');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,7 +130,7 @@ class MiningRoutes {
|
|||||||
res.json(pools);
|
res.json(pools);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get pools');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get pools');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get pools historical hashrate');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get pool historical hashrate');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,7 +204,7 @@ class MiningRoutes {
|
|||||||
currentDifficulty: currentDifficulty,
|
currentDifficulty: currentDifficulty,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get historical hashrate');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get historical block fees');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get historical block fees');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get historical block rewards');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get historical block fee rates');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,7 +282,7 @@ class MiningRoutes {
|
|||||||
weights: blockWeights
|
weights: blockWeights
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get historical block size and weight');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get historical difficulty adjustments');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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) {
|
||||||
res.status(500).end();
|
handleError(req, res, 500, 'Failed to get reward stats');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get historical blocks health');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get block audit');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get height from timestamp');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get block audit scores');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get block audit score');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get accelerations by pool');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get accelerations by height');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get recent accelerations');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get acceleration totals');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -459,9 +459,9 @@ class MiningRoutes {
|
|||||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(accelerationApi.accelerations || []);
|
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get active accelerations');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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, e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to request acceleration');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -136,9 +136,13 @@ class Mining {
|
|||||||
poolsStatistics['blockCount'] = blockCount;
|
poolsStatistics['blockCount'] = blockCount;
|
||||||
|
|
||||||
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
|
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
|
||||||
|
const totalBlock3d: number = await BlocksRepository.$blockCount(null, '3d');
|
||||||
|
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
|
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
|
||||||
|
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
|
||||||
|
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
poolsStatistics['lastEstimatedHashrate'] = 0;
|
poolsStatistics['lastEstimatedHashrate'] = 0;
|
||||||
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
|
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
|
||||||
|
|||||||
@ -1,10 +1,15 @@
|
|||||||
import { Application, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import pricesUpdater from '../../tasks/price-updater';
|
import pricesUpdater from '../../tasks/price-updater';
|
||||||
|
import logger from '../../logger';
|
||||||
|
import PricesRepository from '../../repositories/PricesRepository';
|
||||||
|
|
||||||
class PricesRoutes {
|
class PricesRoutes {
|
||||||
public initRoutes(app: Application): void {
|
public initRoutes(app: Application): void {
|
||||||
app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this));
|
app
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this))
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/usd-price-history', this.$getAllPrices.bind(this))
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
private $getCurrentPrices(req: Request, res: Response): void {
|
private $getCurrentPrices(req: Request, res: Response): void {
|
||||||
@ -14,6 +19,23 @@ class PricesRoutes {
|
|||||||
|
|
||||||
res.json(pricesUpdater.getLatestPrices());
|
res.json(pricesUpdater.getLatestPrices());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $getAllPrices(req: Request, res: Response): Promise<void> {
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR).toUTCString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usdPriceHistory = await PricesRepository.$getPricesTimesAndId();
|
||||||
|
const responseData = usdPriceHistory.map(p => {
|
||||||
|
return { time: p.time, USD: p.USD };
|
||||||
|
});
|
||||||
|
res.status(200).json(responseData);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Exception ${e} in PricesRoutes::$getAllPrices. Code: ${e.code}. Message: ${e.message}`);
|
||||||
|
res.status(403).send();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new PricesRoutes();
|
export default new PricesRoutes();
|
||||||
|
|||||||
@ -119,7 +119,11 @@ class RbfCache {
|
|||||||
|
|
||||||
|
|
||||||
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
if ( !newTxExtended
|
||||||
|
|| !replaced?.length
|
||||||
|
|| this.txs.has(newTxExtended.txid)
|
||||||
|
|| !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
|
import { WebSocket } from 'ws';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import { BlockExtended } from '../../mempool.interfaces';
|
import { BlockExtended } from '../../mempool.interfaces';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import mempool from '../mempool';
|
||||||
|
import websocketHandler from '../websocket-handler';
|
||||||
|
|
||||||
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
|
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
|
||||||
|
|
||||||
@ -37,14 +40,23 @@ export interface AccelerationHistory {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class AccelerationApi {
|
class AccelerationApi {
|
||||||
|
private ws: WebSocket | null = null;
|
||||||
|
private useWebsocket: boolean = config.MEMPOOL.OFFICIAL && config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||||
|
private startedWebsocketLoop: boolean = false;
|
||||||
|
private websocketConnected: boolean = false;
|
||||||
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
|
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||||
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
|
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
|
||||||
private _accelerations: Acceleration[] | null = null;
|
private websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/';
|
||||||
|
private _accelerations: Record<string, Acceleration> = {};
|
||||||
private lastPoll = 0;
|
private lastPoll = 0;
|
||||||
|
private lastPing = Date.now();
|
||||||
|
private lastPong = Date.now();
|
||||||
private forcePoll = false;
|
private forcePoll = false;
|
||||||
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
|
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
|
||||||
|
|
||||||
public get accelerations(): Acceleration[] | null {
|
public constructor() {}
|
||||||
|
|
||||||
|
public getAccelerations(): Record<string, Acceleration> {
|
||||||
return this._accelerations;
|
return this._accelerations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,11 +84,18 @@ class AccelerationApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateAccelerations(): Promise<Acceleration[] | null> {
|
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> {
|
||||||
|
if (this.useWebsocket && this.websocketConnected) {
|
||||||
|
return this._accelerations;
|
||||||
|
}
|
||||||
if (!this.onDemandPollingEnabled) {
|
if (!this.onDemandPollingEnabled) {
|
||||||
const accelerations = await this.$fetchAccelerations();
|
const accelerations = await this.$fetchAccelerations();
|
||||||
if (accelerations) {
|
if (accelerations) {
|
||||||
this._accelerations = accelerations;
|
const latestAccelerations = {};
|
||||||
|
for (const acc of accelerations) {
|
||||||
|
latestAccelerations[acc.txid] = acc;
|
||||||
|
}
|
||||||
|
this._accelerations = latestAccelerations;
|
||||||
return this._accelerations;
|
return this._accelerations;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -85,7 +104,7 @@ class AccelerationApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
|
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> {
|
||||||
const shouldUpdate = this.forcePoll
|
const shouldUpdate = this.forcePoll
|
||||||
|| this.countMyAccelerationsWithStatus('requested') > 0
|
|| this.countMyAccelerationsWithStatus('requested') > 0
|
||||||
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
|
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
|
||||||
@ -120,7 +139,11 @@ class AccelerationApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
|
const latestAccelerations = {};
|
||||||
|
for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) {
|
||||||
|
latestAccelerations[acc.txid] = acc;
|
||||||
|
}
|
||||||
|
this._accelerations = latestAccelerations;
|
||||||
return this._accelerations;
|
return this._accelerations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,6 +175,148 @@ class AccelerationApi {
|
|||||||
}
|
}
|
||||||
return anyAccelerated;
|
return anyAccelerated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get a list of accelerations that have changed between two sets of accelerations
|
||||||
|
public getAccelerationDelta(oldAccelerationMap: Record<string, Acceleration>, newAccelerationMap: Record<string, Acceleration>): string[] {
|
||||||
|
const changed: string[] = [];
|
||||||
|
const mempoolCache = mempool.getMempool();
|
||||||
|
|
||||||
|
for (const acceleration of Object.values(newAccelerationMap)) {
|
||||||
|
// skip transactions we don't know about
|
||||||
|
if (!mempoolCache[acceleration.txid]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (oldAccelerationMap[acceleration.txid] == null) {
|
||||||
|
// new acceleration
|
||||||
|
changed.push(acceleration.txid);
|
||||||
|
} else {
|
||||||
|
if (oldAccelerationMap[acceleration.txid].feeDelta !== acceleration.feeDelta) {
|
||||||
|
// feeDelta changed
|
||||||
|
changed.push(acceleration.txid);
|
||||||
|
} else if (oldAccelerationMap[acceleration.txid].pools?.length) {
|
||||||
|
let poolsChanged = false;
|
||||||
|
const pools = new Set();
|
||||||
|
oldAccelerationMap[acceleration.txid].pools.forEach(pool => {
|
||||||
|
pools.add(pool);
|
||||||
|
});
|
||||||
|
acceleration.pools.forEach(pool => {
|
||||||
|
if (!pools.has(pool)) {
|
||||||
|
poolsChanged = true;
|
||||||
|
} else {
|
||||||
|
pools.delete(pool);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (pools.size > 0) {
|
||||||
|
poolsChanged = true;
|
||||||
|
}
|
||||||
|
if (poolsChanged) {
|
||||||
|
// pools changed
|
||||||
|
changed.push(acceleration.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const oldTxid of Object.keys(oldAccelerationMap)) {
|
||||||
|
if (!newAccelerationMap[oldTxid]) {
|
||||||
|
// removed
|
||||||
|
changed.push(oldTxid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleWebsocketMessage(msg: any): void {
|
||||||
|
if (msg?.accelerations !== null) {
|
||||||
|
const latestAccelerations = {};
|
||||||
|
for (const acc of msg?.accelerations || []) {
|
||||||
|
latestAccelerations[acc.txid] = acc;
|
||||||
|
}
|
||||||
|
this._accelerations = latestAccelerations;
|
||||||
|
websocketHandler.handleAccelerationsChanged(this._accelerations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connectWebsocket(): Promise<void> {
|
||||||
|
if (this.startedWebsocketLoop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (this.useWebsocket) {
|
||||||
|
this.startedWebsocketLoop = true;
|
||||||
|
if (!this.ws) {
|
||||||
|
this.ws = new WebSocket(this.websocketPath);
|
||||||
|
this.lastPing = 0;
|
||||||
|
|
||||||
|
this.ws.on('open', () => {
|
||||||
|
logger.info(`Acceleration websocket opened to ${this.websocketPath}`);
|
||||||
|
this.websocketConnected = true;
|
||||||
|
this.ws?.send(JSON.stringify({
|
||||||
|
'watch-accelerations': true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('error', (error) => {
|
||||||
|
let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`;
|
||||||
|
if (error['errors']) {
|
||||||
|
errMsg += ' - ' + error['errors'].join(' - ');
|
||||||
|
}
|
||||||
|
logger.err(errMsg);
|
||||||
|
this.ws = null;
|
||||||
|
this.websocketConnected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('close', () => {
|
||||||
|
logger.info('Acceleration websocket closed');
|
||||||
|
this.ws = null;
|
||||||
|
this.websocketConnected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('message', (data, isBinary) => {
|
||||||
|
try {
|
||||||
|
const msg = (isBinary ? data : data.toString()) as string;
|
||||||
|
const parsedMsg = msg?.length ? JSON.parse(msg) : null;
|
||||||
|
this.handleWebsocketMessage(parsedMsg);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('ping', () => {
|
||||||
|
logger.debug('received ping from acceleration websocket server');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('pong', () => {
|
||||||
|
logger.debug('received pong from acceleration websocket server');
|
||||||
|
this.lastPong = Date.now();
|
||||||
|
});
|
||||||
|
} else if (this.websocketConnected) {
|
||||||
|
if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) {
|
||||||
|
logger.warn('No pong received within 10 seconds, terminating connection');
|
||||||
|
try {
|
||||||
|
this.ws?.terminate();
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e));
|
||||||
|
} finally {
|
||||||
|
this.ws = null;
|
||||||
|
this.websocketConnected = false;
|
||||||
|
this.lastPing = 0;
|
||||||
|
}
|
||||||
|
} else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) {
|
||||||
|
logger.debug('sending ping to acceleration websocket server');
|
||||||
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
this.ws?.ping();
|
||||||
|
this.lastPing = Date.now();
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('failed to send ping to acceleration websocket server: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AccelerationApi();
|
export default new AccelerationApi();
|
||||||
@ -1,6 +1,7 @@
|
|||||||
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 {
|
||||||
@ -18,7 +19,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get wallet');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
105
backend/src/api/services/stratum.ts
Normal file
105
backend/src/api/services/stratum.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { WebSocket } from 'ws';
|
||||||
|
import logger from '../../logger';
|
||||||
|
import config from '../../config';
|
||||||
|
import websocketHandler from '../websocket-handler';
|
||||||
|
|
||||||
|
export interface StratumJob {
|
||||||
|
pool: number;
|
||||||
|
height: number;
|
||||||
|
coinbase: string;
|
||||||
|
scriptsig: string;
|
||||||
|
reward: number;
|
||||||
|
jobId: string;
|
||||||
|
extraNonce: string;
|
||||||
|
extraNonce2Size: number;
|
||||||
|
prevHash: string;
|
||||||
|
coinbase1: string;
|
||||||
|
coinbase2: string;
|
||||||
|
merkleBranches: string[];
|
||||||
|
version: string;
|
||||||
|
bits: string;
|
||||||
|
time: string;
|
||||||
|
timestamp: number;
|
||||||
|
cleanJobs: boolean;
|
||||||
|
received: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStratumJob(obj: any): obj is StratumJob {
|
||||||
|
return obj
|
||||||
|
&& typeof obj === 'object'
|
||||||
|
&& 'pool' in obj
|
||||||
|
&& 'prevHash' in obj
|
||||||
|
&& 'height' in obj
|
||||||
|
&& 'received' in obj
|
||||||
|
&& 'version' in obj
|
||||||
|
&& 'timestamp' in obj
|
||||||
|
&& 'bits' in obj
|
||||||
|
&& 'merkleBranches' in obj
|
||||||
|
&& 'cleanJobs' in obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StratumApi {
|
||||||
|
private ws: WebSocket | null = null;
|
||||||
|
private runWebsocketLoop: boolean = false;
|
||||||
|
private startedWebsocketLoop: boolean = false;
|
||||||
|
private websocketConnected: boolean = false;
|
||||||
|
private jobs: Record<string, StratumJob> = {};
|
||||||
|
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
public getJobs(): Record<string, StratumJob> {
|
||||||
|
return this.jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleWebsocketMessage(msg: any): void {
|
||||||
|
if (isStratumJob(msg)) {
|
||||||
|
this.jobs[msg.pool] = msg;
|
||||||
|
websocketHandler.handleNewStratumJob(this.jobs[msg.pool]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connectWebsocket(): Promise<void> {
|
||||||
|
if (!config.STRATUM.ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.runWebsocketLoop = true;
|
||||||
|
if (this.startedWebsocketLoop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (this.runWebsocketLoop) {
|
||||||
|
this.startedWebsocketLoop = true;
|
||||||
|
if (!this.ws) {
|
||||||
|
this.ws = new WebSocket(`${config.STRATUM.API}`);
|
||||||
|
this.websocketConnected = true;
|
||||||
|
|
||||||
|
this.ws.on('open', () => {
|
||||||
|
logger.info('Stratum websocket opened');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('error', (error) => {
|
||||||
|
logger.err('Stratum websocket error: ' + error);
|
||||||
|
this.ws = null;
|
||||||
|
this.websocketConnected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('close', () => {
|
||||||
|
logger.info('Stratum websocket closed');
|
||||||
|
this.ws = null;
|
||||||
|
this.websocketConnected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('message', (data, isBinary) => {
|
||||||
|
try {
|
||||||
|
const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string);
|
||||||
|
this.handleWebsocketMessage(parsedMsg);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Failed to parse stratum websocket message: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new StratumApi();
|
||||||
@ -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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, 'Failed to get statistics');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository
|
|||||||
import Audit from './audit';
|
import Audit from './audit';
|
||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
import { ApiPrice } from '../repositories/PricesRepository';
|
import { ApiPrice } from '../repositories/PricesRepository';
|
||||||
|
import { Acceleration } from './services/acceleration';
|
||||||
import accelerationApi from './services/acceleration';
|
import accelerationApi from './services/acceleration';
|
||||||
import mempool from './mempool';
|
import mempool from './mempool';
|
||||||
import statistics from './statistics/statistics';
|
import statistics from './statistics/statistics';
|
||||||
@ -37,6 +38,7 @@ interface AddressTransactions {
|
|||||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||||
import { calculateMempoolTxCpfp } from './cpfp';
|
import { calculateMempoolTxCpfp } from './cpfp';
|
||||||
import { getRecentFirstSeen } from '../utils/file-read';
|
import { getRecentFirstSeen } from '../utils/file-read';
|
||||||
|
import stratumApi, { StratumJob } from './services/stratum';
|
||||||
|
|
||||||
// valid 'want' subscriptions
|
// valid 'want' subscriptions
|
||||||
const wantable = [
|
const wantable = [
|
||||||
@ -60,6 +62,8 @@ class WebsocketHandler {
|
|||||||
private lastRbfSummary: ReplacementInfo[] | null = null;
|
private lastRbfSummary: ReplacementInfo[] | null = null;
|
||||||
private mempoolSequence: number = 0;
|
private mempoolSequence: number = 0;
|
||||||
|
|
||||||
|
private accelerations: Record<string, Acceleration> = {};
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
addWebsocketServer(wss: WebSocket.Server) {
|
addWebsocketServer(wss: WebSocket.Server) {
|
||||||
@ -400,6 +404,16 @@ class WebsocketHandler {
|
|||||||
delete client['track-mempool'];
|
delete client['track-mempool'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsedMessage && parsedMessage['track-stratum'] != null) {
|
||||||
|
if (parsedMessage['track-stratum']) {
|
||||||
|
const sub = parsedMessage['track-stratum'];
|
||||||
|
client['track-stratum'] = sub;
|
||||||
|
response['stratumJobs'] = this.socketData['stratumJobs'];
|
||||||
|
} else {
|
||||||
|
client['track-stratum'] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(response).length) {
|
if (Object.keys(response).length) {
|
||||||
client.send(this.serializeResponse(response));
|
client.send(this.serializeResponse(response));
|
||||||
}
|
}
|
||||||
@ -495,6 +509,42 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleAccelerationsChanged(accelerations: Record<string, Acceleration>): void {
|
||||||
|
if (!this.webSocketServers.length) {
|
||||||
|
throw new Error('No WebSocket.Server has been set');
|
||||||
|
}
|
||||||
|
|
||||||
|
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
|
||||||
|
this.accelerations = accelerations;
|
||||||
|
|
||||||
|
if (!websocketAccelerationDelta.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pre-compute acceleration delta
|
||||||
|
const accelerationUpdate = {
|
||||||
|
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||||
|
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = JSON.stringify({
|
||||||
|
accelerations: accelerationUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach((client) => {
|
||||||
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.send(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug(`Error sending acceleration update to websocket clients: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleReorg(): void {
|
handleReorg(): void {
|
||||||
if (!this.webSocketServers.length) {
|
if (!this.webSocketServers.length) {
|
||||||
throw new Error('No WebSocket.Server have been set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
@ -571,7 +621,7 @@ class WebsocketHandler {
|
|||||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
|
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
|
||||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
const accelerations = memPool.getAccelerations();
|
const accelerations = accelerationApi.getAccelerations();
|
||||||
memPool.handleRbfTransactions(rbfTransactions);
|
memPool.handleRbfTransactions(rbfTransactions);
|
||||||
const rbfChanges = rbfCache.getRbfChanges();
|
const rbfChanges = rbfCache.getRbfChanges();
|
||||||
let rbfReplacements;
|
let rbfReplacements;
|
||||||
@ -679,10 +729,13 @@ class WebsocketHandler {
|
|||||||
const addressCache = this.makeAddressCache(newTransactions);
|
const addressCache = this.makeAddressCache(newTransactions);
|
||||||
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
||||||
|
|
||||||
|
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
|
||||||
|
this.accelerations = accelerations;
|
||||||
|
|
||||||
// pre-compute acceleration delta
|
// pre-compute acceleration delta
|
||||||
const accelerationUpdate = {
|
const accelerationUpdate = {
|
||||||
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||||
removed: accelerationDelta.filter(txid => !accelerations[txid]),
|
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO - Fix indentation after PR is merged
|
// TODO - Fix indentation after PR is merged
|
||||||
@ -1342,6 +1395,23 @@ class WebsocketHandler {
|
|||||||
await statistics.runStatistics();
|
await statistics.runStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public handleNewStratumJob(job: StratumJob): void {
|
||||||
|
this.updateSocketDataFields({ 'stratumJobs': stratumApi.getJobs() });
|
||||||
|
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach((client) => {
|
||||||
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (client['track-stratum'] && (client['track-stratum'] === 'all' || client['track-stratum'] === job.pool)) {
|
||||||
|
client.send(JSON.stringify({
|
||||||
|
'stratumJob': job
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// takes a dictionary of JSON serialized values
|
// takes a dictionary of JSON serialized values
|
||||||
// and zips it together into a valid JSON object
|
// and zips it together into a valid JSON object
|
||||||
private serializeResponse(response): string {
|
private serializeResponse(response): string {
|
||||||
|
|||||||
@ -165,6 +165,10 @@ interface IConfig {
|
|||||||
WALLETS: {
|
WALLETS: {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
WALLETS: string[];
|
WALLETS: string[];
|
||||||
|
},
|
||||||
|
STRATUM: {
|
||||||
|
ENABLED: boolean;
|
||||||
|
API: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,6 +336,10 @@ const defaults: IConfig = {
|
|||||||
'ENABLED': false,
|
'ENABLED': false,
|
||||||
'WALLETS': [],
|
'WALLETS': [],
|
||||||
},
|
},
|
||||||
|
'STRATUM': {
|
||||||
|
'ENABLED': false,
|
||||||
|
'API': 'http://localhost:1234',
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class Config implements IConfig {
|
class Config implements IConfig {
|
||||||
@ -354,6 +362,7 @@ class Config implements IConfig {
|
|||||||
REDIS: IConfig['REDIS'];
|
REDIS: IConfig['REDIS'];
|
||||||
FIAT_PRICE: IConfig['FIAT_PRICE'];
|
FIAT_PRICE: IConfig['FIAT_PRICE'];
|
||||||
WALLETS: IConfig['WALLETS'];
|
WALLETS: IConfig['WALLETS'];
|
||||||
|
STRATUM: IConfig['STRATUM'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const configs = this.merge(configFromFile, defaults);
|
const configs = this.merge(configFromFile, defaults);
|
||||||
@ -376,6 +385,7 @@ class Config implements IConfig {
|
|||||||
this.REDIS = configs.REDIS;
|
this.REDIS = configs.REDIS;
|
||||||
this.FIAT_PRICE = configs.FIAT_PRICE;
|
this.FIAT_PRICE = configs.FIAT_PRICE;
|
||||||
this.WALLETS = configs.WALLETS;
|
this.WALLETS = configs.WALLETS;
|
||||||
|
this.STRATUM = configs.STRATUM;
|
||||||
}
|
}
|
||||||
|
|
||||||
merge = (...objects: object[]): IConfig => {
|
merge = (...objects: object[]): IConfig => {
|
||||||
|
|||||||
@ -48,6 +48,7 @@ import accelerationRoutes from './api/acceleration/acceleration.routes';
|
|||||||
import aboutRoutes from './api/about.routes';
|
import aboutRoutes from './api/about.routes';
|
||||||
import mempoolBlocks from './api/mempool-blocks';
|
import mempoolBlocks from './api/mempool-blocks';
|
||||||
import walletApi from './api/services/wallets';
|
import walletApi from './api/services/wallets';
|
||||||
|
import stratumApi from './api/services/stratum';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@ -233,11 +234,11 @@ class Server {
|
|||||||
const newMempool = await bitcoinApi.$getRawMempool();
|
const newMempool = await bitcoinApi.$getRawMempool();
|
||||||
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
||||||
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
||||||
const newAccelerations = await accelerationApi.$updateAccelerations();
|
const latestAccelerations = await accelerationApi.$updateAccelerations();
|
||||||
const numHandledBlocks = await blocks.$updateBlocks();
|
const numHandledBlocks = await blocks.$updateBlocks();
|
||||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||||
if (numHandledBlocks === 0) {
|
if (numHandledBlocks === 0) {
|
||||||
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
|
await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate);
|
||||||
}
|
}
|
||||||
indexer.$run();
|
indexer.$run();
|
||||||
if (config.WALLETS.ENABLED) {
|
if (config.WALLETS.ENABLED) {
|
||||||
@ -318,11 +319,18 @@ class Server {
|
|||||||
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||||
}
|
}
|
||||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||||
|
|
||||||
|
accelerationApi.connectWebsocket();
|
||||||
|
if (config.STRATUM.ENABLED) {
|
||||||
|
stratumApi.connectWebsocket();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setUpHttpApiRoutes(): void {
|
setUpHttpApiRoutes(): void {
|
||||||
bitcoinRoutes.initRoutes(this.app);
|
bitcoinRoutes.initRoutes(this.app);
|
||||||
bitcoinCoreRoutes.initRoutes(this.app);
|
if (config.MEMPOOL.OFFICIAL) {
|
||||||
|
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);
|
||||||
|
|||||||
@ -501,7 +501,7 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query += ` ORDER BY height DESC
|
query += ` ORDER BY height DESC
|
||||||
LIMIT 10`;
|
LIMIT 100`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [rows]: any[] = await DB.query(query, params);
|
const [rows]: any[] = await DB.query(query, params);
|
||||||
|
|||||||
@ -148,6 +148,10 @@
|
|||||||
"API": "__MEMPOOL_SERVICES_API__",
|
"API": "__MEMPOOL_SERVICES_API__",
|
||||||
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
|
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
|
||||||
},
|
},
|
||||||
|
"STRATUM": {
|
||||||
|
"ENABLED": __STRATUM_ENABLED__,
|
||||||
|
"API": "__STRATUM_API__"
|
||||||
|
},
|
||||||
"REDIS": {
|
"REDIS": {
|
||||||
"ENABLED": __REDIS_ENABLED__,
|
"ENABLED": __REDIS_ENABLED__,
|
||||||
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
|
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
|
||||||
|
|||||||
@ -149,6 +149,10 @@ __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
|||||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
|
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
|
||||||
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||||
|
|
||||||
|
# STRATUM
|
||||||
|
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
|
||||||
|
__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
|
||||||
|
|
||||||
# REDIS
|
# REDIS
|
||||||
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
|
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
|
||||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
|
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
|
||||||
@ -300,6 +304,10 @@ sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.j
|
|||||||
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
|
||||||
|
|
||||||
|
# STRATUM
|
||||||
|
sed -i "s!__STRATUM_ENABLED__!${__STRATUM_ENABLED__}!g" mempool-config.json
|
||||||
|
sed -i "s!__STRATUM_API__!${__STRATUM_API__}!g" mempool-config.json
|
||||||
|
|
||||||
# REDIS
|
# REDIS
|
||||||
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
||||||
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||||
|
|||||||
@ -45,6 +45,7 @@ __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__
|
||||||
@ -76,6 +77,7 @@ 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}
|
||||||
|
|||||||
51
frontend/custom-meta-config.json
Normal file
51
frontend/custom-meta-config.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"theme": "contrast",
|
||||||
|
"enterprise": "meta",
|
||||||
|
"branding": {
|
||||||
|
"name": "metaplanet",
|
||||||
|
"title": "Metaplanet",
|
||||||
|
"site_id": 21,
|
||||||
|
"header_img": "/resources/metalogo.svg",
|
||||||
|
"footer_img": "/resources/metalogo.svg"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"component": "fees",
|
||||||
|
"mobileOrder": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "walletBalance",
|
||||||
|
"mobileOrder": 1,
|
||||||
|
"props": {
|
||||||
|
"wallet": "3350"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "twitter",
|
||||||
|
"mobileOrder": 5,
|
||||||
|
"props": {
|
||||||
|
"handle": "Metaplanet_JP"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "wallet",
|
||||||
|
"mobileOrder": 2,
|
||||||
|
"props": {
|
||||||
|
"wallet": "3350",
|
||||||
|
"period": "all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "blocks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "walletTransactions",
|
||||||
|
"mobileOrder": 3,
|
||||||
|
"props": {
|
||||||
|
"wallet": "3350"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -344,7 +344,9 @@ describe('Mainnet', () => {
|
|||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
|
|
||||||
cy.changeNetwork('testnet4');
|
//TODO(knorrium): add a check for the proxied server
|
||||||
|
// cy.changeNetwork('testnet4');
|
||||||
|
|
||||||
cy.changeNetwork('signet');
|
cy.changeNetwork('signet');
|
||||||
cy.changeNetwork('mainnet');
|
cy.changeNetwork('mainnet');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,5 +27,6 @@
|
|||||||
"ACCELERATOR": false,
|
"ACCELERATOR": false,
|
||||||
"ACCELERATOR_BUTTON": true,
|
"ACCELERATOR_BUTTON": true,
|
||||||
"PUBLIC_ACCELERATIONS": false,
|
"PUBLIC_ACCELERATIONS": false,
|
||||||
|
"STRATUM_ENABLED": false,
|
||||||
"SERVICES_API": "https://mempool.space/api/v1/services"
|
"SERVICES_API": "https://mempool.space/api/v1/services"
|
||||||
}
|
}
|
||||||
|
|||||||
361
frontend/package-lock.json
generated
361
frontend/package-lock.json
generated
@ -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.6.0",
|
"@fortawesome/fontawesome-common-types": "~6.7.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "~6.6.0",
|
"@fortawesome/fontawesome-svg-core": "~6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "~6.6.0",
|
"@fortawesome/free-solid-svg-icons": "~6.7.2",
|
||||||
"@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,9 +33,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.5.0",
|
"echarts": "~5.6.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",
|
||||||
@ -62,7 +61,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.15.0",
|
"cypress": "^13.17.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",
|
||||||
@ -3113,9 +3112,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cypress/request": {
|
"node_modules/@cypress/request": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz",
|
||||||
"integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==",
|
"integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==",
|
||||||
|
"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.0",
|
"qs": "6.13.1",
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"tough-cookie": "^4.1.3",
|
"tough-cookie": "^5.0.0",
|
||||||
"tunnel-agent": "^0.6.0",
|
"tunnel-agent": "^0.6.0",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
@ -3141,6 +3141,22 @@
|
|||||||
"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",
|
||||||
@ -3674,30 +3690,33 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||||
"version": "6.6.0",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
|
||||||
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
|
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||||
"version": "6.6.0",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
|
||||||
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
|
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
"version": "6.6.0",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
|
||||||
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
|
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
|
||||||
|
"license": "(CC-BY-4.0 AND MIT)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@ -5673,6 +5692,7 @@
|
|||||||
"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"
|
||||||
@ -5707,6 +5727,7 @@
|
|||||||
"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"
|
||||||
@ -5827,6 +5848,7 @@
|
|||||||
"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": "*"
|
||||||
@ -5836,6 +5858,7 @@
|
|||||||
"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": {
|
||||||
@ -5993,6 +6016,7 @@
|
|||||||
"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"
|
||||||
@ -7068,6 +7092,7 @@
|
|||||||
"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": {
|
||||||
@ -7170,15 +7195,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ci-info": {
|
"node_modules/ci-info": {
|
||||||
"version": "3.8.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz",
|
||||||
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
|
"integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==",
|
||||||
"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"
|
||||||
@ -7953,13 +7979,14 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/cypress": {
|
"node_modules/cypress": {
|
||||||
"version": "13.15.0",
|
"version": "13.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz",
|
||||||
"integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==",
|
"integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cypress/request": "^3.0.4",
|
"@cypress/request": "^3.0.6",
|
||||||
"@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",
|
||||||
@ -7970,6 +7997,7 @@
|
|||||||
"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",
|
||||||
@ -7984,7 +8012,6 @@
|
|||||||
"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",
|
||||||
@ -7999,6 +8026,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@ -8201,6 +8229,7 @@
|
|||||||
"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"
|
||||||
@ -8687,6 +8716,7 @@
|
|||||||
"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",
|
||||||
@ -8694,12 +8724,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/echarts": {
|
"node_modules/echarts": {
|
||||||
"version": "5.5.0",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
|
||||||
"integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
|
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "2.3.0",
|
"tslib": "2.3.0",
|
||||||
"zrender": "5.5.0"
|
"zrender": "5.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/echarts/node_modules/tslib": {
|
"node_modules/echarts/node_modules/tslib": {
|
||||||
@ -9905,6 +9935,7 @@
|
|||||||
"engines": [
|
"engines": [
|
||||||
"node >=0.6.0"
|
"node >=0.6.0"
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/falafel": {
|
"node_modules/falafel": {
|
||||||
@ -9921,11 +9952,6 @@
|
|||||||
"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",
|
||||||
@ -10193,6 +10219,7 @@
|
|||||||
"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": "*"
|
||||||
@ -10400,6 +10427,7 @@
|
|||||||
"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"
|
||||||
@ -10854,6 +10882,7 @@
|
|||||||
"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",
|
||||||
@ -11220,18 +11249,6 @@
|
|||||||
"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",
|
||||||
@ -11481,6 +11498,7 @@
|
|||||||
"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": {
|
||||||
@ -11545,6 +11563,7 @@
|
|||||||
"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": {
|
||||||
@ -11678,6 +11697,7 @@
|
|||||||
"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": {
|
||||||
@ -11706,6 +11726,7 @@
|
|||||||
"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": {
|
||||||
@ -11723,6 +11744,7 @@
|
|||||||
"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": {
|
||||||
@ -11783,6 +11805,7 @@
|
|||||||
"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",
|
||||||
@ -12106,14 +12129,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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",
|
||||||
@ -14110,6 +14125,7 @@
|
|||||||
"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": {
|
||||||
@ -14540,12 +14556,6 @@
|
|||||||
"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",
|
||||||
@ -14661,12 +14671,6 @@
|
|||||||
"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",
|
||||||
@ -16028,6 +16032,7 @@
|
|||||||
"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",
|
||||||
@ -16577,6 +16582,26 @@
|
|||||||
"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",
|
||||||
@ -16621,27 +16646,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tough-cookie": {
|
"node_modules/tough-cookie": {
|
||||||
"version": "4.1.4",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz",
|
||||||
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
|
"integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"psl": "^1.1.33",
|
"tldts": "^6.1.32"
|
||||||
"punycode": "^2.1.1",
|
|
||||||
"universalify": "^0.2.0",
|
|
||||||
"url-parse": "^1.5.3"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=16"
|
||||||
}
|
|
||||||
},
|
|
||||||
"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": {
|
||||||
@ -16810,6 +16824,7 @@
|
|||||||
"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"
|
||||||
@ -16822,6 +16837,7 @@
|
|||||||
"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": {
|
||||||
@ -17130,16 +17146,6 @@
|
|||||||
"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",
|
||||||
@ -17207,6 +17213,7 @@
|
|||||||
"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",
|
||||||
@ -18359,9 +18366,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zrender": {
|
"node_modules/zrender": {
|
||||||
"version": "5.5.0",
|
"version": "5.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
|
||||||
"integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
|
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "2.3.0"
|
"tslib": "2.3.0"
|
||||||
}
|
}
|
||||||
@ -20348,9 +20355,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@cypress/request": {
|
"@cypress/request": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz",
|
||||||
"integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==",
|
"integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"aws-sign2": "~0.7.0",
|
"aws-sign2": "~0.7.0",
|
||||||
@ -20366,11 +20373,22 @@
|
|||||||
"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.0",
|
"qs": "6.13.1",
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"tough-cookie": "^4.1.3",
|
"tough-cookie": "^5.0.0",
|
||||||
"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": {
|
||||||
@ -20649,24 +20667,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@fortawesome/fontawesome-common-types": {
|
"@fortawesome/fontawesome-common-types": {
|
||||||
"version": "6.6.0",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
|
||||||
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw=="
|
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg=="
|
||||||
},
|
},
|
||||||
"@fortawesome/fontawesome-svg-core": {
|
"@fortawesome/fontawesome-svg-core": {
|
||||||
"version": "6.6.0",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
|
||||||
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
|
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@fortawesome/free-solid-svg-icons": {
|
"@fortawesome/free-solid-svg-icons": {
|
||||||
"version": "6.6.0",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
|
||||||
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
|
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@goto-bus-stop/common-shake": {
|
"@goto-bus-stop/common-shake": {
|
||||||
@ -23298,9 +23316,9 @@
|
|||||||
"integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="
|
"integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg=="
|
||||||
},
|
},
|
||||||
"ci-info": {
|
"ci-info": {
|
||||||
"version": "3.8.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz",
|
||||||
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
|
"integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"cipher-base": {
|
"cipher-base": {
|
||||||
@ -23896,12 +23914,12 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"cypress": {
|
"cypress": {
|
||||||
"version": "13.15.0",
|
"version": "13.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz",
|
||||||
"integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==",
|
"integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@cypress/request": "^3.0.4",
|
"@cypress/request": "^3.0.6",
|
||||||
"@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",
|
||||||
@ -23912,6 +23930,7 @@
|
|||||||
"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",
|
||||||
@ -23926,7 +23945,6 @@
|
|||||||
"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",
|
||||||
@ -23941,6 +23959,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@ -24466,12 +24485,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"echarts": {
|
"echarts": {
|
||||||
"version": "5.5.0",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
|
||||||
"integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
|
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "2.3.0",
|
"tslib": "2.3.0",
|
||||||
"zrender": "5.5.0"
|
"zrender": "5.6.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": {
|
"tslib": {
|
||||||
@ -25433,11 +25452,6 @@
|
|||||||
"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",
|
||||||
@ -26373,15 +26387,6 @@
|
|||||||
"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",
|
||||||
@ -27015,14 +27020,6 @@
|
|||||||
"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",
|
||||||
@ -28806,12 +28803,6 @@
|
|||||||
"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",
|
||||||
@ -28903,12 +28894,6 @@
|
|||||||
"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",
|
||||||
@ -30373,6 +30358,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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,23 +30405,12 @@
|
|||||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
|
||||||
},
|
},
|
||||||
"tough-cookie": {
|
"tough-cookie": {
|
||||||
"version": "4.1.4",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz",
|
||||||
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
|
"integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"psl": "^1.1.33",
|
"tldts": "^6.1.32"
|
||||||
"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": {
|
||||||
@ -30757,16 +30746,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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",
|
||||||
@ -31506,9 +31485,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zrender": {
|
"zrender": {
|
||||||
"version": "5.5.0",
|
"version": "5.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
|
||||||
"integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
|
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "2.3.0"
|
"tslib": "2.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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.6.0",
|
"@fortawesome/fontawesome-common-types": "~6.7.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "~6.6.0",
|
"@fortawesome/fontawesome-svg-core": "~6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "~6.6.0",
|
"@fortawesome/free-solid-svg-icons": "~6.7.2",
|
||||||
"@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,8 +86,7 @@
|
|||||||
"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.5.0",
|
"echarts": "~5.6.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",
|
||||||
@ -115,7 +114,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.15.0",
|
"cypress": "^13.17.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",
|
||||||
|
|||||||
@ -3,8 +3,10 @@ const fs = require('fs');
|
|||||||
let PROXY_CONFIG = require('./proxy.conf');
|
let PROXY_CONFIG = require('./proxy.conf');
|
||||||
|
|
||||||
PROXY_CONFIG.forEach(entry => {
|
PROXY_CONFIG.forEach(entry => {
|
||||||
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
|
const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space';
|
||||||
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
|
console.log(`e2e tests running against ${hostname}`);
|
||||||
|
entry.target = entry.target.replace("mempool.space", hostname);
|
||||||
|
entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space");
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = PROXY_CONFIG;
|
module.exports = PROXY_CONFIG;
|
||||||
|
|||||||
@ -439,4 +439,39 @@ export const fiatCurrencies = {
|
|||||||
code: 'ZAR',
|
code: 'ZAR',
|
||||||
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)' }
|
||||||
|
];
|
||||||
@ -3,7 +3,7 @@ import { NgModule } from '@angular/core';
|
|||||||
import { ServerModule } from '@angular/platform-server';
|
import { ServerModule } from '@angular/platform-server';
|
||||||
|
|
||||||
import { ZONE_SERVICE } from '@app/injection-tokens';
|
import { ZONE_SERVICE } from '@app/injection-tokens';
|
||||||
import { AppModule } from '@app/app.module';
|
import { AppModule } from './app.module';
|
||||||
import { AppComponent } from '@components/app/app.component';
|
import { AppComponent } from '@components/app/app.component';
|
||||||
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
|
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
|
||||||
import { ZoneService } from '@app/services/zone.service';
|
import { ZoneService } from '@app/services/zone.service';
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core';
|
|||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { ZONE_SERVICE } from '@app/injection-tokens';
|
import { ZONE_SERVICE } from '@app/injection-tokens';
|
||||||
import { AppRoutingModule } from '@app/app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from '@components/app/app.component';
|
import { AppComponent } from '@components/app/app.component';
|
||||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||||
import { OrdApiService } from '@app/services/ord-api.service';
|
import { OrdApiService } from '@app/services/ord-api.service';
|
||||||
|
|||||||
@ -1,10 +1,18 @@
|
|||||||
<div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
|
<div class="box card w-100 accelerate-checkout-inner" [class.input-disabled]="isCheckoutLocked > 0" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
|
||||||
@if (accelerateError) {
|
@if (accelerateError) {
|
||||||
<div class="row mb-1 text-center">
|
@if (accelerateError.includes('Payment declined')) {
|
||||||
<div class="col-sm">
|
<div class="row mb-1 text-center">
|
||||||
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
|
<div class="col-sm">
|
||||||
|
<h1 style="font-size: larger;">{{ accelerateError }}</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
} @else {
|
||||||
|
<div class="row mb-1 text-center">
|
||||||
|
<div class="col-sm">
|
||||||
|
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div 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">
|
||||||
@ -361,7 +369,7 @@
|
|||||||
<div class="row text-center justify-content-center mx-2">
|
<div class="row text-center justify-content-center mx-2">
|
||||||
<p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p>
|
<p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p>
|
||||||
</div>
|
</div>
|
||||||
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
|
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
||||||
<p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container> <small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></p>
|
<p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container> <small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></p>
|
||||||
@ -389,13 +397,13 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
|
||||||
<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">—<span i18n="or">OR</span>—</p>
|
<p class="text-nowrap">—<span i18n="or">OR</span>—</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
|
||||||
<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> <app-fiat [value]="cost"></app-fiat> with</p>
|
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <app-fiat [value]="cost"></app-fiat> with</p>
|
||||||
@if (canPayWithCashapp) {
|
@if (canPayWithCashapp) {
|
||||||
@ -413,6 +421,13 @@
|
|||||||
<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')">
|
||||||
|
<fa-icon style="font-size: 24px; color: white" [icon]="['fas', 'credit-card']"></fa-icon>
|
||||||
|
<span class="ml-2" style="font-size: 22px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -435,7 +450,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') {
|
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') {
|
||||||
<!-- 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">
|
||||||
@ -451,7 +466,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
|
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay || step === 'cardonfile' && !loadingCardOnFile) {
|
||||||
<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">
|
||||||
@ -476,14 +491,24 @@
|
|||||||
<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) {
|
@if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) {
|
||||||
<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>
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,13 @@
|
|||||||
color: var(--green)
|
color: var(--green)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accelerate-checkout-inner {
|
||||||
|
&.input-disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.paymentMethod {
|
.paymentMethod {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: var(--secondary);
|
background-color: var(--secondary);
|
||||||
@ -172,10 +179,6 @@
|
|||||||
background-color: var(--tertiary);
|
background-color: var(--tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-small-height {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-row {
|
.summary-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
|
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
|
||||||
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
|
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
|
||||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||||
import { md5, insecureRandomUUID } from '@app/shared/common.utils';
|
import { md5 } from '@app/shared/common.utils';
|
||||||
import { StateService } from '@app/services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { AudioService } from '@app/services/audio.service';
|
import { AudioService } from '@app/services/audio.service';
|
||||||
import { ETA, EtaService } from '@app/services/eta.service';
|
import { ETA, EtaService } from '@app/services/eta.service';
|
||||||
@ -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';
|
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay' | 'cardOnFile';
|
||||||
|
|
||||||
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}>;
|
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number, card?: {card_id: string, last_4: string, brand: string, name: string, billing: any}}>;
|
||||||
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' | 'processing' | 'paid' | 'success';
|
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'cardonfile' | 'processing' | 'paid' | 'success';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-accelerate-checkout',
|
selector: 'app-accelerate-checkout',
|
||||||
@ -65,6 +65,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
@Input() cashappEnabled: boolean = true;
|
@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,6 +77,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
calculating = true;
|
calculating = true;
|
||||||
processing = false;
|
processing = false;
|
||||||
|
isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
|
||||||
|
isTokenizing = 0; // reference counter, 0 = false, >0 = true
|
||||||
selectedOption: 'wait' | 'accel';
|
selectedOption: 'wait' | 'accel';
|
||||||
cantPayReason = '';
|
cantPayReason = '';
|
||||||
quoteError = ''; // error fetching estimate or initial data
|
quoteError = ''; // error fetching estimate or initial data
|
||||||
@ -94,7 +97,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
auth: IAuth | null = null;
|
auth: IAuth | null = null;
|
||||||
|
|
||||||
// accelerator stuff
|
// accelerator stuff
|
||||||
accelerationUUID: string;
|
|
||||||
accelerationSubscription: Subscription;
|
accelerationSubscription: Subscription;
|
||||||
difficultySubscription: Subscription;
|
difficultySubscription: Subscription;
|
||||||
estimateSubscription: Subscription;
|
estimateSubscription: Subscription;
|
||||||
@ -116,6 +118,7 @@ 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;
|
||||||
@ -138,7 +141,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
private enterpriseService: EnterpriseService,
|
private enterpriseService: EnterpriseService,
|
||||||
) {
|
) {
|
||||||
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
|
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
|
||||||
this.accelerationUUID = insecureRandomUUID();
|
|
||||||
|
|
||||||
// Check if Apple Pay available
|
// Check if Apple Pay available
|
||||||
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
|
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
|
||||||
@ -156,7 +158,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerateError = null;
|
this.accelerateError = null;
|
||||||
this.timePaid = 0;
|
this.timePaid = 0;
|
||||||
this.btcpayInvoiceFailed = false;
|
this.btcpayInvoiceFailed = false;
|
||||||
this.moveToStep('summary');
|
this.moveToStep('summary', true);
|
||||||
} else {
|
} else {
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
}
|
}
|
||||||
@ -165,11 +167,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
|
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
|
||||||
this.moveToStep('processing');
|
this.moveToStep('processing', true);
|
||||||
this.insertSquare();
|
this.insertSquare();
|
||||||
this.setupSquare();
|
this.setupSquare();
|
||||||
} else {
|
} else {
|
||||||
this.moveToStep('summary');
|
this.moveToStep('summary', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
@ -194,14 +196,18 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
if (changes.accelerating && this.accelerating) {
|
if (changes.accelerating && this.accelerating) {
|
||||||
if (this.step === 'processing' || this.step === 'paid') {
|
if (this.step === 'processing' || this.step === 'paid') {
|
||||||
this.moveToStep('success');
|
this.moveToStep('success', true);
|
||||||
} else { // Edge case where the transaction gets accelerated by someone else or on another session
|
} else { // Edge case where the transaction gets accelerated by someone else or on another session
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
moveToStep(step: CheckoutStep): void {
|
moveToStep(step: CheckoutStep, force: boolean = false): void {
|
||||||
|
if (this.isCheckoutLocked > 0 && !force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.processing = false;
|
||||||
this._step = step;
|
this._step = step;
|
||||||
if (this.timeoutTimer) {
|
if (this.timeoutTimer) {
|
||||||
clearTimeout(this.timeoutTimer);
|
clearTimeout(this.timeoutTimer);
|
||||||
@ -230,6 +236,10 @@ 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(() => {
|
||||||
@ -243,7 +253,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
closeModal(): void {
|
closeModal(): void {
|
||||||
this.completed.emit(true);
|
this.completed.emit(true);
|
||||||
this.moveToStep('summary');
|
this.moveToStep('summary', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -387,7 +397,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerationSubscription = this.servicesApiService.accelerate$(
|
this.accelerationSubscription = this.servicesApiService.accelerate$(
|
||||||
this.tx.txid,
|
this.tx.txid,
|
||||||
this.userBid,
|
this.userBid,
|
||||||
this.accelerationUUID
|
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
@ -395,7 +404,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
this.showSuccess = true;
|
this.showSuccess = true;
|
||||||
this.estimateSubscription.unsubscribe();
|
this.estimateSubscription.unsubscribe();
|
||||||
this.moveToStep('paid');
|
this.moveToStep('paid', true);
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
@ -451,6 +460,8 @@ 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: () => {
|
||||||
@ -505,57 +516,75 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.loadingApplePay = false;
|
this.loadingApplePay = false;
|
||||||
applePayButton.addEventListener('click', async event => {
|
applePayButton.addEventListener('click', async event => {
|
||||||
|
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const tokenResult = await this.applePay.tokenize();
|
try {
|
||||||
if (tokenResult?.status === 'OK') {
|
// lock the checkout UI and show a loading spinner until the square modals are finished
|
||||||
const card = tokenResult.details?.card;
|
this.isCheckoutLocked++;
|
||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
this.isTokenizing++;
|
||||||
console.error(`Cannot retreive payment card details`);
|
const tokenResult = await this.applePay.tokenize();
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
if (tokenResult?.status === 'OK') {
|
||||||
this.processing = false;
|
const card = tokenResult.details?.card;
|
||||||
return;
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
}
|
console.error(`Cannot retreive payment card details`);
|
||||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
this.servicesApiService.accelerateWithApplePay$(
|
|
||||||
this.tx.txid,
|
|
||||||
tokenResult.token,
|
|
||||||
cardTag,
|
|
||||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
|
||||||
this.accelerationUUID,
|
|
||||||
costUSD
|
|
||||||
).subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
return;
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
|
||||||
if (this.applePay) {
|
|
||||||
this.applePay.destroy();
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
this.moveToStep('paid');
|
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
error: (response) => {
|
|
||||||
this.processing = false;
|
|
||||||
this.accelerateError = response.error;
|
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
|
||||||
setTimeout(() => {
|
|
||||||
// Reset everything by reloading the page :D, can be improved
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||||
} else {
|
// keep checkout in loading state until the acceleration request completes
|
||||||
this.processing = false;
|
this.isTokenizing++;
|
||||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
this.isCheckoutLocked++;
|
||||||
if (tokenResult.errors) {
|
this.servicesApiService.accelerateWithApplePay$(
|
||||||
errorMessage += ` and errors: ${JSON.stringify(
|
this.tx.txid,
|
||||||
tokenResult.errors,
|
tokenResult.token,
|
||||||
)}`;
|
cardTag,
|
||||||
|
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||||
|
costUSD
|
||||||
|
).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
|
if (this.applePay) {
|
||||||
|
this.applePay.destroy();
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
this.moveToStep('paid', true);
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
|
this.accelerateError = response.error;
|
||||||
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
// Reset everything by reloading the page :D, can be improved
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.processing = false;
|
||||||
|
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||||
|
if (tokenResult.errors) {
|
||||||
|
errorMessage += ` and errors: ${JSON.stringify(
|
||||||
|
tokenResult.errors,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
} finally {
|
||||||
|
// always unlock the checkout once we're finished
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -605,63 +634,193 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.loadingGooglePay = false;
|
this.loadingGooglePay = false;
|
||||||
|
|
||||||
document.getElementById('google-pay-button').addEventListener('click', async event => {
|
document.getElementById('google-pay-button').addEventListener('click', async event => {
|
||||||
|
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const tokenResult = await this.googlePay.tokenize();
|
try {
|
||||||
if (tokenResult?.status === 'OK') {
|
// lock the checkout UI and show a loading spinner until the square modals are finished
|
||||||
const card = tokenResult.details?.card;
|
this.isCheckoutLocked++;
|
||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
this.isTokenizing++;
|
||||||
console.error(`Cannot retreive payment card details`);
|
const tokenResult = await this.googlePay.tokenize();
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
if (tokenResult?.status === 'OK') {
|
||||||
this.processing = false;
|
const card = tokenResult.details?.card;
|
||||||
return;
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
}
|
console.error(`Cannot retreive payment card details`);
|
||||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
this.servicesApiService.accelerateWithGooglePay$(
|
|
||||||
this.tx.txid,
|
|
||||||
tokenResult.token,
|
|
||||||
cardTag,
|
|
||||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
|
||||||
this.accelerationUUID,
|
|
||||||
costUSD
|
|
||||||
).subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
return;
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
|
||||||
if (this.googlePay) {
|
|
||||||
this.googlePay.destroy();
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
this.moveToStep('paid');
|
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
error: (response) => {
|
|
||||||
this.processing = false;
|
|
||||||
this.accelerateError = response.error;
|
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
|
||||||
setTimeout(() => {
|
|
||||||
// Reset everything by reloading the page :D, can be improved
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
|
||||||
} else {
|
if (!verificationToken || !verificationToken.token) {
|
||||||
this.processing = false;
|
console.error(`SCA verification failed`);
|
||||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
|
||||||
if (tokenResult.errors) {
|
this.processing = false;
|
||||||
errorMessage += ` and errors: ${JSON.stringify(
|
return;
|
||||||
tokenResult.errors,
|
}
|
||||||
)}`;
|
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||||
|
// keep checkout in loading state until the acceleration request completes
|
||||||
|
this.isCheckoutLocked++;
|
||||||
|
this.isTokenizing++;
|
||||||
|
this.servicesApiService.accelerateWithGooglePay$(
|
||||||
|
this.tx.txid,
|
||||||
|
tokenResult.token,
|
||||||
|
verificationToken.token,
|
||||||
|
cardTag,
|
||||||
|
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||||
|
costUSD,
|
||||||
|
verificationToken.userChallenged
|
||||||
|
).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
|
if (this.googlePay) {
|
||||||
|
this.googlePay.destroy();
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
this.moveToStep('paid', true);
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
|
this.accelerateError = response.error;
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Reset everything by reloading the page :D, can be improved
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.processing = false;
|
||||||
|
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||||
|
if (tokenResult.errors) {
|
||||||
|
errorMessage += ` and errors: ${JSON.stringify(
|
||||||
|
tokenResult.errors,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
} finally {
|
||||||
|
// always unlock the checkout once we're finished
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
*/
|
*/
|
||||||
@ -712,7 +871,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
tokenResult.token,
|
tokenResult.token,
|
||||||
tokenResult.details.cashAppPay.cashtag,
|
tokenResult.details.cashAppPay.cashtag,
|
||||||
tokenResult.details.cashAppPay.referenceId,
|
tokenResult.details.cashAppPay.referenceId,
|
||||||
this.accelerationUUID,
|
|
||||||
costUSD
|
costUSD
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@ -723,7 +881,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.cashAppPay.destroy();
|
this.cashAppPay.destroy();
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.moveToStep('paid');
|
this.moveToStep('paid', true);
|
||||||
if (window.history.replaceState) {
|
if (window.history.replaceState) {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
|
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
|
||||||
@ -738,7 +896,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')}`, ``));
|
||||||
}, 3000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -748,6 +906,32 @@ 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
|
||||||
*/
|
*/
|
||||||
@ -771,7 +955,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
this.estimateSubscription.unsubscribe();
|
this.estimateSubscription.unsubscribe();
|
||||||
this.moveToStep('paid');
|
this.moveToStep('paid', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggedIn(): boolean {
|
isLoggedIn(): boolean {
|
||||||
@ -882,6 +1066,22 @@ 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;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed">
|
<div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed">
|
||||||
<div class="timeline-wrapper">
|
<div class="timeline-wrapper">
|
||||||
@if (!tx.status.confirmed) {
|
@if (!tx.status.confirmed || canceled) {
|
||||||
<div class="timeline">
|
<div class="timeline">
|
||||||
<div class="intervals">
|
<div class="intervals">
|
||||||
<div class="node-spacer"></div>
|
<div class="node-spacer"></div>
|
||||||
@ -8,7 +8,7 @@
|
|||||||
<div class="node-spacer"></div>
|
<div class="node-spacer"></div>
|
||||||
<div class="interval">
|
<div class="interval">
|
||||||
<div class="interval-time">
|
<div class="interval-time">
|
||||||
@if (eta) {
|
@if (eta && !canceled) {
|
||||||
~<app-time [time]="eta?.wait / 1000"></app-time>
|
~<app-time [time]="eta?.wait / 1000"></app-time>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -19,16 +19,20 @@
|
|||||||
<div class="node-spacer"></div>
|
<div class="node-spacer"></div>
|
||||||
<div class="interval-spacer"></div>
|
<div class="interval-spacer"></div>
|
||||||
<div class="node">
|
<div class="node">
|
||||||
<div class="acc-to-confirmed right go-faster"></div>
|
<div class="acc-to-confirmed right go-faster" [class.no-animation]="canceled"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="interval-spacer">
|
<div class="interval-spacer">
|
||||||
</div>
|
</div>
|
||||||
<div class="node" [id]="'confirmed'">
|
<div class="node" [id]="'confirmed'">
|
||||||
<div class="acc-to-confirmed left go-faster"></div>
|
<div class="acc-to-confirmed left go-faster" [class.no-animation]="canceled"></div>
|
||||||
<div class="shape-border waiting">
|
<div class="shape-border waiting">
|
||||||
<div class="shape"></div>
|
<div class="shape"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
|
@if (canceled) {
|
||||||
|
<div class="status"><span class="badge badge-danger" i18n="accelerator.canceled">Canceled</span></div>
|
||||||
|
} @else {
|
||||||
|
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -45,9 +49,9 @@
|
|||||||
<div class="interval">
|
<div class="interval">
|
||||||
<div class="interval-time">
|
<div class="interval-time">
|
||||||
@if (tx.status.confirmed) {
|
@if (tx.status.confirmed) {
|
||||||
<div class="interval-time">
|
<app-time [time]="acceleratedToMined"></app-time>
|
||||||
<app-time [time]="acceleratedToMined"></app-time>
|
} @else if (eta && canceled) {
|
||||||
</div>
|
~<app-time [time]="eta?.wait / 1000"></app-time>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -71,42 +75,42 @@
|
|||||||
<div class="interval-spacer">
|
<div class="interval-spacer">
|
||||||
<div class="seen-to-acc"></div>
|
<div class="seen-to-acc"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="node" [class.accelerated]="!tx.status.confirmed" [id]="'accelerated'">
|
<div class="node" [class.accelerated]="!tx.status.confirmed && !canceled" [id]="'accelerated'">
|
||||||
<div class="seen-to-acc left"></div>
|
<div class="seen-to-acc left"></div>
|
||||||
@if (tx.status.confirmed) {
|
@if (tx.status.confirmed && !canceled) {
|
||||||
<div class="acc-to-confirmed right"></div>
|
<div class="acc-to-confirmed right"></div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="seen-to-acc right"></div>
|
<div class="seen-to-acc right"></div>
|
||||||
}
|
}
|
||||||
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
|
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
|
||||||
<div class="shape"></div>
|
<div class="shape"></div>
|
||||||
@if (!tx.status.confirmed) {
|
@if (!tx.status.confirmed || canceled) {
|
||||||
<div class="connector down loading"></div>
|
<div class="connector down" [class.loading]="!canceled"></div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (tx.status.confirmed) {
|
@if (tx.status.confirmed && !canceled) {
|
||||||
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
|
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
|
||||||
}
|
}
|
||||||
<div class="time" [class.no-margin]="!tx.status.confirmed" [class.offset-left]="!tx.status.confirmed">
|
<div class="time" [class.no-margin]="!tx.status.confirmed || canceled" [class.offset-left]="!tx.status.confirmed || canceled">
|
||||||
@if (!tx.status.confirmed) {
|
@if (!tx.status.confirmed) {
|
||||||
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
|
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
|
||||||
}
|
}
|
||||||
@if (useAbsoluteTime) {
|
@if (useAbsoluteTime) {
|
||||||
<span>{{ acceleratedAt * 1000 | date }}</span>
|
<span>{{ acceleratedAt * 1000 | date }}</span>
|
||||||
} @else {
|
} @else {
|
||||||
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed"></app-time>
|
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed || canceled"></app-time>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="interval-spacer">
|
<div class="interval-spacer">
|
||||||
@if (tx.status.confirmed) {
|
@if (tx.status.confirmed && !canceled) {
|
||||||
<div class="acc-to-confirmed"></div>
|
<div class="acc-to-confirmed"></div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="seen-to-acc"></div>
|
<div class="seen-to-acc"></div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'">
|
<div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'">
|
||||||
@if (tx.status.confirmed) {
|
@if (tx.status.confirmed && !canceled) {
|
||||||
<div class="acc-to-confirmed left"></div>
|
<div class="acc-to-confirmed left"></div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="seen-to-acc left"></div>
|
<div class="seen-to-acc left"></div>
|
||||||
|
|||||||
@ -129,6 +129,9 @@
|
|||||||
margin-left: calc(-4em + 5px);
|
margin-left: calc(-4em + 5px);
|
||||||
animation: goFasterLeft 0.8s infinite linear;
|
animation: goFasterLeft 0.8s infinite linear;
|
||||||
}
|
}
|
||||||
|
&.no-animation {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.left {
|
&.left {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
|||||||
@Input() tx: Transaction;
|
@Input() tx: Transaction;
|
||||||
@Input() accelerationInfo: Acceleration;
|
@Input() accelerationInfo: Acceleration;
|
||||||
@Input() eta: ETA;
|
@Input() eta: ETA;
|
||||||
|
@Input() canceled: boolean;
|
||||||
|
|
||||||
now: number;
|
now: number;
|
||||||
accelerateRatio: number;
|
accelerateRatio: number;
|
||||||
|
|||||||
@ -46,6 +46,8 @@ 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 = '';
|
||||||
@ -79,8 +81,8 @@ 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.route.fragment.subscribe((fragment) => {
|
this.fragmentSubscription = 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 });
|
||||||
}
|
}
|
||||||
@ -113,7 +115,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
|||||||
share(),
|
share(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.aggregatedHistory$.subscribe();
|
this.aggregatedHistorySubscription = this.aggregatedHistory$.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
@ -335,8 +337,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
if (this.statsSubscription) {
|
this.aggregatedHistorySubscription?.unsubscribe();
|
||||||
this.statsSubscription.unsubscribe();
|
this.fragmentSubscription?.unsubscribe();
|
||||||
}
|
this.statsSubscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
<div class="acceleration-list">
|
<div class="acceleration-list" *ngIf="{ accelerations: accelerationList$ | async } as state">
|
||||||
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
|
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
|
||||||
<thead>
|
<thead>
|
||||||
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
|
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||||
@ -14,15 +14,15 @@
|
|||||||
<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" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
|
<th class="fee text-right text-truncate" 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>
|
||||||
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
|
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
<tbody *ngIf="state.accelerations && nonEmptyAccelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||||
<tr *ngFor="let acceleration of accelerations; let i= index;">
|
<tr *ngFor="let acceleration of state.accelerations; let i= index;">
|
||||||
<td class="txid text-left">
|
<td class="txid text-left">
|
||||||
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
|
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
|
||||||
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>
|
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>
|
||||||
@ -64,7 +64,8 @@
|
|||||||
<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'"> ⌛</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'"> ⌛</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'"> ⌛</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'"> ⌛</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'"> ⌛</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'"> ⌛</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'"> ⌛</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>
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
|
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
@if (hasCpfp) {
|
@if (hasCpfp) {
|
||||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
|
||||||
}
|
}
|
||||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
@ -36,7 +36,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="pt-0">
|
<td colspan="3" class="pt-0">
|
||||||
<div class="d-flex justify-content-end align-items-start">
|
<div class="d-flex justify-content-end align-items-start">
|
||||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -76,15 +76,21 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
acceleratingPools.forEach((poolId, index) => {
|
acceleratingPools.forEach((poolId, index) => {
|
||||||
const pool = pools[poolId];
|
const pool = pools[poolId];
|
||||||
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
|
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
|
||||||
|
let color = 'white';
|
||||||
|
if (index >= firstSignificantPool) {
|
||||||
|
if (numSignificantPools > 1) {
|
||||||
|
color = toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / Math.max((numSignificantPools - 1), 1)));
|
||||||
|
} else {
|
||||||
|
color = toRGB({ r: 147, g: 57, b: 244 });
|
||||||
|
}
|
||||||
|
}
|
||||||
data.push(getDataItem(
|
data.push(getDataItem(
|
||||||
pool.lastEstimatedHashrate,
|
pool.lastEstimatedHashrate,
|
||||||
index >= firstSignificantPool
|
color,
|
||||||
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
|
|
||||||
: 'white',
|
|
||||||
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
|
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
|
||||||
true,
|
true,
|
||||||
) as PieSeriesOption);
|
) as PieSeriesOption);
|
||||||
})
|
});
|
||||||
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
|
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
|
||||||
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
|
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
|
||||||
data.push(getDataItem(
|
data.push(getDataItem(
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pip
|
|||||||
import { StateService } from '@app/services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { PriceService } from '@app/services/price.service';
|
import { PriceService } from '@app/services/price.service';
|
||||||
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||||
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
|
|
||||||
|
|
||||||
const periodSeconds = {
|
const periodSeconds = {
|
||||||
'1d': (60 * 60 * 24),
|
'1d': (60 * 60 * 24),
|
||||||
@ -45,14 +44,18 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
@Input() right: number | string = 10;
|
@Input() right: number | string = 10;
|
||||||
@Input() left: number | string = 70;
|
@Input() left: number | string = 70;
|
||||||
@Input() widget: boolean = false;
|
@Input() widget: 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;
|
||||||
@ -77,7 +80,6 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
private relativeUrlPipe: RelativeUrlPipe,
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
private priceService: PriceService,
|
private priceService: PriceService,
|
||||||
private fiatCurrencyPipe: FiatCurrencyPipe,
|
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||||
private fiatShortenerPipe: FiatShortenerPipe,
|
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -86,6 +88,9 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
if (!this.addressSummary$ && (!this.address || !this.stats)) {
|
if (!this.addressSummary$ && (!this.address || !this.stats)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (changes.defaultFiat) {
|
||||||
|
this.selected['Fiat'] = !!this.defaultFiat;
|
||||||
|
}
|
||||||
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
|
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
|
||||||
if (this.subscription) {
|
if (this.subscription) {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
@ -118,7 +123,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 };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -147,7 +152,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
if (!summary) {
|
if (!summary) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
|
const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
|
||||||
let runningTotal = total;
|
let runningTotal = total;
|
||||||
const processData = summary.map(d => {
|
const processData = summary.map(d => {
|
||||||
@ -161,7 +166,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
d
|
d
|
||||||
};
|
};
|
||||||
}).reverse();
|
}).reverse();
|
||||||
|
|
||||||
this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]);
|
this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]);
|
||||||
this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]);
|
this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]);
|
||||||
|
|
||||||
@ -179,6 +184,9 @@ 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.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40;
|
||||||
|
|
||||||
this.chartOptions = {
|
this.chartOptions = {
|
||||||
color: [
|
color: [
|
||||||
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
@ -194,10 +202,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.right,
|
right: this.adjustedRight,
|
||||||
left: this.left,
|
left: this.adjustedLeft,
|
||||||
},
|
},
|
||||||
legend: !this.stateService.isAnyTestnet() ? {
|
legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? {
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
|
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
|
||||||
@ -245,21 +253,22 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
let tooltip = '<div>';
|
let tooltip = '<div>';
|
||||||
|
|
||||||
const hasTx = data[0].data[2].txid;
|
const hasTx = data[0].data[2].txid;
|
||||||
|
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
|
|
||||||
|
tooltip += `<div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<div><b>${date}</b></div>`;
|
||||||
|
|
||||||
if (hasTx) {
|
if (hasTx) {
|
||||||
const header = data.length === 1
|
const header = data.length === 1
|
||||||
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
|
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
|
||||||
: `${data.length} transactions`;
|
: `${data.length} transactions`;
|
||||||
tooltip += `<span><b>${header}</b></span>`;
|
tooltip += `<div><b>${header}</b></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
||||||
|
|
||||||
tooltip += `<div>
|
|
||||||
<div style="text-align: right;">`;
|
|
||||||
|
|
||||||
const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal);
|
const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal);
|
||||||
const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
|
const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
|
||||||
|
|
||||||
const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0);
|
const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0);
|
||||||
const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0);
|
const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0);
|
||||||
const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)');
|
const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)');
|
||||||
@ -291,7 +300,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip += `</div><span>${date}</span></div>`;
|
tooltip += `</div></div>`;
|
||||||
return tooltip;
|
return tooltip;
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
},
|
},
|
||||||
@ -307,22 +316,26 @@ 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);
|
||||||
if (valSpan > 100_000_000_000) {
|
if (valSpan > 100_000_000_000) {
|
||||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0)} BTC`;
|
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0, undefined, true)} BTC`;
|
||||||
}
|
}
|
||||||
else if (valSpan > 1_000_000_000) {
|
else if (valSpan > 1_000_000_000) {
|
||||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2)} BTC`;
|
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2, undefined, true)} BTC`;
|
||||||
} else if (valSpan > 100_000_000) {
|
} else if (valSpan > 100_000_000) {
|
||||||
return `${(val / 100_000_000).toFixed(1)} BTC`;
|
return `${(val / 100_000_000).toFixed(1)} BTC`;
|
||||||
} else if (valSpan > 10_000_000) {
|
} else if (valSpan > 10_000_000) {
|
||||||
return `${(val / 100_000_000).toFixed(2)} BTC`;
|
return `${(val / 100_000_000).toFixed(2)} BTC`;
|
||||||
} else if (valSpan > 1_000_000) {
|
} else if (valSpan > 1_000_000) {
|
||||||
|
if (maxValue > 100_000_000_000) {
|
||||||
|
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 3, undefined, true)} BTC`;
|
||||||
|
}
|
||||||
return `${(val / 100_000_000).toFixed(3)} BTC`;
|
return `${(val / 100_000_000).toFixed(3)} BTC`;
|
||||||
} else {
|
} else {
|
||||||
return `${this.amountShortenerPipe.transform(val, 0)} sats`;
|
return `${this.amountShortenerPipe.transform(val, 0, undefined, true)} sats`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -334,9 +347,10 @@ 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.fiatShortenerPipe.transform(val, null, 'USD');
|
return `$${this.amountShortenerPipe.transform(val, 3, undefined, true, true)}`;
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
@ -390,8 +404,8 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
type: 'slider',
|
type: 'slider',
|
||||||
brushSelect: false,
|
brushSelect: false,
|
||||||
realtime: true,
|
realtime: true,
|
||||||
left: this.left,
|
left: this.adjustedLeft,
|
||||||
right: this.right,
|
right: this.adjustedRight,
|
||||||
selectedDataBackground: {
|
selectedDataBackground: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
@ -404,7 +418,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
onChartClick(e) {
|
onChartClick(e) {
|
||||||
if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) {
|
if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
const url = this.relativeUrlPipe.transform(`/tx/${this.hoverData[0][2].txid}`);
|
const url = this.relativeUrlPipe.transform(`/tx/${this.hoverData[0][2].txid}`);
|
||||||
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
|
if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
|
||||||
window.open(url);
|
window.open(url);
|
||||||
@ -421,26 +435,26 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
onLegendSelectChanged(e) {
|
onLegendSelectChanged(e) {
|
||||||
this.selected = e.selected;
|
this.selected = e.selected;
|
||||||
this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
|
this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right;
|
||||||
this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
|
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40;
|
||||||
|
|
||||||
this.chartOptions = {
|
this.chartOptions = {
|
||||||
grid: {
|
grid: {
|
||||||
right: this.right,
|
right: this.adjustedRight,
|
||||||
left: this.left,
|
left: this.adjustedLeft,
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
selected: this.selected,
|
selected: this.selected,
|
||||||
},
|
},
|
||||||
dataZoom: this.allowZoom ? [{
|
dataZoom: this.allowZoom ? [{
|
||||||
left: this.left,
|
left: this.adjustedLeft,
|
||||||
right: this.right,
|
right: this.adjustedRight,
|
||||||
}, {
|
}, {
|
||||||
left: this.left,
|
left: this.adjustedLeft,
|
||||||
right: this.right,
|
right: this.adjustedRight,
|
||||||
}] : undefined
|
}] : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.chartInstance) {
|
if (this.chartInstance) {
|
||||||
this.chartInstance.setOption(this.chartOptions);
|
this.chartInstance.setOption(this.chartOptions);
|
||||||
}
|
}
|
||||||
@ -464,25 +478,30 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extendSummary(summary) {
|
extendSummary(summary) {
|
||||||
let extendedSummary = summary.slice();
|
const extendedSummary = summary.slice();
|
||||||
|
|
||||||
// Add a point at today's date to make the graph end at the current time
|
// Add a point at today's date to make the graph end at the current time
|
||||||
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
|
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
|
||||||
extendedSummary.reverse();
|
|
||||||
|
let 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++) {
|
||||||
let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);
|
if (extendedSummary[i].time > maxTime) {
|
||||||
|
extendedSummary[i].time = maxTime - 30;
|
||||||
|
}
|
||||||
|
maxTime = extendedSummary[i].time;
|
||||||
|
const hours = Math.floor((extendedSummary[i].time - extendedSummary[i + 1].time) / oneHour);
|
||||||
if (hours > 1) {
|
if (hours > 1) {
|
||||||
for (let j = 1; j < hours; j++) {
|
for (let j = 1; j < hours; j++) {
|
||||||
let newTime = extendedSummary[i].time + oneHour * j;
|
const newTime = extendedSummary[i].time - oneHour * j;
|
||||||
extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
|
extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
|
||||||
}
|
}
|
||||||
i += hours - 1;
|
i += hours - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return extendedSummary.reverse();
|
return extendedSummary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// prevent arrow key horizontal scrolling
|
// prevent arrow key horizontal scrolling
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { AudioService } from '@app/services/audio.service';
|
|||||||
import { ApiService } from '@app/services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { of, merge, Subscription, combineLatest } from 'rxjs';
|
import { of, merge, Subscription, combineLatest } from 'rxjs';
|
||||||
import { SeoService } from '@app/services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { environment } from '@app/../environments/environment';
|
import { environment } from '@environments/environment';
|
||||||
import { AssetsService } from '@app/services/assets.service';
|
import { AssetsService } from '@app/services/assets.service';
|
||||||
import { moveDec } from '@app/bitcoin.utils';
|
import { moveDec } from '@app/bitcoin.utils';
|
||||||
|
|
||||||
|
|||||||
@ -172,13 +172,19 @@ 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);
|
||||||
this.themeChangedSubscription?.unsubscribe();
|
|
||||||
}
|
}
|
||||||
|
if (this.scene) {
|
||||||
|
this.scene.destroy();
|
||||||
|
}
|
||||||
|
this.vertexArray.destroy();
|
||||||
|
this.vertexArray = null;
|
||||||
|
this.themeChangedSubscription?.unsubscribe();
|
||||||
|
this.searchSubscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(direction): void {
|
clear(direction): void {
|
||||||
@ -447,7 +453,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) {
|
if (this.scene && this.gl && this.vertexArray) {
|
||||||
/* 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);
|
||||||
@ -489,9 +495,7 @@ 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);
|
||||||
|
|||||||
@ -19,6 +19,7 @@ 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;
|
||||||
@ -32,6 +33,9 @@ export class FastVertexArray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
insert(sprite: TxSprite): number {
|
insert(sprite: TxSprite): number {
|
||||||
|
if (this.destroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.count++;
|
this.count++;
|
||||||
|
|
||||||
let position;
|
let position;
|
||||||
@ -45,11 +49,14 @@ export class FastVertexArray {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.sprites[position] = sprite;
|
this.sprites[position] = sprite;
|
||||||
return position;
|
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
@ -61,20 +68,26 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearData(index: number): void {
|
private 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
expand(): void {
|
private 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);
|
||||||
@ -82,7 +95,7 @@ export class FastVertexArray {
|
|||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
compact(): void {
|
private 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) {
|
||||||
@ -110,4 +123,13 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,7 +116,7 @@ export class BlockViewComponent implements OnInit, OnDestroy {
|
|||||||
this.isLoadingBlock = false;
|
this.isLoadingBlock = false;
|
||||||
this.isLoadingOverview = true;
|
this.isLoadingOverview = true;
|
||||||
}),
|
}),
|
||||||
shareReplay(1)
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
);
|
);
|
||||||
|
|
||||||
this.overviewSubscription = block$.pipe(
|
this.overviewSubscription = block$.pipe(
|
||||||
@ -176,5 +176,8 @@ export class BlockViewComponent implements OnInit, OnDestroy {
|
|||||||
if (this.queryParamsSubscription) {
|
if (this.queryParamsSubscription) {
|
||||||
this.queryParamsSubscription.unsubscribe();
|
this.queryParamsSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
if (this.blockGraph) {
|
||||||
|
this.blockGraph.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -117,7 +117,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
|||||||
this.openGraphService.waitOver('block-data-' + this.rawId);
|
this.openGraphService.waitOver('block-data-' + this.rawId);
|
||||||
}),
|
}),
|
||||||
throttleTime(50, asyncScheduler, { leading: true, trailing: true }),
|
throttleTime(50, asyncScheduler, { leading: true, trailing: true }),
|
||||||
shareReplay(1)
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
);
|
);
|
||||||
|
|
||||||
this.overviewSubscription = block$.pipe(
|
this.overviewSubscription = block$.pipe(
|
||||||
|
|||||||
@ -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, Router } from '@angular/router';
|
import { ActivatedRoute, ParamMap, Params, 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 } from 'rxjs/operators';
|
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter, take } 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,6 +68,7 @@ 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;
|
||||||
@ -80,8 +81,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;
|
||||||
|
|
||||||
@ -118,7 +119,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.setAuditAvailable(this.auditSupported);
|
this.setAuditAvailable(this.auditSupported);
|
||||||
|
|
||||||
if (this.auditSupported) {
|
if (this.auditSupported) {
|
||||||
this.isAuditEnabledFromParam().subscribe(auditParam => {
|
this.isAuditEnabledSubscription = this.isAuditEnabledFromParam().subscribe(auditParam => {
|
||||||
if (this.auditParamEnabled) {
|
if (this.auditParamEnabled) {
|
||||||
this.auditModeEnabled = auditParam;
|
this.auditModeEnabled = auditParam;
|
||||||
} else {
|
} else {
|
||||||
@ -281,7 +282,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
|
throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
|
||||||
shareReplay(1)
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
);
|
);
|
||||||
|
|
||||||
this.overviewSubscription = this.block$.pipe(
|
this.overviewSubscription = this.block$.pipe(
|
||||||
@ -363,6 +364,7 @@ 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 {
|
||||||
@ -414,6 +416,7 @@ 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();
|
||||||
@ -421,8 +424,16 @@ 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.priceSubscription?.unsubscribe();
|
this.auditPrefSubscription?.unsubscribe();
|
||||||
|
this.isAuditEnabledSubscription?.unsubscribe();
|
||||||
this.oobSubscription?.unsubscribe();
|
this.oobSubscription?.unsubscribe();
|
||||||
|
this.priceSubscription?.unsubscribe();
|
||||||
|
this.blockGraphProjected.forEach(graph => {
|
||||||
|
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
|
||||||
@ -733,19 +744,18 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
toggleAuditMode(): void {
|
toggleAuditMode(): void {
|
||||||
this.stateService.hideAudit.next(this.auditModeEnabled);
|
this.stateService.hideAudit.next(this.auditModeEnabled);
|
||||||
|
|
||||||
this.route.queryParams.subscribe(params => {
|
const queryParams = { ...this.currentQueryParams };
|
||||||
const queryParams = { ...params };
|
delete queryParams['audit'];
|
||||||
delete queryParams['audit'];
|
|
||||||
|
|
||||||
let newUrl = this.router.url.split('?')[0];
|
let newUrl = this.router.url.split('?')[0];
|
||||||
const queryString = new URLSearchParams(queryParams).toString();
|
const queryString = new URLSearchParams(queryParams).toString();
|
||||||
if (queryString) {
|
if (queryString) {
|
||||||
newUrl += '?' + queryString;
|
newUrl += '?' + queryString;
|
||||||
}
|
}
|
||||||
|
this.location.replaceState(newUrl);
|
||||||
this.location.replaceState(newUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// avoid duplicate subscriptions
|
||||||
|
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;
|
||||||
@ -762,7 +772,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
return this.route.queryParams.pipe(
|
return this.route.queryParams.pipe(
|
||||||
map(params => {
|
map(params => {
|
||||||
this.auditParamEnabled = 'audit' in params;
|
this.auditParamEnabled = 'audit' in params;
|
||||||
|
|
||||||
return this.auditParamEnabled ? !(params['audit'] === 'false') : true;
|
return this.auditParamEnabled ? !(params['audit'] === 'false') : true;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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}">
|
||||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [hideTimeSince]="true"></app-timestamp>
|
||||||
</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
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { WebsocketService } from '@app/services/websocket.service';
|
|||||||
import { SeoService } from '@app/services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { OpenGraphService } from '@app/services/opengraph.service';
|
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||||
|
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-blocks-list',
|
selector: 'app-blocks-list',
|
||||||
@ -49,6 +50,7 @@ export class BlocksList implements OnInit {
|
|||||||
private ogService: OpenGraphService,
|
private ogService: OpenGraphService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
@Inject(LOCALE_ID) private locale: string,
|
@Inject(LOCALE_ID) private locale: string,
|
||||||
) {
|
) {
|
||||||
this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool';
|
this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool';
|
||||||
@ -182,7 +184,7 @@ export class BlocksList implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pageChange(page: number): void {
|
pageChange(page: number): void {
|
||||||
this.router.navigate(['blocks', page]);
|
this.router.navigate([this.relativeUrlPipe.transform('/blocks/'), page]);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByBlock(index: number, block: BlockExtended): number {
|
trackByBlock(index: number, block: BlockExtended): number {
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
<ng-template [ngIf]="button" [ngIfElse]="btnLink">
|
<ng-template [ngIf]="button" [ngIfElse]="btnLink">
|
||||||
<button #btn [attr.data-clipboard-text]="text" [class]="class" type="button" [disabled]="text === ''">
|
<button [class]="class" type="button" [disabled]="text === ''" style="box-shadow: none;" (click)="copyText()">
|
||||||
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;top: -2px;left: 1px;">
|
<span style="position: relative;top: -2px;left: 1px;">
|
||||||
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
|
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
|
||||||
|
<span *ngIf="showMessage" class="copied-message" style="top: 29px; left: -23.5px;">{{ copiedMessage }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #btnLink>
|
<ng-template #btnLink>
|
||||||
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;">
|
<span style="position: relative;">
|
||||||
<button #btn class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" [attr.data-clipboard-text]="text">
|
<button class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" style="box-shadow: none;" (click)="copyText()">
|
||||||
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
|
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
|
||||||
</button>
|
</button>
|
||||||
|
<span *ngIf="showMessage" class="copied-message" style="top: 29px; left: -23.5px;">{{ copiedMessage }}</span>
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@ -7,7 +7,19 @@
|
|||||||
padding-left: 0.4rem;
|
padding-left: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
.copied-message {
|
||||||
position: relative;
|
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
|
||||||
left: -3px;
|
color: var(--fg);
|
||||||
}
|
font-family: sans-serif;
|
||||||
|
font-size: .8rem;
|
||||||
|
font-weight: 400;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: left;
|
||||||
|
padding: .6em .75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: absolute;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 .5rem 1rem -.5rem #000;
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: .9;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import { Component, ViewChild, ElementRef, AfterViewInit, Input, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||||
import * as ClipboardJS from 'clipboard';
|
|
||||||
import * as tlite from 'tlite';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-clipboard',
|
selector: 'app-clipboard',
|
||||||
@ -8,15 +6,14 @@ import * as tlite from 'tlite';
|
|||||||
styleUrls: ['./clipboard.component.scss'],
|
styleUrls: ['./clipboard.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ClipboardComponent implements AfterViewInit {
|
export class ClipboardComponent {
|
||||||
@ViewChild('btn') btn: ElementRef;
|
|
||||||
@ViewChild('buttonWrapper') buttonWrapper: ElementRef;
|
|
||||||
@Input() button = false;
|
@Input() button = false;
|
||||||
@Input() class = 'btn btn-secondary ml-1';
|
@Input() class = 'btn btn-secondary ml-1';
|
||||||
@Input() size: 'small' | 'normal' | 'large' = 'normal';
|
@Input() size: 'small' | 'normal' | 'large' = 'normal';
|
||||||
@Input() text: string;
|
@Input() text: string;
|
||||||
@Input() leftPadding = true;
|
@Input() leftPadding = true;
|
||||||
copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`;
|
copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`;
|
||||||
|
showMessage = false;
|
||||||
|
|
||||||
widths = {
|
widths = {
|
||||||
small: '10',
|
small: '10',
|
||||||
@ -24,22 +21,40 @@ export class ClipboardComponent implements AfterViewInit {
|
|||||||
large: '18',
|
large: '18',
|
||||||
};
|
};
|
||||||
|
|
||||||
clipboard: any;
|
constructor(
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
) { }
|
||||||
|
|
||||||
constructor() { }
|
async copyText() {
|
||||||
|
if (this.text && !this.showMessage) {
|
||||||
ngAfterViewInit() {
|
try {
|
||||||
this.clipboard = new ClipboardJS(this.btn.nativeElement);
|
await this.copyToClipboard(this.text);
|
||||||
this.clipboard.on('success', () => {
|
this.showMessage = true;
|
||||||
tlite.show(this.buttonWrapper.nativeElement);
|
this.cd.markForCheck();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tlite.hide(this.buttonWrapper.nativeElement);
|
this.showMessage = false;
|
||||||
}, 1000);
|
this.cd.markForCheck();
|
||||||
});
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Clipboard copy failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy() {
|
async copyToClipboard(text: string) {
|
||||||
this.clipboard.destroy();
|
if (navigator.clipboard) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
} else {
|
||||||
|
// Use the 'out of viewport hidden text area' trick on non-secure contexts
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = this.text;
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
textarea.setAttribute('readonly', 'true'); // Don't trigger keyboard on mobile
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
textarea.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -238,7 +238,7 @@
|
|||||||
<span> </span>
|
<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>
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [height]="graphHeight"></app-address-graph>
|
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [defaultFiat]="true" [height]="graphHeight"></app-address-graph>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -281,9 +281,11 @@
|
|||||||
<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">
|
||||||
<span class="title-link">
|
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/wallet/' + widget.props.wallet | relativeUrl]">
|
||||||
<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>
|
<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>
|
||||||
|
|||||||
@ -73,7 +73,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
|
|||||||
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
|
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
|
||||||
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
|
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
|
||||||
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
|
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
|
||||||
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
|
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'fake_scripthash', 'op_return'], gradient: 'fee' },
|
||||||
];
|
];
|
||||||
goggleFlags = 0n;
|
goggleFlags = 0n;
|
||||||
goggleMode: FilterMode = 'and';
|
goggleMode: FilterMode = 'and';
|
||||||
|
|||||||
@ -162,6 +162,9 @@ 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 {
|
||||||
|
|||||||
@ -21,10 +21,8 @@
|
|||||||
<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>To use the faucet, please </span>
|
<span>To use the faucet, please </span>
|
||||||
<a routerLink="/login" [queryParams]="{'redirectTo': '/testnet4/faucet'}">login</a>
|
|
||||||
<span class="mr-2"> or</span>
|
|
||||||
</div>
|
</div>
|
||||||
<app-twitter-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Twitter"></app-twitter-login>
|
<app-github-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Github"></app-github-login>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@else if (user && user.status === 'pending' && !user.email && user.snsId) {
|
@else if (user && user.status === 'pending' && !user.email && user.snsId) {
|
||||||
@ -36,18 +34,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 Twitter account -->
|
<!-- User logged in but not a paid user or did not link its Github 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-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
|
<app-github-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Github"></app-github-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 Twitter account does not allow you to access the faucet</span>
|
<span class="mb-2 mr-2">Your account does not allow you to access the faucet</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
<a href="#" (click)="githubLogin()" [class]="(disabled ? 'disabled': '') + (customClass ? customClass : 'w-100 btn mt-1 d-flex justify-content-center align-items-center')" style="background-color: rgb(31, 35, 40)" [style]="width ? 'width: ' + width : ''">
|
||||||
|
<svg height="32" viewBox="0 0 18 16" width="32" style="fill: white;">
|
||||||
|
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="ml-2 text-light align-middle">{{ buttonString }}</span>
|
||||||
|
</a>
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { StateService } from '@app/services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { WebsocketService } from '@app/services/websocket.service';
|
import { WebsocketService } from '@app/services/websocket.service';
|
||||||
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
|
import { handleDemoRedirect } from '../../shared/common.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-graphs',
|
selector: 'app-graphs',
|
||||||
@ -13,7 +15,9 @@ export class GraphsComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private websocketService: WebsocketService
|
private websocketService: WebsocketService,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -22,5 +26,7 @@ export class GraphsComponent implements OnInit {
|
|||||||
if (this.stateService.env.ACCELERATOR === true && (this.stateService.env.MINING_DASHBOARD === true || this.stateService.env.LIGHTNING === true)) {
|
if (this.stateService.env.ACCELERATOR === true && (this.stateService.env.MINING_DASHBOARD === true || this.stateService.env.LIGHTNING === true)) {
|
||||||
this.flexWrap = true;
|
this.flexWrap = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDemoRedirect(this.route, this.router);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,8 +56,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
<td class="timestamp text-left">
|
<td class="timestamp text-left">
|
||||||
‎{{ utxo.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="utxo.blocktime"></app-timestamp>
|
||||||
<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>
|
||||||
|
|||||||
@ -53,8 +53,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
<td class="timestamp text-left">
|
<td class="timestamp text-left">
|
||||||
‎{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="peg.blocktime"></app-timestamp>
|
||||||
<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>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.sticky-loading {
|
.sticky-loading {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
z-index: 99;
|
z-index: 1000;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@media (width >= 992px) {
|
@media (width >= 992px) {
|
||||||
left: 32px;
|
left: 32px;
|
||||||
|
|||||||
@ -120,6 +120,7 @@ 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();
|
||||||
|
|||||||
@ -267,7 +267,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
if (event.key === prevKey) {
|
if (event.key === prevKey) {
|
||||||
if (this.mempoolBlocks[this.markIndex - 1]) {
|
if (this.mempoolBlocks[this.markIndex - 1]) {
|
||||||
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
|
this.router.navigate([this.relativeUrlPipe.transform('/mempool-block/'), this.markIndex - 1]);
|
||||||
} else {
|
} else {
|
||||||
const blocks = this.stateService.blocksSubject$.getValue();
|
const blocks = this.stateService.blocksSubject$.getValue();
|
||||||
for (const block of (blocks || [])) {
|
for (const block of (blocks || [])) {
|
||||||
|
|||||||
@ -90,9 +90,9 @@
|
|||||||
<th class="d-none d-md-table-cell" i18n="mining.rank">Rank</th>
|
<th class="d-none d-md-table-cell" i18n="mining.rank">Rank</th>
|
||||||
<th class=""></th>
|
<th class=""></th>
|
||||||
<th class="" i18n="mining.pool-name">Pool</th>
|
<th class="" i18n="mining.pool-name">Pool</th>
|
||||||
<th class="" *ngIf="this.miningWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th>
|
<th class="" *ngIf="['24h', '3d', '1w'].includes(this.miningWindowPreference)" i18n="mining.hashrate">Hashrate</th>
|
||||||
<th class="" i18n="master-page.blocks">Blocks</th>
|
<th class="" i18n="master-page.blocks">Blocks</th>
|
||||||
<th *ngIf="auditAvailable" class="health text-right widget" [ngClass]="{'health-column': this.miningWindowPreference === '24h'}" i18n="latest-blocks.avg_health"
|
<th *ngIf="auditAvailable" class="health text-right widget" [ngClass]="{'health-column': ['24h', '3d', '1w'].includes(this.miningWindowPreference)}" i18n="latest-blocks.avg_health"
|
||||||
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
|
i18n-ngbTooltip="latest-blocks.avg_health" ngbTooltip="Avg Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Avg Health</th>
|
||||||
<th *ngIf="auditAvailable" class="d-none d-sm-table-cell" i18n="mining.fees-per-block">Avg Block Fees</th>
|
<th *ngIf="auditAvailable" class="d-none d-sm-table-cell" i18n="mining.fees-per-block">Avg Block Fees</th>
|
||||||
<th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty Blocks</th>
|
<th class="d-none d-lg-table-cell" i18n="mining.empty-blocks">Empty Blocks</th>
|
||||||
@ -105,12 +105,13 @@
|
|||||||
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.onerror=null; this.src = '/resources/mining-pools/default.svg'">
|
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.onerror=null; this.src = '/resources/mining-pools/default.svg'">
|
||||||
</td>
|
</td>
|
||||||
<td class="pool-name"><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
|
<td class="pool-name"><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
|
||||||
<td class="" *ngIf="this.miningWindowPreference === '24h'">{{ pool.lastEstimatedHashrate | number: '1.2-2' }} {{
|
<td class="" *ngIf="'24h' === this.miningWindowPreference">{{ pool.lastEstimatedHashrate | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||||
miningStats.miningUnits.hashrateUnit }}</td>
|
<td class="" *ngIf="'3d' === this.miningWindowPreference">{{ pool.lastEstimatedHashrate3d | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||||
|
<td class="" *ngIf="'1w' === this.miningWindowPreference">{{ pool.lastEstimatedHashrate1w | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||||
<td class="d-flex justify-content-center">
|
<td class="d-flex justify-content-center">
|
||||||
{{ pool.blockCount }}<span class="d-none d-md-table-cell"> ({{ pool.share }}%)</span>
|
{{ pool.blockCount }}<span class="d-none d-md-table-cell"> ({{ pool.share }}%)</span>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable, 'health-column': this.miningWindowPreference === '24h'}">
|
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable, 'health-column': ['24h', '3d', '1w'].includes(this.miningWindowPreference)}">
|
||||||
<a
|
<a
|
||||||
class="health-badge badge"
|
class="health-badge badge"
|
||||||
[class.badge-success]="pool.avgMatchRate >= 99"
|
[class.badge-success]="pool.avgMatchRate >= 99"
|
||||||
@ -136,8 +137,9 @@
|
|||||||
<td class="d-none d-md-table-cell"></td>
|
<td class="d-none d-md-table-cell"></td>
|
||||||
<td class="text-right"></td>
|
<td class="text-right"></td>
|
||||||
<td class=""><b i18n="mining.all-miners">All miners</b></td>
|
<td class=""><b i18n="mining.all-miners">All miners</b></td>
|
||||||
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate | number: '1.2-2' }} {{
|
<td class="" *ngIf="'24h' === this.miningWindowPreference">{{ miningStats.lastEstimatedHashrate| number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||||
miningStats.miningUnits.hashrateUnit }}</b></td>
|
<td class="" *ngIf="'3d' === this.miningWindowPreference">{{ miningStats.lastEstimatedHashrate3d | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||||
|
<td class="" *ngIf="'1w' === this.miningWindowPreference">{{ miningStats.lastEstimatedHashrate1w | number: '1.2-2' }} {{ miningStats.miningUnits.hashrateUnit }}</td>
|
||||||
<td class=""><b>{{ miningStats.blockCount }}</b></td>
|
<td class=""><b>{{ miningStats.blockCount }}</b></td>
|
||||||
<td *ngIf="auditAvailable"></td>
|
<td *ngIf="auditAvailable"></td>
|
||||||
<td *ngIf="auditAvailable"></td>
|
<td *ngIf="auditAvailable"></td>
|
||||||
|
|||||||
@ -161,9 +161,12 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
borderColor: '#000',
|
borderColor: '#000',
|
||||||
formatter: () => {
|
formatter: () => {
|
||||||
const i = pool.blockCount.toString();
|
const i = pool.blockCount.toString();
|
||||||
if (this.miningWindowPreference === '24h') {
|
if (['24h', '3d', '1w'].includes(this.miningWindowPreference)) {
|
||||||
|
let hashrate = pool.lastEstimatedHashrate;
|
||||||
|
if ('3d' === this.miningWindowPreference) { hashrate = pool.lastEstimatedHashrate3d; }
|
||||||
|
if ('1w' === this.miningWindowPreference) { hashrate = pool.lastEstimatedHashrate1w; }
|
||||||
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||||
pool.lastEstimatedHashrate.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit +
|
hashrate.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit +
|
||||||
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
||||||
} else {
|
} else {
|
||||||
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||||
@ -200,13 +203,10 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
borderColor: '#000',
|
borderColor: '#000',
|
||||||
formatter: () => {
|
formatter: () => {
|
||||||
const i = totalBlockOther.toString();
|
const i = totalBlockOther.toString();
|
||||||
if (this.miningWindowPreference === '24h') {
|
if (['24h', '3d', '1w'].includes(this.miningWindowPreference)) {
|
||||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
|
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` + totalEstimatedHashrateOther.toFixed(2) + ' ' + miningStats.miningUnits.hashrateUnit + `<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
||||||
totalEstimatedHashrateOther.toString() + ' ' + miningStats.miningUnits.hashrateUnit +
|
|
||||||
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
|
||||||
} else {
|
} else {
|
||||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
|
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
||||||
$localize`${ i }:INTERPOLATION: blocks`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -292,6 +292,8 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
getEmptyMiningStat(): MiningStats {
|
getEmptyMiningStat(): MiningStats {
|
||||||
return {
|
return {
|
||||||
lastEstimatedHashrate: 0,
|
lastEstimatedHashrate: 0,
|
||||||
|
lastEstimatedHashrate3d: 0,
|
||||||
|
lastEstimatedHashrate1w: 0,
|
||||||
blockCount: 0,
|
blockCount: 0,
|
||||||
totalEmptyBlock: 0,
|
totalEmptyBlock: 0,
|
||||||
totalEmptyBlockRatio: '',
|
totalEmptyBlockRatio: '',
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
|
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box pool-details">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
@ -173,7 +173,123 @@
|
|||||||
<div class="spinner-border text-light"></div>
|
<div class="spinner-border text-light"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Stratum Job -->
|
||||||
|
<ng-container *ngIf="(job$ | async) as job;">
|
||||||
|
<h2 i18n="pool.next_block">Next block</h2>
|
||||||
|
<div class="box mb-3">
|
||||||
|
<div class="row" >
|
||||||
|
<div class="col">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="job-table table table-xs table-borderless table-fixed table-data">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="data-title clip text-center height" i18n="latest-blocks.height">Height</th>
|
||||||
|
<th class="data-title clip text-center expected" i18n="next-block.expected-time">Expected</th>
|
||||||
|
<th class="data-title clip text-center reward" i18n="latest-blocks.reward">Reward</th>
|
||||||
|
<th class="data-title clip text-center timestamp" i18n="next-block.timestamp">Timestamp</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center height">
|
||||||
|
{{ job.height }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center expected">
|
||||||
|
<ng-container *ngIf="(expectedBlockTime$ | async) as expectedBlockTime; else expectedPlaceholder">
|
||||||
|
<app-time kind="until" [time]="expectedBlockTime" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #expectedPlaceholder>~</ng-template>
|
||||||
|
</td>
|
||||||
|
<td class="text-center reward">
|
||||||
|
<app-amount [satoshis]="job.reward"></app-amount>
|
||||||
|
</td>
|
||||||
|
<td class="text-center timestamp">
|
||||||
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.timestamp" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="job-table table table-xs table-borderless table-fixed table-data">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="data-title clip text-center coinbase" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
|
||||||
|
<th class="data-title clip text-center clean" i18n="next-block.clean">Clean</th>
|
||||||
|
<th class="data-title clip text-center prevhash" i18n="next-block.prevhash">Prevhash</th>
|
||||||
|
<th class="data-title clip text-center job-received" i18n="next-block.job-received">Job Received</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center coinbase">
|
||||||
|
{{ job.scriptsig | hex2ascii }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center clean">
|
||||||
|
@if (job.cleanJobs) {
|
||||||
|
<fa-icon [icon]="['fas', 'check-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
} @else {
|
||||||
|
<fa-icon [icon]="['fas', 'times-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="text-center prevhash">
|
||||||
|
<a [routerLink]="['/block' | relativeUrl, job.prevHash]">
|
||||||
|
<app-truncate [text]="job.prevHash" [lastChars]="8"></app-truncate>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-center job-received">
|
||||||
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.received / 1000" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="stratum-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="data-title clip text-center" [attr.colspan]="Math.max(job.merkleBranches.length, 12)">
|
||||||
|
<a class="title-link" href="" [routerLink]="['/stratum' | relativeUrl]">
|
||||||
|
Merkle Branches
|
||||||
|
<span> </span>
|
||||||
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
@for (branch of job.merkleBranches; track $index) {
|
||||||
|
@if ($index === 0 && branch) {
|
||||||
|
<a [routerLink]="['/tx' | relativeUrl, reverseHash(branch)]"><td class="merkle" [style.background-color]="branch ? '#' + branch.slice(0, 6) : ''"><fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 14px; color: white"></fa-icon></td></a>
|
||||||
|
} @else {
|
||||||
|
<td class="merkle" [style.background-color]="branch ? '#' + branch.slice(0, 6) : ''"></td>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@for (_ of [].constructor(Math.max(0, 12 - job.merkleBranches.length)); track $index) {
|
||||||
|
<td class="merkle empty-branch"></td>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<!-- Blocks list -->
|
<!-- 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">
|
||||||
@ -194,7 +310,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">
|
||||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [hideTimeSince]="true"></app-timestamp>
|
||||||
</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>
|
||||||
|
|||||||
@ -49,111 +49,110 @@ div.scrollable {
|
|||||||
max-height: 75px;
|
max-height: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.pool-details {
|
||||||
padding-bottom: 5px;
|
|
||||||
@media (min-width: 767.98px) {
|
@media (min-width: 767.98px) {
|
||||||
min-height: 187px;
|
min-height: 187px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
@media (min-width: 767.98px) {
|
@media (min-width: 767.98px) {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 767.98px) {
|
.label.addresses {
|
||||||
font-weight: bold;
|
vertical-align: top;
|
||||||
|
padding-top: 25px;
|
||||||
|
}
|
||||||
|
.addresses-data {
|
||||||
|
vertical-align: top;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.label.addresses {
|
|
||||||
vertical-align: top;
|
|
||||||
padding-top: 25px;
|
|
||||||
}
|
|
||||||
.addresses-data {
|
|
||||||
vertical-align: top;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data {
|
.data {
|
||||||
text-align: right;
|
|
||||||
padding-left: 5%;
|
|
||||||
@media (max-width: 992px) {
|
|
||||||
text-align: left;
|
|
||||||
padding-left: 12px;
|
|
||||||
}
|
|
||||||
@media (max-width: 450px) {
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
padding-left: 5%;
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
@media (max-width: 450px) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
background-color: var(--secondary);
|
background-color: var(--secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coinbase {
|
.coinbase {
|
||||||
width: 20%;
|
|
||||||
@media (max-width: 875px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.height {
|
|
||||||
width: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timestamp {
|
|
||||||
@media (max-width: 875px) {
|
|
||||||
padding-left: 50px;
|
|
||||||
}
|
|
||||||
@media (max-width: 685px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mined {
|
|
||||||
width: 13%;
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.txs {
|
|
||||||
padding-right: 40px;
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
@media (max-width: 875px) {
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
@media (max-width: 567px) {
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.size {
|
|
||||||
width: 12%;
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
width: 15%;
|
|
||||||
}
|
|
||||||
@media (max-width: 875px) {
|
|
||||||
width: 20%;
|
width: 20%;
|
||||||
|
@media (max-width: 875px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 650px) {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
@media (max-width: 450px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scriptmessage {
|
.height {
|
||||||
overflow: hidden;
|
width: 10%;
|
||||||
display: inline-block;
|
}
|
||||||
text-overflow: ellipsis;
|
|
||||||
vertical-align: middle;
|
.timestamp {
|
||||||
width: auto;
|
@media (max-width: 875px) {
|
||||||
text-align: left;
|
padding-left: 50px;
|
||||||
|
}
|
||||||
|
@media (max-width: 685px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mined {
|
||||||
|
width: 13%;
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.txs {
|
||||||
|
padding-right: 40px;
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
@media (max-width: 875px) {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
@media (max-width: 567px) {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.size {
|
||||||
|
width: 12%;
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
@media (max-width: 875px) {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
@media (max-width: 450px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scriptmessage {
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-loader {
|
.skeleton-loader {
|
||||||
@ -214,4 +213,56 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-table {
|
||||||
|
td, th {
|
||||||
|
width: 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
min-width: 25%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0.1rem 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.expected, .timestamp, .clean, .job-received {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
@ -10,6 +10,9 @@ import { selectPowerOfTen } from '@app/bitcoin.utils';
|
|||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { SeoService } from '@app/services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { StratumJob } from '../../interfaces/websocket.interface';
|
||||||
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
|
import { MiningService } from '../../services/mining.service';
|
||||||
|
|
||||||
interface AccelerationTotal {
|
interface AccelerationTotal {
|
||||||
cost: number,
|
cost: number,
|
||||||
@ -27,12 +30,16 @@ export class PoolComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
gfg = true;
|
gfg = true;
|
||||||
|
stratumEnabled = this.stateService.env.STRATUM_ENABLED;
|
||||||
|
|
||||||
formatNumber = formatNumber;
|
formatNumber = formatNumber;
|
||||||
|
Math = Math;
|
||||||
slugSubscription: Subscription;
|
slugSubscription: Subscription;
|
||||||
poolStats$: Observable<PoolStat>;
|
poolStats$: Observable<PoolStat>;
|
||||||
blocks$: Observable<BlockExtended[]>;
|
blocks$: Observable<BlockExtended[]>;
|
||||||
oobFees$: Observable<AccelerationTotal[]>;
|
oobFees$: Observable<AccelerationTotal[]>;
|
||||||
|
job$: Observable<StratumJob | null>;
|
||||||
|
expectedBlockTime$: Observable<number>;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
error: HttpErrorResponse | null = null;
|
error: HttpErrorResponse | null = null;
|
||||||
|
|
||||||
@ -53,6 +60,8 @@ export class PoolComponent implements OnInit {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
|
private websocketService: WebsocketService,
|
||||||
|
private miningService: MiningService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
) {
|
) {
|
||||||
this.auditAvailable = this.stateService.env.AUDIT;
|
this.auditAvailable = this.stateService.env.AUDIT;
|
||||||
@ -62,7 +71,7 @@ export class PoolComponent implements OnInit {
|
|||||||
this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
|
this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.blocks = [];
|
this.blocks = [];
|
||||||
this.chartOptions = {};
|
this.chartOptions = {};
|
||||||
this.slug = slug;
|
this.slug = slug;
|
||||||
this.initializeObservables();
|
this.initializeObservables();
|
||||||
});
|
});
|
||||||
@ -129,6 +138,31 @@ export class PoolComponent implements OnInit {
|
|||||||
}),
|
}),
|
||||||
filter(oob => oob.length === 3 && oob[2].count > 0)
|
filter(oob => oob.length === 3 && oob[2].count > 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.stratumEnabled) {
|
||||||
|
this.job$ = combineLatest([
|
||||||
|
this.poolStats$.pipe(
|
||||||
|
tap((poolStats) => {
|
||||||
|
this.websocketService.startTrackStratum(poolStats.pool.unique_id);
|
||||||
|
})
|
||||||
|
),
|
||||||
|
this.stateService.stratumJobs$
|
||||||
|
]).pipe(
|
||||||
|
map(([poolStats, jobs]) => {
|
||||||
|
return jobs[poolStats.pool.unique_id];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.expectedBlockTime$ = combineLatest([
|
||||||
|
this.miningService.getMiningStats('1w'),
|
||||||
|
this.poolStats$,
|
||||||
|
this.stateService.difficultyAdjustment$
|
||||||
|
]).pipe(
|
||||||
|
map(([miningStats, poolStat, da]) => {
|
||||||
|
return (da.timeAvg / ((poolStat.estimatedHashrate || 0) / (miningStats.lastEstimatedHashrate * 1_000_000_000_000_000_000))) + Date.now() + da.timeOffset;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareChartOptions(hashrate, share) {
|
prepareChartOptions(hashrate, share) {
|
||||||
@ -327,6 +361,10 @@ 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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<ng-container *ngIf="(hosts$ | async) as hosts">
|
<ng-container *ngIf="(hosts$ | async) as hosts">
|
||||||
<div class="status-panel">
|
<div class="status-panel">
|
||||||
<table class="status-table table table-borderless table-striped" *ngIf="(tip$ | async) as tip">
|
<table class="status-table table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="rank"></th>
|
<th class="rank"></th>
|
||||||
@ -19,6 +19,9 @@
|
|||||||
<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>
|
||||||
@ -27,7 +30,16 @@
|
|||||||
<td class="updated">{{ getLastUpdateSeconds(host) }}</td>
|
<td class="updated">{{ getLastUpdateSeconds(host) }}</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-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 < tip ? '🟧' : '✅')) }}</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>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-panel {
|
.status-panel {
|
||||||
max-width: 720px;
|
max-width: 1000px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
background: var(--box-bg);
|
background: var(--box-bg);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, ChangeDetectorRef } from '@angular/core';
|
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, ChangeDetectorRef } from '@angular/core';
|
||||||
import { WebsocketService } from '@app/services/websocket.service';
|
import { WebsocketService } from '@app/services/websocket.service';
|
||||||
import { Observable, Subject, map } from 'rxjs';
|
import { Observable, Subject, map, tap } from 'rxjs';
|
||||||
import { StateService } from '@app/services/state.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { HealthCheckHost } from '@interfaces/websocket.interface';
|
import { HealthCheckHost } from '@interfaces/websocket.interface';
|
||||||
import { DomSanitizer } from '@angular/platform-browser';
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
@ -13,7 +13,7 @@ import { DomSanitizer } from '@angular/platform-browser';
|
|||||||
})
|
})
|
||||||
export class ServerHealthComponent implements OnInit {
|
export class ServerHealthComponent implements OnInit {
|
||||||
hosts$: Observable<HealthCheckHost[]>;
|
hosts$: Observable<HealthCheckHost[]>;
|
||||||
tip$: Subject<number>;
|
maxHeight: number;
|
||||||
interval: number;
|
interval: number;
|
||||||
now: number = Date.now();
|
now: number = Date.now();
|
||||||
|
|
||||||
@ -44,9 +44,14 @@ export class ServerHealthComponent implements OnInit {
|
|||||||
host.flag = this.parseFlag(host.host);
|
host.flag = this.parseFlag(host.host);
|
||||||
}
|
}
|
||||||
return hosts;
|
return hosts;
|
||||||
|
}),
|
||||||
|
tap(hosts => {
|
||||||
|
let newMaxHeight = 0;
|
||||||
|
for (const host of hosts) {
|
||||||
|
newMaxHeight = Math.max(newMaxHeight, host.latestHeight);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.tip$ = this.stateService.chainTip$;
|
|
||||||
this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']);
|
this.websocketService.want(['mempool-blocks', 'stats', 'blocks', 'tomahawk']);
|
||||||
|
|
||||||
this.interval = window.setInterval(() => {
|
this.interval = window.setInterval(() => {
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { Subscription } from 'rxjs';
|
|||||||
import { MarkBlockState, StateService } from '@app/services/state.service';
|
import { MarkBlockState, StateService } from '@app/services/state.service';
|
||||||
import { specialBlocks } from '@app/app.constants';
|
import { specialBlocks } from '@app/app.constants';
|
||||||
import { BlockExtended } from '@interfaces/node-api.interface';
|
import { BlockExtended } from '@interfaces/node-api.interface';
|
||||||
import { Router } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { sleep$ } from '@app/shared/common.utils';
|
import { handleDemoRedirect } from '../../shared/common.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-start',
|
selector: 'app-start',
|
||||||
@ -63,7 +63,8 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private router: Router
|
private router: Router,
|
||||||
|
private route: ActivatedRoute
|
||||||
) {
|
) {
|
||||||
this.isiOS = ['iPhone','iPod','iPad'].includes((navigator as any)?.userAgentData?.platform || navigator.platform);
|
this.isiOS = ['iPhone','iPod','iPad'].includes((navigator as any)?.userAgentData?.platform || navigator.platform);
|
||||||
if (this.stateService.network === '') {
|
if (this.stateService.network === '') {
|
||||||
@ -71,26 +72,8 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async demoMode() {
|
|
||||||
// @ts-ignore
|
|
||||||
if (!window.demoMode) {
|
|
||||||
// @ts-ignore
|
|
||||||
window.demoMode = true;
|
|
||||||
const paths = ['', 'acceleration', 'mining', 'lightning'];
|
|
||||||
let i = 0;
|
|
||||||
while ('Bitcoin is still alive') {
|
|
||||||
i = (i + 1) % paths.length;
|
|
||||||
this.router.navigateByUrl(paths[i]);
|
|
||||||
await sleep$(30000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// @ts-ignore
|
handleDemoRedirect(this.route, this.router);
|
||||||
if (window.location.search === '?demo=1') {
|
|
||||||
this.demoMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
||||||
this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
|
this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
@ -0,0 +1,230 @@
|
|||||||
|
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 '│';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
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 : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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">
|
||||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="tx.status.block_time" [hideTimeSince]="true"></app-timestamp>
|
||||||
<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>
|
||||||
|
|||||||
@ -61,10 +61,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td i18n="block.timestamp">Timestamp</td>
|
<td i18n="block.timestamp">Timestamp</td>
|
||||||
<td>
|
<td>
|
||||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="tx.status.block_time"></app-timestamp>
|
||||||
<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 {
|
||||||
@ -217,10 +214,10 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||||
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||||
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
|
@if (isAcceleration && accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
|
||||||
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sats">sats</span>
|
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sats">sats</span>
|
||||||
}
|
}
|
||||||
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
|
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + (isAcceleration ? ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0) : 0)"></app-fiat></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @else {
|
} @else {
|
||||||
@ -247,7 +244,7 @@
|
|||||||
|
|
||||||
<ng-template #effectiveRateRow>
|
<ng-template #effectiveRateRow>
|
||||||
@if (!isLoadingTx) {
|
@if (!isLoadingTx) {
|
||||||
@if ((cpfpInfo && hasEffectiveFeeRate) || accelerationInfo) {
|
@if ((cpfpInfo && hasEffectiveFeeRate) || (accelerationInfo && isAcceleration)) {
|
||||||
<tr>
|
<tr>
|
||||||
@if (isAcceleration) {
|
@if (isAcceleration) {
|
||||||
<td i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
|
<td i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
|
||||||
@ -267,7 +264,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (hasCpfp) {
|
@if (hasCpfp) {
|
||||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="toggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="toggleCpfp()">CPFP</button>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -280,7 +277,7 @@
|
|||||||
<ng-template #acceleratingRow>
|
<ng-template #acceleratingRow>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="2" colspan="2" style="padding: 0;">
|
<td rowspan="2" colspan="2" style="padding: 0;">
|
||||||
<app-active-acceleration-box [acceleratedBy]="tx.acceleratedBy" [effectiveFeeRate]="tx.effectiveFeePerVsize" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="showCpfpDetails = !showCpfpDetails" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
|
<app-active-acceleration-box [acceleratedBy]="tx.acceleratedBy" [effectiveFeeRate]="tx.effectiveFeePerVsize" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="toggleCpfp()" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr></tr>
|
<tr></tr>
|
||||||
|
|||||||
@ -29,7 +29,6 @@ export class TransactionDetailsComponent implements OnInit {
|
|||||||
@Input() hasEffectiveFeeRate: boolean;
|
@Input() hasEffectiveFeeRate: boolean;
|
||||||
@Input() cpfpInfo: CpfpInfo;
|
@Input() cpfpInfo: CpfpInfo;
|
||||||
@Input() hasCpfp: boolean;
|
@Input() hasCpfp: boolean;
|
||||||
@Input() showCpfpDetails: boolean;
|
|
||||||
@Input() accelerationInfo: Acceleration;
|
@Input() accelerationInfo: Acceleration;
|
||||||
@Input() acceleratorAvailable: boolean;
|
@Input() acceleratorAvailable: boolean;
|
||||||
@Input() accelerateCtaType: string;
|
@Input() accelerateCtaType: string;
|
||||||
@ -51,7 +50,7 @@ export class TransactionDetailsComponent implements OnInit {
|
|||||||
this.accelerateClicked.emit(true);
|
this.accelerateClicked.emit(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCpfp(): void {
|
toggleCpfp(): void {
|
||||||
this.toggleCpfp$.emit();
|
this.toggleCpfp$.emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
[height]="tx?.status?.block_height"
|
[height]="tx?.status?.block_height"
|
||||||
[replaced]="replaced"
|
[replaced]="replaced"
|
||||||
[removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed"
|
[removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed"
|
||||||
|
[cached]="isCached"
|
||||||
></app-confirmations>
|
></app-confirmations>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -52,7 +53,6 @@
|
|||||||
[hasEffectiveFeeRate]="hasEffectiveFeeRate"
|
[hasEffectiveFeeRate]="hasEffectiveFeeRate"
|
||||||
[cpfpInfo]="cpfpInfo"
|
[cpfpInfo]="cpfpInfo"
|
||||||
[hasCpfp]="hasCpfp"
|
[hasCpfp]="hasCpfp"
|
||||||
[showCpfpDetails]="showCpfpDetails"
|
|
||||||
[accelerationInfo]="accelerationInfo"
|
[accelerationInfo]="accelerationInfo"
|
||||||
[replaced]="replaced"
|
[replaced]="replaced"
|
||||||
[isCached]="isCached"
|
[isCached]="isCached"
|
||||||
@ -69,7 +69,9 @@
|
|||||||
<!-- CPFP Details -->
|
<!-- CPFP Details -->
|
||||||
<ng-template [ngIf]="showCpfpDetails">
|
<ng-template [ngIf]="showCpfpDetails">
|
||||||
<br>
|
<br>
|
||||||
<h2 class="text-left">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="xs"></fa-icon></h2>
|
<div class="title">
|
||||||
|
<h2 class="text-left" i18n="transaction.related-transactions|CPFP List">Related Transactions</h2>
|
||||||
|
</div>
|
||||||
<div class="box cpfp-details">
|
<div class="box cpfp-details">
|
||||||
<table class="table table-fixed table-borderless table-striped">
|
<table class="table table-fixed table-borderless table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
@ -164,12 +166,12 @@
|
|||||||
<br>
|
<br>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="transactionTime > 0 && tx.acceleratedAt > 0 && isAcceleration">
|
<ng-container *ngIf="transactionTime > 0 && tx.acceleratedAt > 0 && (isAcceleration || accelerationCanceled)">
|
||||||
<div class="title float-left">
|
<div class="title float-left">
|
||||||
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
|
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
<app-acceleration-timeline [transactionTime]="transactionTime" [acceleratedAt]="tx.acceleratedAt" [tx]="tx" [accelerationInfo]="accelerationInfo" [eta]="(ETA$ | async)"></app-acceleration-timeline>
|
<app-acceleration-timeline [transactionTime]="transactionTime" [acceleratedAt]="tx.acceleratedAt" [tx]="tx" [accelerationInfo]="accelerationInfo" [eta]="(ETA$ | async)" [canceled]="accelerationCanceled"></app-acceleration-timeline>
|
||||||
<br>
|
<br>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|||||||
@ -66,10 +66,6 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-small-height {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-green {
|
.arrow-green {
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,6 +107,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
pool: Pool | null;
|
pool: Pool | null;
|
||||||
auditStatus: TxAuditStatus | null;
|
auditStatus: TxAuditStatus | null;
|
||||||
isAcceleration: boolean = false;
|
isAcceleration: boolean = false;
|
||||||
|
accelerationCanceled: boolean = false;
|
||||||
filters: Filter[] = [];
|
filters: Filter[] = [];
|
||||||
showCpfpDetails = false;
|
showCpfpDetails = false;
|
||||||
miningStats: MiningStats;
|
miningStats: MiningStats;
|
||||||
@ -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?.length && transactionTimes[0] > 0 && !this.tx.status?.confirmed),
|
filter((transactionTimes) => transactionTimes?.[0] > 0 || this.tx.status?.confirmed),
|
||||||
take(1),
|
take(1),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
@ -360,16 +361,17 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
).subscribe((accelerationHistory) => {
|
).subscribe((accelerationHistory) => {
|
||||||
for (const acceleration of accelerationHistory) {
|
for (const acceleration of accelerationHistory) {
|
||||||
if (acceleration.txid === this.txId) {
|
if (acceleration.txid === this.txId) {
|
||||||
if (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') {
|
if ((acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
|
||||||
if (acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
|
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
||||||
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||||
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
acceleration.boost = boostCost;
|
||||||
acceleration.boost = boostCost;
|
this.tx.acceleratedAt = acceleration.added;
|
||||||
this.tx.acceleratedAt = acceleration.added;
|
this.accelerationInfo = acceleration;
|
||||||
this.accelerationInfo = acceleration;
|
}
|
||||||
} else {
|
if (acceleration.status === 'failed' || acceleration.status === 'failed_provisional') {
|
||||||
this.tx.feeDelta = undefined;
|
this.accelerationCanceled = true;
|
||||||
}
|
this.tx.acceleratedAt = acceleration.added;
|
||||||
|
this.accelerationInfo = acceleration;
|
||||||
}
|
}
|
||||||
this.waitingForAccelerationInfo = false;
|
this.waitingForAccelerationInfo = false;
|
||||||
this.setIsAccelerated();
|
this.setIsAccelerated();
|
||||||
@ -406,6 +408,30 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
const auditAvailable = this.isAuditAvailable(height);
|
const auditAvailable = this.isAuditAvailable(height);
|
||||||
const isCoinbase = this.tx.vin.some(v => v.is_coinbase);
|
const isCoinbase = this.tx.vin.some(v => v.is_coinbase);
|
||||||
const fetchAudit = auditAvailable && !isCoinbase;
|
const fetchAudit = auditAvailable && !isCoinbase;
|
||||||
|
|
||||||
|
const addFirstSeen = (audit: TxAuditStatus | null, hash: string, height: number, txid: string, useFullSummary: boolean) => {
|
||||||
|
if (
|
||||||
|
this.isFirstSeenAvailable(height)
|
||||||
|
&& !audit?.firstSeen // firstSeen is not already in audit
|
||||||
|
&& (!audit || audit?.seen) // audit is disabled or tx is already seen (meaning 'firstSeen' is in block summary)
|
||||||
|
) {
|
||||||
|
return useFullSummary ?
|
||||||
|
this.apiService.getStrippedBlockTransactions$(hash).pipe(
|
||||||
|
map(strippedTxs => {
|
||||||
|
return { audit, firstSeen: strippedTxs.find(tx => tx.txid === txid)?.time };
|
||||||
|
}),
|
||||||
|
catchError(() => of({ audit }))
|
||||||
|
) :
|
||||||
|
this.apiService.getStrippedBlockTransaction$(hash, txid).pipe(
|
||||||
|
map(strippedTx => {
|
||||||
|
return { audit, firstSeen: strippedTx?.time };
|
||||||
|
}),
|
||||||
|
catchError(() => of({ audit }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return of({ audit });
|
||||||
|
};
|
||||||
|
|
||||||
if (fetchAudit) {
|
if (fetchAudit) {
|
||||||
// If block audit is already cached, use it to get transaction audit
|
// If block audit is already cached, use it to get transaction audit
|
||||||
const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash);
|
const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash);
|
||||||
@ -428,24 +454,31 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
accelerated: isAccelerated,
|
accelerated: isAccelerated,
|
||||||
firstSeen,
|
firstSeen,
|
||||||
};
|
};
|
||||||
|
}),
|
||||||
|
switchMap(audit => addFirstSeen(audit, hash, height, txid, true)),
|
||||||
|
catchError(() => {
|
||||||
|
return of({ audit: null });
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return this.apiService.getBlockTxAudit$(hash, txid).pipe(
|
return this.apiService.getBlockTxAudit$(hash, txid).pipe(
|
||||||
retry({ count: 3, delay: 2000 }),
|
retry({ count: 3, delay: 2000 }),
|
||||||
|
switchMap(audit => addFirstSeen(audit, hash, height, txid, false)),
|
||||||
catchError(() => {
|
catchError(() => {
|
||||||
return of(null);
|
return of({ audit: null });
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return of(isCoinbase ? { coinbase: true } : null);
|
const audit = isCoinbase ? { coinbase: true } : null;
|
||||||
|
return addFirstSeen(audit, hash, height, txid, this.apiService.getBlockSummaryLoaded(hash));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).subscribe(auditStatus => {
|
).subscribe(auditStatus => {
|
||||||
this.auditStatus = auditStatus;
|
this.auditStatus = auditStatus?.audit;
|
||||||
if (this.auditStatus?.firstSeen) {
|
const firstSeen = this.auditStatus?.firstSeen || auditStatus['firstSeen'];
|
||||||
this.transactionTime = this.auditStatus.firstSeen;
|
if (firstSeen) {
|
||||||
|
this.transactionTime = firstSeen;
|
||||||
}
|
}
|
||||||
this.setIsAccelerated();
|
this.setIsAccelerated();
|
||||||
});
|
});
|
||||||
@ -847,6 +880,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
||||||
this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
|
this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
|
||||||
this.tx.feeDelta = cpfpInfo.feeDelta;
|
this.tx.feeDelta = cpfpInfo.feeDelta;
|
||||||
|
this.accelerationCanceled = false;
|
||||||
|
this.setIsAccelerated(firstCpfp);
|
||||||
|
} else if (cpfpInfo.acceleratedAt) { // Acceleration was cancelled: reset acceleration state
|
||||||
|
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
||||||
|
this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
|
||||||
|
this.tx.feeDelta = cpfpInfo.feeDelta;
|
||||||
|
this.accelerationCanceled = true;
|
||||||
this.setIsAccelerated(firstCpfp);
|
this.setIsAccelerated(firstCpfp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -870,7 +910,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsAccelerated(initialState: boolean = false) {
|
setIsAccelerated(initialState: boolean = false) {
|
||||||
this.isAcceleration = ((this.tx.acceleration && (!this.tx.status.confirmed || this.waitingForAccelerationInfo)) || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
|
this.isAcceleration =
|
||||||
|
(
|
||||||
|
(this.tx.acceleration && (!this.tx.status.confirmed || this.waitingForAccelerationInfo)) ||
|
||||||
|
(this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id)))
|
||||||
|
) &&
|
||||||
|
!this.accelerationCanceled;
|
||||||
if (this.isAcceleration) {
|
if (this.isAcceleration) {
|
||||||
if (initialState) {
|
if (initialState) {
|
||||||
this.accelerationFlowCompleted = true;
|
this.accelerationFlowCompleted = true;
|
||||||
@ -922,6 +967,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'testnet4':
|
||||||
|
if (blockHeight < this.stateService.env.TESTNET4_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'signet':
|
case 'signet':
|
||||||
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
|
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
return false;
|
return false;
|
||||||
@ -935,6 +985,34 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFirstSeenAvailable(blockHeight: number): boolean {
|
||||||
|
if (this.stateService.env.BASE_MODULE !== 'mempool') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (this.stateService.network) {
|
||||||
|
case 'testnet':
|
||||||
|
if (this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET_TX_FIRST_SEEN_START_HEIGHT) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'testnet4':
|
||||||
|
if (this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.TESTNET4_TX_FIRST_SEEN_START_HEIGHT) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'signet':
|
||||||
|
if (this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.SIGNET_TX_FIRST_SEEN_START_HEIGHT) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT && blockHeight >= this.stateService.env.MAINNET_TX_FIRST_SEEN_START_HEIGHT) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
resetTransaction() {
|
resetTransaction() {
|
||||||
this.firstLoad = false;
|
this.firstLoad = false;
|
||||||
this.gotInitialPosition = false;
|
this.gotInitialPosition = false;
|
||||||
|
|||||||
@ -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">‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}</ng-template>
|
<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 && 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: vin?.prevout?.value > 1000000000 || vin.isInscription}">
|
<td class="text-right nowrap amount" [class]="{large: tx.largeInput}">
|
||||||
<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: vout?.value > 1000000000}">
|
<td class="text-right nowrap amount" [class]="{large: tx.largeOutput}">
|
||||||
<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>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user