Compare commits
101 Commits
orangesurf
...
knorrium/u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
541555051b | ||
|
|
5aeaa68259 | ||
|
|
e01898a4c5 | ||
|
|
0568a8c6c1 | ||
|
|
e53e810a55 | ||
|
|
e2c44b6c62 | ||
|
|
36b691e25b | ||
|
|
5f5e96984a | ||
|
|
4a14e8d921 | ||
|
|
4e735cc8b0 | ||
|
|
4520e3fdf2 | ||
|
|
390bbf1097 | ||
|
|
bd8c1efc8e | ||
|
|
8fbc497a58 | ||
|
|
003956fd16 | ||
|
|
227d99e990 | ||
|
|
3d1aacbd66 | ||
|
|
1098d2fe3c | ||
|
|
f59e95fcc8 | ||
|
|
7f6399093e | ||
|
|
e9e8b0c758 | ||
|
|
517a30d2b0 | ||
|
|
7e766cc28d | ||
|
|
34099e3861 | ||
|
|
671b5ea2f2 | ||
|
|
caa2d83247 | ||
|
|
703241acf0 | ||
|
|
6e8579363d | ||
|
|
b254be2f49 | ||
|
|
d6283c54ee | ||
|
|
9ba7172b5b | ||
|
|
cb4bf0611e | ||
|
|
3ea491ad13 | ||
|
|
eddd7344ad | ||
|
|
4ecf2eb679 | ||
|
|
34acbca4b9 | ||
|
|
8793fafa4c | ||
|
|
341da85c77 | ||
|
|
0d8f63feff | ||
|
|
e7af43efa2 | ||
|
|
f5a54ae62b | ||
|
|
4da145ff5c | ||
|
|
a898701830 | ||
|
|
aca2f2ec7d | ||
|
|
803b005880 | ||
|
|
204d54b189 | ||
|
|
c248544fe8 | ||
|
|
b65d00f289 | ||
|
|
f77dc68ec7 | ||
|
|
c4ec50b771 | ||
|
|
8529b99675 | ||
|
|
cd02d89235 | ||
|
|
4dcbccd9b2 | ||
|
|
6a4aeaf7ed | ||
|
|
6432f72664 | ||
|
|
f6ab2caaf9 | ||
|
|
0a255d7fe5 | ||
|
|
ca0a8aee49 | ||
|
|
4a4259fa7d | ||
|
|
f142b421f9 | ||
|
|
47cc58c610 | ||
|
|
c3686a5500 | ||
|
|
9fbbe4980d | ||
|
|
0611773647 | ||
|
|
7740908a4c | ||
|
|
68ea7c59f3 | ||
|
|
915f7a6c27 | ||
|
|
e18c572549 | ||
|
|
25133d8505 | ||
|
|
9f5666f410 | ||
|
|
6553344489 | ||
|
|
3c84505579 | ||
|
|
81315af206 | ||
|
|
6fa747b303 | ||
|
|
37ddc29c2c | ||
|
|
c66f028f12 | ||
|
|
a5c67b5ca1 | ||
|
|
f49152d09d | ||
|
|
464fabf137 | ||
|
|
ed28a24c8a | ||
|
|
ba1ee15286 | ||
|
|
ddcf745722 | ||
|
|
774c0b4f83 | ||
|
|
734d5f2461 | ||
|
|
24a76cafa4 | ||
|
|
348a12c4a1 | ||
|
|
67750bd166 | ||
|
|
e5ea7afbe2 | ||
|
|
4b56144e6e | ||
|
|
332099cc01 | ||
|
|
0a933d022c | ||
|
|
47044db043 | ||
|
|
01df22ef86 | ||
|
|
ad360db71f | ||
|
|
a2bc6f5bba | ||
|
|
9e5b7436d4 | ||
|
|
72ddb8c6a4 | ||
|
|
5a3ee725b8 | ||
|
|
cdc4a430cd | ||
|
|
7a8ae7c9a6 | ||
|
|
74b420c258 |
78
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
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/')"
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ["20", "21"]
|
node: ["20", "22"]
|
||||||
flavor: ["dev", "prod"]
|
flavor: ["dev", "prod"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
@@ -160,7 +160,7 @@ jobs:
|
|||||||
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/')"
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ["20", "21"]
|
node: ["20", "22"]
|
||||||
flavor: ["dev", "prod"]
|
flavor: ["dev", "prod"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
@@ -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:
|
||||||
@@ -273,7 +263,7 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
|
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
|
||||||
|
|
||||||
@@ -310,8 +300,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Unzip assets before building (src/resources)
|
- name: Unzip assets before building (src/resources)
|
||||||
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
|
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
|
||||||
|
|
||||||
|
# mempool
|
||||||
- name: Chrome browser tests (${{ matrix.module }})
|
- name: Chrome browser tests (${{ matrix.module }})
|
||||||
|
if: ${{ matrix.module == 'mempool' }}
|
||||||
uses: cypress-io/github-action@v5
|
uses: cypress-io/github-action@v5
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.event_name }}
|
tag: ${{ github.event_name }}
|
||||||
@@ -322,7 +314,9 @@ jobs:
|
|||||||
wait-on-timeout: 120
|
wait-on-timeout: 120
|
||||||
record: true
|
record: true
|
||||||
parallel: true
|
parallel: true
|
||||||
spec: ${{ matrix.spec }}
|
spec: |
|
||||||
|
cypress/e2e/mainnet/*.spec.ts
|
||||||
|
cypress/e2e/signet/*.spec.ts
|
||||||
group: Tests on Chrome (${{ matrix.module }})
|
group: Tests on Chrome (${{ matrix.module }})
|
||||||
browser: "chrome"
|
browser: "chrome"
|
||||||
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
|
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
|
||||||
@@ -332,6 +326,56 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
|
# liquid
|
||||||
|
- name: Chrome browser tests (${{ matrix.module }})
|
||||||
|
if: ${{ matrix.module == 'liquid' }}
|
||||||
|
uses: cypress-io/github-action@v5
|
||||||
|
with:
|
||||||
|
tag: ${{ github.event_name }}
|
||||||
|
working-directory: ${{ matrix.module }}/frontend
|
||||||
|
build: npm run config:defaults:${{ matrix.module }}
|
||||||
|
start: npm run start:local-staging
|
||||||
|
wait-on: "http://localhost:4200"
|
||||||
|
wait-on-timeout: 120
|
||||||
|
record: true
|
||||||
|
parallel: true
|
||||||
|
spec: |
|
||||||
|
cypress/e2e/liquid/liquid.spec.ts
|
||||||
|
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
|
||||||
|
group: Tests on Chrome (${{ matrix.module }})
|
||||||
|
browser: "chrome"
|
||||||
|
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
|
||||||
|
env:
|
||||||
|
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
|
# testnet
|
||||||
|
- name: Chrome browser tests (${{ matrix.module }})
|
||||||
|
if: ${{ matrix.module == 'testnet4' }}
|
||||||
|
uses: cypress-io/github-action@v5
|
||||||
|
with:
|
||||||
|
tag: ${{ github.event_name }}
|
||||||
|
working-directory: ${{ matrix.module }}/frontend
|
||||||
|
build: npm run config:defaults:mempool
|
||||||
|
start: npm run start:local-staging
|
||||||
|
wait-on: "http://localhost:4200"
|
||||||
|
wait-on-timeout: 120
|
||||||
|
record: true
|
||||||
|
parallel: true
|
||||||
|
spec: |
|
||||||
|
cypress/e2e/testnet4/*.spec.ts
|
||||||
|
group: Tests on Chrome (${{ matrix.module }})
|
||||||
|
browser: "chrome"
|
||||||
|
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
|
||||||
|
env:
|
||||||
|
CYPRESS_REROUTE_TESTNET: true
|
||||||
|
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
validate_docker_json:
|
validate_docker_json:
|
||||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
@@ -359,4 +403,4 @@ jobs:
|
|||||||
- name: Validate JSON syntax
|
- name: Validate JSON syntax
|
||||||
run: |
|
run: |
|
||||||
cat mempool-config.json | jq
|
cat mempool-config.json | jq
|
||||||
working-directory: docker/docker/backend
|
working-directory: docker/docker/backend
|
||||||
@@ -155,6 +155,10 @@
|
|||||||
"API": "https://mempool.space/api/v1/services",
|
"API": "https://mempool.space/api/v1/services",
|
||||||
"ACCELERATIONS": false
|
"ACCELERATIONS": false
|
||||||
},
|
},
|
||||||
|
"STRATUM": {
|
||||||
|
"ENABLED": false,
|
||||||
|
"API": "http://localhost:1234"
|
||||||
|
},
|
||||||
"FIAT_PRICE": {
|
"FIAT_PRICE": {
|
||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
"PAID": false,
|
"PAID": false,
|
||||||
|
|||||||
@@ -151,5 +151,9 @@
|
|||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
"PAID": false,
|
"PAID": false,
|
||||||
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
|
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
|
||||||
|
},
|
||||||
|
"STRATUM": {
|
||||||
|
"ENABLED": false,
|
||||||
|
"API": "http://localhost:1234"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,11 @@ describe('Mempool Backend Config', () => {
|
|||||||
PAID: false,
|
PAID: false,
|
||||||
API_KEY: '',
|
API_KEY: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(config.STRATUM).toStrictEqual({
|
||||||
|
ENABLED: false,
|
||||||
|
API: 'http://localhost:1234',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import logger from '../../logger';
|
|||||||
import bitcoinClient from './bitcoin-client';
|
import bitcoinClient from './bitcoin-client';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
|
|
||||||
|
const BLOCKHASH_REGEX = /^[a-f0-9]{64}$/i;
|
||||||
|
const TXID_REGEX = /^[a-f0-9]{64}$/i;
|
||||||
|
const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a set of routes used by the accelerator server
|
* Define a set of routes used by the accelerator server
|
||||||
* Those routes are not designed to be public
|
* Those routes are not designed to be public
|
||||||
@@ -10,7 +14,7 @@ import config from '../../config';
|
|||||||
class BitcoinBackendRoutes {
|
class BitcoinBackendRoutes {
|
||||||
private static tag = 'BitcoinBackendRoutes';
|
private static tag = 'BitcoinBackendRoutes';
|
||||||
|
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application): void {
|
||||||
app
|
app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
|
||||||
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
|
||||||
@@ -26,10 +30,10 @@ class BitcoinBackendRoutes {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 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');
|
||||||
@@ -40,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);
|
||||||
}
|
}
|
||||||
@@ -58,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);
|
||||||
@@ -76,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);
|
||||||
@@ -95,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);
|
||||||
@@ -123,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);
|
||||||
@@ -141,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);
|
||||||
@@ -160,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);
|
||||||
@@ -184,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);
|
||||||
@@ -213,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);
|
||||||
@@ -247,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
|
||||||
@@ -90,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,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);
|
||||||
@@ -212,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');
|
||||||
@@ -226,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,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);
|
||||||
@@ -307,36 +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) {
|
private async getStrippedBlockTransaction(req: Request, res: Response) {
|
||||||
|
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
|
||||||
|
handleError(req, res, 501, `Invalid block hash`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!TXID_REGEX.test(req.params.txid)) {
|
||||||
|
handleError(req, res, 501, `Invalid transaction ID`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
|
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
|
||||||
if (!transaction) {
|
if (!transaction) {
|
||||||
handleError(req, res, 404, `transaction not found in summary`);
|
handleError(req, res, 404, `Transaction not found in summary`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||||
res.json(transaction);
|
res.json(transaction);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : 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);
|
||||||
|
|
||||||
@@ -348,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,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);
|
||||||
|
|
||||||
@@ -510,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,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 = '';
|
||||||
@@ -556,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,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
|
||||||
@@ -583,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,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
|
||||||
@@ -607,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,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;
|
||||||
@@ -718,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,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) {
|
||||||
@@ -749,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,7 +461,7 @@ class MiningRoutes {
|
|||||||
}
|
}
|
||||||
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
|
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, res, 500, 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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ interface AddressTransactions {
|
|||||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||||
import { calculateMempoolTxCpfp } from './cpfp';
|
import { calculateMempoolTxCpfp } from './cpfp';
|
||||||
import { getRecentFirstSeen } from '../utils/file-read';
|
import { getRecentFirstSeen } from '../utils/file-read';
|
||||||
|
import stratumApi, { StratumJob } from './services/stratum';
|
||||||
|
|
||||||
// valid 'want' subscriptions
|
// valid 'want' subscriptions
|
||||||
const wantable = [
|
const wantable = [
|
||||||
@@ -403,6 +404,16 @@ class WebsocketHandler {
|
|||||||
delete client['track-mempool'];
|
delete client['track-mempool'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsedMessage && parsedMessage['track-stratum'] != null) {
|
||||||
|
if (parsedMessage['track-stratum']) {
|
||||||
|
const sub = parsedMessage['track-stratum'];
|
||||||
|
client['track-stratum'] = sub;
|
||||||
|
response['stratumJobs'] = this.socketData['stratumJobs'];
|
||||||
|
} else {
|
||||||
|
client['track-stratum'] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(response).length) {
|
if (Object.keys(response).length) {
|
||||||
client.send(this.serializeResponse(response));
|
client.send(this.serializeResponse(response));
|
||||||
}
|
}
|
||||||
@@ -1384,6 +1395,23 @@ class WebsocketHandler {
|
|||||||
await statistics.runStatistics();
|
await statistics.runStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public handleNewStratumJob(job: StratumJob): void {
|
||||||
|
this.updateSocketDataFields({ 'stratumJobs': stratumApi.getJobs() });
|
||||||
|
|
||||||
|
for (const server of this.webSocketServers) {
|
||||||
|
server.clients.forEach((client) => {
|
||||||
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (client['track-stratum'] && (client['track-stratum'] === 'all' || client['track-stratum'] === job.pool)) {
|
||||||
|
client.send(JSON.stringify({
|
||||||
|
'stratumJob': job
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// takes a dictionary of JSON serialized values
|
// takes a dictionary of JSON serialized values
|
||||||
// and zips it together into a valid JSON object
|
// and zips it together into a valid JSON object
|
||||||
private serializeResponse(response): string {
|
private serializeResponse(response): string {
|
||||||
|
|||||||
@@ -165,6 +165,10 @@ interface IConfig {
|
|||||||
WALLETS: {
|
WALLETS: {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
WALLETS: string[];
|
WALLETS: string[];
|
||||||
|
},
|
||||||
|
STRATUM: {
|
||||||
|
ENABLED: boolean;
|
||||||
|
API: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,6 +336,10 @@ const defaults: IConfig = {
|
|||||||
'ENABLED': false,
|
'ENABLED': false,
|
||||||
'WALLETS': [],
|
'WALLETS': [],
|
||||||
},
|
},
|
||||||
|
'STRATUM': {
|
||||||
|
'ENABLED': false,
|
||||||
|
'API': 'http://localhost:1234',
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class Config implements IConfig {
|
class Config implements IConfig {
|
||||||
@@ -354,6 +362,7 @@ class Config implements IConfig {
|
|||||||
REDIS: IConfig['REDIS'];
|
REDIS: IConfig['REDIS'];
|
||||||
FIAT_PRICE: IConfig['FIAT_PRICE'];
|
FIAT_PRICE: IConfig['FIAT_PRICE'];
|
||||||
WALLETS: IConfig['WALLETS'];
|
WALLETS: IConfig['WALLETS'];
|
||||||
|
STRATUM: IConfig['STRATUM'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const configs = this.merge(configFromFile, defaults);
|
const configs = this.merge(configFromFile, defaults);
|
||||||
@@ -376,6 +385,7 @@ class Config implements IConfig {
|
|||||||
this.REDIS = configs.REDIS;
|
this.REDIS = configs.REDIS;
|
||||||
this.FIAT_PRICE = configs.FIAT_PRICE;
|
this.FIAT_PRICE = configs.FIAT_PRICE;
|
||||||
this.WALLETS = configs.WALLETS;
|
this.WALLETS = configs.WALLETS;
|
||||||
|
this.STRATUM = configs.STRATUM;
|
||||||
}
|
}
|
||||||
|
|
||||||
merge = (...objects: object[]): IConfig => {
|
merge = (...objects: object[]): IConfig => {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import accelerationRoutes from './api/acceleration/acceleration.routes';
|
|||||||
import aboutRoutes from './api/about.routes';
|
import aboutRoutes from './api/about.routes';
|
||||||
import mempoolBlocks from './api/mempool-blocks';
|
import mempoolBlocks from './api/mempool-blocks';
|
||||||
import walletApi from './api/services/wallets';
|
import walletApi from './api/services/wallets';
|
||||||
|
import stratumApi from './api/services/stratum';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@@ -320,11 +321,16 @@ class Server {
|
|||||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||||
|
|
||||||
accelerationApi.connectWebsocket();
|
accelerationApi.connectWebsocket();
|
||||||
|
if (config.STRATUM.ENABLED) {
|
||||||
|
stratumApi.connectWebsocket();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setUpHttpApiRoutes(): void {
|
setUpHttpApiRoutes(): void {
|
||||||
bitcoinRoutes.initRoutes(this.app);
|
bitcoinRoutes.initRoutes(this.app);
|
||||||
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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"theme": "wiz",
|
|
||||||
"enterprise": "river",
|
|
||||||
"branding": {
|
|
||||||
"name": "river",
|
|
||||||
"title": "river",
|
|
||||||
"site_id": 22,
|
|
||||||
"header_img": "/resources/riverlogo.svg",
|
|
||||||
"footer_img": "/resources/riverlogo.svg"
|
|
||||||
},
|
|
||||||
"dashboard": {
|
|
||||||
"widgets": [
|
|
||||||
{
|
|
||||||
"component": "fees",
|
|
||||||
"mobileOrder": 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "walletBalance",
|
|
||||||
"mobileOrder": 1,
|
|
||||||
"props": {
|
|
||||||
"wallet": "RIVER"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "twitter",
|
|
||||||
"mobileOrder": 5,
|
|
||||||
"props": {
|
|
||||||
"handle": "River"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "wallet",
|
|
||||||
"mobileOrder": 2,
|
|
||||||
"props": {
|
|
||||||
"wallet": "RIVER",
|
|
||||||
"period": "all"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "blocks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": "walletTransactions",
|
|
||||||
"mobileOrder": 3,
|
|
||||||
"props": {
|
|
||||||
"wallet": "RIVER"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
331
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",
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
"echarts": "~5.5.0",
|
"echarts": "~5.5.0",
|
||||||
"esbuild": "^0.24.0",
|
"esbuild": "^0.24.0",
|
||||||
"lightweight-charts": "~3.8.0",
|
|
||||||
"ngx-echarts": "~17.2.0",
|
"ngx-echarts": "~17.2.0",
|
||||||
"ngx-infinite-scroll": "^17.0.0",
|
"ngx-infinite-scroll": "^17.0.0",
|
||||||
"qrcode": "1.5.1",
|
"qrcode": "1.5.1",
|
||||||
@@ -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",
|
||||||
@@ -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",
|
||||||
@@ -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"
|
||||||
},
|
},
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -87,7 +87,6 @@
|
|||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
"echarts": "~5.5.0",
|
"echarts": "~5.5.0",
|
||||||
"lightweight-charts": "~3.8.0",
|
|
||||||
"ngx-echarts": "~17.2.0",
|
"ngx-echarts": "~17.2.0",
|
||||||
"ngx-infinite-scroll": "^17.0.0",
|
"ngx-infinite-scroll": "^17.0.0",
|
||||||
"qrcode": "1.5.1",
|
"qrcode": "1.5.1",
|
||||||
@@ -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)' }
|
||||||
|
];
|
||||||
@@ -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>
|
||||||
@@ -484,6 +492,11 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@if (isTokenizing > 0) {
|
||||||
|
<div class="d-flex flex-row justify-content-center">
|
||||||
|
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
color: var(--green)
|
color: var(--green)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accelerate-checkout-inner {
|
||||||
|
&.input-disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.paymentMethod {
|
.paymentMethod {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: var(--secondary);
|
background-color: var(--secondary);
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
calculating = true;
|
calculating = true;
|
||||||
processing = false;
|
processing = false;
|
||||||
|
isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
|
||||||
|
isTokenizing = 0; // reference counter, 0 = false, >0 = true
|
||||||
selectedOption: 'wait' | 'accel';
|
selectedOption: 'wait' | 'accel';
|
||||||
cantPayReason = '';
|
cantPayReason = '';
|
||||||
quoteError = ''; // error fetching estimate or initial data
|
quoteError = ''; // error fetching estimate or initial data
|
||||||
@@ -154,7 +156,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.accelerateError = null;
|
this.accelerateError = null;
|
||||||
this.timePaid = 0;
|
this.timePaid = 0;
|
||||||
this.btcpayInvoiceFailed = false;
|
this.btcpayInvoiceFailed = false;
|
||||||
this.moveToStep('summary');
|
this.moveToStep('summary', true);
|
||||||
} else {
|
} else {
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
}
|
}
|
||||||
@@ -163,11 +165,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
|
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
|
||||||
this.moveToStep('processing');
|
this.moveToStep('processing', true);
|
||||||
this.insertSquare();
|
this.insertSquare();
|
||||||
this.setupSquare();
|
this.setupSquare();
|
||||||
} else {
|
} else {
|
||||||
this.moveToStep('summary');
|
this.moveToStep('summary', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
@@ -192,14 +194,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
if (changes.accelerating && this.accelerating) {
|
if (changes.accelerating && this.accelerating) {
|
||||||
if (this.step === 'processing' || this.step === 'paid') {
|
if (this.step === 'processing' || this.step === 'paid') {
|
||||||
this.moveToStep('success');
|
this.moveToStep('success', true);
|
||||||
} else { // Edge case where the transaction gets accelerated by someone else or on another session
|
} else { // Edge case where the transaction gets accelerated by someone else or on another session
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
moveToStep(step: CheckoutStep): void {
|
moveToStep(step: CheckoutStep, force: boolean = false): void {
|
||||||
|
if (this.isCheckoutLocked > 0 && !force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this._step = step;
|
this._step = step;
|
||||||
if (this.timeoutTimer) {
|
if (this.timeoutTimer) {
|
||||||
@@ -242,7 +247,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
closeModal(): void {
|
closeModal(): void {
|
||||||
this.completed.emit(true);
|
this.completed.emit(true);
|
||||||
this.moveToStep('summary');
|
this.moveToStep('summary', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -393,7 +398,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
this.showSuccess = true;
|
this.showSuccess = true;
|
||||||
this.estimateSubscription.unsubscribe();
|
this.estimateSubscription.unsubscribe();
|
||||||
this.moveToStep('paid');
|
this.moveToStep('paid', true);
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
@@ -503,56 +508,75 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.loadingApplePay = false;
|
this.loadingApplePay = false;
|
||||||
applePayButton.addEventListener('click', async event => {
|
applePayButton.addEventListener('click', async event => {
|
||||||
|
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const tokenResult = await this.applePay.tokenize();
|
try {
|
||||||
if (tokenResult?.status === 'OK') {
|
// lock the checkout UI and show a loading spinner until the square modals are finished
|
||||||
const card = tokenResult.details?.card;
|
this.isCheckoutLocked++;
|
||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
this.isTokenizing++;
|
||||||
console.error(`Cannot retreive payment card details`);
|
const tokenResult = await this.applePay.tokenize();
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
if (tokenResult?.status === 'OK') {
|
||||||
this.processing = false;
|
const card = tokenResult.details?.card;
|
||||||
return;
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
}
|
console.error(`Cannot retreive payment card details`);
|
||||||
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
this.servicesApiService.accelerateWithApplePay$(
|
|
||||||
this.tx.txid,
|
|
||||||
tokenResult.token,
|
|
||||||
cardTag,
|
|
||||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
|
||||||
costUSD
|
|
||||||
).subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
return;
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
|
||||||
if (this.applePay) {
|
|
||||||
this.applePay.destroy();
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
this.moveToStep('paid');
|
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
error: (response) => {
|
|
||||||
this.processing = false;
|
|
||||||
this.accelerateError = response.error;
|
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
|
||||||
setTimeout(() => {
|
|
||||||
// Reset everything by reloading the page :D, can be improved
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||||
} else {
|
// keep checkout in loading state until the acceleration request completes
|
||||||
this.processing = false;
|
this.isTokenizing++;
|
||||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
this.isCheckoutLocked++;
|
||||||
if (tokenResult.errors) {
|
this.servicesApiService.accelerateWithApplePay$(
|
||||||
errorMessage += ` and errors: ${JSON.stringify(
|
this.tx.txid,
|
||||||
tokenResult.errors,
|
tokenResult.token,
|
||||||
)}`;
|
cardTag,
|
||||||
|
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||||
|
costUSD
|
||||||
|
).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
|
if (this.applePay) {
|
||||||
|
this.applePay.destroy();
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
this.moveToStep('paid', true);
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
|
this.accelerateError = response.error;
|
||||||
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTokenizing--;
|
||||||
|
this.isCheckoutLocked--;
|
||||||
|
// Reset everything by reloading the page :D, can be improved
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
||||||
|
}, 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) {
|
||||||
@@ -602,56 +626,84 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.loadingGooglePay = false;
|
this.loadingGooglePay = false;
|
||||||
|
|
||||||
document.getElementById('google-pay-button').addEventListener('click', async event => {
|
document.getElementById('google-pay-button').addEventListener('click', async event => {
|
||||||
|
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const tokenResult = await this.googlePay.tokenize();
|
try {
|
||||||
if (tokenResult?.status === 'OK') {
|
// lock the checkout UI and show a loading spinner until the square modals are finished
|
||||||
const card = tokenResult.details?.card;
|
this.isCheckoutLocked++;
|
||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
this.isTokenizing++;
|
||||||
console.error(`Cannot retreive payment card details`);
|
const tokenResult = await this.googlePay.tokenize();
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
if (tokenResult?.status === 'OK') {
|
||||||
this.processing = false;
|
const card = tokenResult.details?.card;
|
||||||
return;
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
}
|
console.error(`Cannot retreive payment card details`);
|
||||||
const 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)}`,
|
|
||||||
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--;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -718,7 +770,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.cashAppPay.destroy();
|
this.cashAppPay.destroy();
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.moveToStep('paid');
|
this.moveToStep('paid', true);
|
||||||
if (window.history.replaceState) {
|
if (window.history.replaceState) {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
|
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
|
||||||
@@ -733,7 +785,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -743,6 +795,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
|
||||||
*/
|
*/
|
||||||
@@ -766,7 +844,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
this.estimateSubscription.unsubscribe();
|
this.estimateSubscription.unsubscribe();
|
||||||
this.moveToStep('paid');
|
this.moveToStep('paid', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggedIn(): boolean {
|
isLoggedIn(): boolean {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -478,25 +478,30 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extendSummary(summary) {
|
extendSummary(summary) {
|
||||||
let extendedSummary = summary.slice();
|
const extendedSummary = summary.slice();
|
||||||
|
|
||||||
// Add a point at today's date to make the graph end at the current time
|
// Add a point at today's date to make the graph end at the current time
|
||||||
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
|
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
|
||||||
extendedSummary.reverse();
|
|
||||||
|
|
||||||
let oneHour = 60 * 60;
|
let maxTime = Date.now() / 1000;
|
||||||
|
|
||||||
|
const oneHour = 60 * 60;
|
||||||
// Fill gaps longer than interval
|
// Fill gaps longer than interval
|
||||||
for (let i = 0; i < extendedSummary.length - 1; i++) {
|
for (let i = 0; i < extendedSummary.length - 1; i++) {
|
||||||
let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);
|
if (extendedSummary[i].time > maxTime) {
|
||||||
|
extendedSummary[i].time = maxTime - 30;
|
||||||
|
}
|
||||||
|
maxTime = extendedSummary[i].time;
|
||||||
|
const hours = Math.floor((extendedSummary[i].time - extendedSummary[i + 1].time) / oneHour);
|
||||||
if (hours > 1) {
|
if (hours > 1) {
|
||||||
for (let j = 1; j < hours; j++) {
|
for (let j = 1; j < hours; j++) {
|
||||||
let newTime = extendedSummary[i].time + oneHour * j;
|
const newTime = extendedSummary[i].time - oneHour * j;
|
||||||
extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
|
extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
|
||||||
}
|
}
|
||||||
i += hours - 1;
|
i += hours - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return extendedSummary.reverse();
|
return extendedSummary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
|
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box pool-details">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
@@ -173,7 +173,119 @@
|
|||||||
<div class="spinner-border text-light"></div>
|
<div class="spinner-border text-light"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Stratum Job -->
|
||||||
|
<ng-container *ngIf="(job$ | async) as job;">
|
||||||
|
<h2 i18n="pool.next_block">Next block</h2>
|
||||||
|
<div class="box mb-3">
|
||||||
|
<div class="row" >
|
||||||
|
<div class="col">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="job-table table table-xs table-borderless table-fixed table-data">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="data-title clip text-center height" i18n="latest-blocks.height">Height</th>
|
||||||
|
<th class="data-title clip text-center expected" i18n="next-block.expected-time">Expected</th>
|
||||||
|
<th class="data-title clip text-center reward" i18n="latest-blocks.reward">Reward</th>
|
||||||
|
<th class="data-title clip text-center timestamp" i18n="next-block.timestamp">Timestamp</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center height">
|
||||||
|
{{ job.height }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center expected">
|
||||||
|
<ng-container *ngIf="(expectedBlockTime$ | async) as expectedBlockTime; else expectedPlaceholder">
|
||||||
|
<app-time kind="until" [time]="expectedBlockTime" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #expectedPlaceholder>~</ng-template>
|
||||||
|
</td>
|
||||||
|
<td class="text-center reward">
|
||||||
|
<app-amount [satoshis]="job.reward"></app-amount>
|
||||||
|
</td>
|
||||||
|
<td class="text-center timestamp">
|
||||||
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.timestamp" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="job-table table table-xs table-borderless table-fixed table-data">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="data-title clip text-center coinbase" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
|
||||||
|
<th class="data-title clip text-center clean" i18n="next-block.clean">Clean</th>
|
||||||
|
<th class="data-title clip text-center prevhash" i18n="next-block.prevhash">Prevhash</th>
|
||||||
|
<th class="data-title clip text-center job-received" i18n="next-block.job-received">Job Received</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center coinbase">
|
||||||
|
{{ job.scriptsig | hex2ascii }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center clean">
|
||||||
|
@if (job.cleanJobs) {
|
||||||
|
<fa-icon [icon]="['fas', 'check-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
} @else {
|
||||||
|
<fa-icon [icon]="['fas', 'times-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="text-center prevhash">
|
||||||
|
<a [routerLink]="['/block' | relativeUrl, job.prevHash]">
|
||||||
|
<app-truncate [text]="job.prevHash" [lastChars]="8"></app-truncate>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-center job-received">
|
||||||
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.received / 1000" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="stratum-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="data-title clip text-center" [attr.colspan]="Math.max(job.merkleBranches.length, 12)">
|
||||||
|
<a class="title-link" href="" [routerLink]="['/stratum' | relativeUrl]">
|
||||||
|
Merkle Branches
|
||||||
|
<span> </span>
|
||||||
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
@for (branch of job.merkleBranches; track $index) {
|
||||||
|
<td class="merkle" [style.background-color]="branch ? '#' + branch.slice(0, 6) : ''"></td>
|
||||||
|
}
|
||||||
|
@for (_ of [].constructor(Math.max(0, 12 - job.merkleBranches.length)); track $index) {
|
||||||
|
<td class="merkle empty-branch"></td>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<!-- Blocks list -->
|
<!-- Blocks list -->
|
||||||
|
<h2 i18n="master-page.blocks">Blocks</h2>
|
||||||
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5"
|
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5"
|
||||||
[infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
|
[infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
|
||||||
<ng-container *ngIf="blocks$ | async as blocks; else skeleton">
|
<ng-container *ngIf="blocks$ | async as blocks; else skeleton">
|
||||||
@@ -194,7 +306,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,55 @@ div.scrollable {
|
|||||||
|
|
||||||
.taller-row {
|
.taller-row {
|
||||||
height: 75px;
|
height: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stratum-table {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.merkle {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-branch {
|
||||||
|
outline: solid 1px white;
|
||||||
|
outline-offset: -1px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(to top left, transparent, transparent 48%, white 49%, white 51%, transparent 52%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
position: relative;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-table {
|
||||||
|
td, th {
|
||||||
|
width: 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
min-width: 25%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0.1rem 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.expected, .timestamp, .clean, .job-received {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,9 @@ import { selectPowerOfTen } from '@app/bitcoin.utils';
|
|||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { SeoService } from '@app/services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { StratumJob } from '../../interfaces/websocket.interface';
|
||||||
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
|
import { MiningService } from '../../services/mining.service';
|
||||||
|
|
||||||
interface AccelerationTotal {
|
interface AccelerationTotal {
|
||||||
cost: number,
|
cost: number,
|
||||||
@@ -27,12 +30,16 @@ export class PoolComponent implements OnInit {
|
|||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
gfg = true;
|
gfg = true;
|
||||||
|
stratumEnabled = this.stateService.env.STRATUM_ENABLED;
|
||||||
|
|
||||||
formatNumber = formatNumber;
|
formatNumber = formatNumber;
|
||||||
|
Math = Math;
|
||||||
slugSubscription: Subscription;
|
slugSubscription: Subscription;
|
||||||
poolStats$: Observable<PoolStat>;
|
poolStats$: Observable<PoolStat>;
|
||||||
blocks$: Observable<BlockExtended[]>;
|
blocks$: Observable<BlockExtended[]>;
|
||||||
oobFees$: Observable<AccelerationTotal[]>;
|
oobFees$: Observable<AccelerationTotal[]>;
|
||||||
|
job$: Observable<StratumJob | null>;
|
||||||
|
expectedBlockTime$: Observable<number>;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
error: HttpErrorResponse | null = null;
|
error: HttpErrorResponse | null = null;
|
||||||
|
|
||||||
@@ -53,6 +60,8 @@ export class PoolComponent implements OnInit {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
|
private websocketService: WebsocketService,
|
||||||
|
private miningService: MiningService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
) {
|
) {
|
||||||
this.auditAvailable = this.stateService.env.AUDIT;
|
this.auditAvailable = this.stateService.env.AUDIT;
|
||||||
@@ -62,7 +71,7 @@ export class PoolComponent implements OnInit {
|
|||||||
this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
|
this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.blocks = [];
|
this.blocks = [];
|
||||||
this.chartOptions = {};
|
this.chartOptions = {};
|
||||||
this.slug = slug;
|
this.slug = slug;
|
||||||
this.initializeObservables();
|
this.initializeObservables();
|
||||||
});
|
});
|
||||||
@@ -129,6 +138,31 @@ export class PoolComponent implements OnInit {
|
|||||||
}),
|
}),
|
||||||
filter(oob => oob.length === 3 && oob[2].count > 0)
|
filter(oob => oob.length === 3 && oob[2].count > 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.stratumEnabled) {
|
||||||
|
this.job$ = combineLatest([
|
||||||
|
this.poolStats$.pipe(
|
||||||
|
tap((poolStats) => {
|
||||||
|
this.websocketService.startTrackStratum(poolStats.pool.unique_id);
|
||||||
|
})
|
||||||
|
),
|
||||||
|
this.stateService.stratumJobs$
|
||||||
|
]).pipe(
|
||||||
|
map(([poolStats, jobs]) => {
|
||||||
|
return jobs[poolStats.pool.unique_id];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.expectedBlockTime$ = combineLatest([
|
||||||
|
this.miningService.getMiningStats('1w'),
|
||||||
|
this.poolStats$,
|
||||||
|
this.stateService.difficultyAdjustment$
|
||||||
|
]).pipe(
|
||||||
|
map(([miningStats, poolStat, da]) => {
|
||||||
|
return (da.timeAvg / ((poolStat.estimatedHashrate || 0) / (miningStats.lastEstimatedHashrate * 1_000_000_000_000_000_000))) + Date.now() + da.timeOffset;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareChartOptions(hashrate, share) {
|
prepareChartOptions(hashrate, share) {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<div class="container-xl" style="min-height: 335px">
|
||||||
|
<h1 class="float-left" i18n="master-page.blocks">Stratum Jobs</h1>
|
||||||
|
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
|
<div style="min-height: 295px">
|
||||||
|
<table *ngIf="poolsReady && (rows$ | async) as rows;" class="stratum-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td class="height">Height</td>
|
||||||
|
<td class="reward">Reward</td>
|
||||||
|
<td class="tag">Coinbase Tag</td>
|
||||||
|
<td class="merkle" [attr.colspan]="rows[0]?.merkleCells?.length || 4">
|
||||||
|
Merkle Branches
|
||||||
|
</td>
|
||||||
|
<td class="pool">Pool</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (row of rows; track row.job.pool) {
|
||||||
|
<tr>
|
||||||
|
<td class="height">
|
||||||
|
{{ row.job.height }}
|
||||||
|
</td>
|
||||||
|
<td class="reward">
|
||||||
|
<app-amount [satoshis]="row.job.reward"></app-amount>
|
||||||
|
</td>
|
||||||
|
<td class="tag">
|
||||||
|
{{ row.job.tag }}
|
||||||
|
</td>
|
||||||
|
@for (cell of row.merkleCells; track $index) {
|
||||||
|
<td class="merkle" [style.background-color]="cell.hash ? '#' + cell.hash.slice(0, 6) : ''">
|
||||||
|
<div class="pipe-segment" [class]="pipeToClass(cell.type)"></div>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
<td class="pool">
|
||||||
|
@if (pools[row.job.pool]) {
|
||||||
|
<a class="badge" [routerLink]="[('/mining/pool/' + pools[row.job.pool].slug) | relativeUrl]">
|
||||||
|
<img class="pool-logo" [src]="'/resources/mining-pools/' + pools[row.job.pool].slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pools[row.job.pool].name + ' mining pool'">
|
||||||
|
{{ pools[row.job.pool].name}}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
.stratum-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
position: relative;
|
||||||
|
height: 2em;
|
||||||
|
|
||||||
|
&.height, &.reward, &.tag {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag {
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pool {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.merkle {
|
||||||
|
width: 100px;
|
||||||
|
.pipe-segment {
|
||||||
|
position: absolute;
|
||||||
|
border-color: white;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
border-left: solid 4px;
|
||||||
|
}
|
||||||
|
&.horizontal {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
border-top: solid 4px;
|
||||||
|
}
|
||||||
|
&.branch-top {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
border-top: solid 4px;
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: content-box;
|
||||||
|
top: -4px;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 50%;
|
||||||
|
border-top: solid 4px;
|
||||||
|
border-left: solid 4px;
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.branch-mid {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0px;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
border-left: solid 4px;
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: content-box;
|
||||||
|
top: -4px;
|
||||||
|
left: -4px;
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
border-bottom: solid 4px;
|
||||||
|
border-left: solid 4px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.branch-end {
|
||||||
|
top: -4px;
|
||||||
|
right: 0;
|
||||||
|
width: 50%;
|
||||||
|
height: 50%;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
border-bottom: solid 4px;
|
||||||
|
border-left: solid 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: relative;
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-logo {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import { Component, OnInit, ChangeDetectionStrategy, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||||
|
import { StateService } from '../../../services/state.service';
|
||||||
|
import { WebsocketService } from '../../../services/websocket.service';
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { StratumJob } from '../../../interfaces/websocket.interface';
|
||||||
|
import { MiningService } from '../../../services/mining.service';
|
||||||
|
import { SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||||
|
|
||||||
|
type MerkleCellType = ' ' | '┬' | '├' | '└' | '│' | '─' | 'leaf';
|
||||||
|
|
||||||
|
|
||||||
|
interface TaggedStratumJob extends StratumJob {
|
||||||
|
tag: string;
|
||||||
|
merkleBranchIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MerkleCell {
|
||||||
|
hash: string;
|
||||||
|
type: MerkleCellType;
|
||||||
|
job?: TaggedStratumJob;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MerkleTree {
|
||||||
|
hash?: string;
|
||||||
|
job: string;
|
||||||
|
size: number;
|
||||||
|
children?: MerkleTree[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PoolRow {
|
||||||
|
job: TaggedStratumJob;
|
||||||
|
merkleCells: MerkleCell[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTag(scriptSig: string): string {
|
||||||
|
const hex = scriptSig.slice(8).replace(/6d6d.{64}/, '');
|
||||||
|
const bytes: number[] = [];
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes.push(parseInt(hex.substr(i, 2), 16));
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const ascii = new TextDecoder('utf8').decode(Uint8Array.from(bytes)).replace(/\uFFFD/g, '').replace(/\\0/g, '').replace(/[\x00-\x1F\x7F-\x9F]/g, '');
|
||||||
|
if (ascii.includes('/ViaBTC/')) {
|
||||||
|
return '/ViaBTC/';
|
||||||
|
} else if (ascii.includes('SpiderPool/')) {
|
||||||
|
return 'SpiderPool/';
|
||||||
|
}
|
||||||
|
return (ascii.match(/\/.*\//)?.[0] || ascii).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMerkleBranchIds(merkleBranches: string[], numBranches: number): string[] {
|
||||||
|
let lastHash = '';
|
||||||
|
const ids: string[] = [];
|
||||||
|
for (let i = 0; i < numBranches; i++) {
|
||||||
|
if (merkleBranches[i]) {
|
||||||
|
lastHash = merkleBranches[i];
|
||||||
|
}
|
||||||
|
ids.push(`${i}-${lastHash}`);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-stratum-list',
|
||||||
|
templateUrl: './stratum-list.component.html',
|
||||||
|
styleUrls: ['./stratum-list.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class StratumList implements OnInit, OnDestroy {
|
||||||
|
rows$: Observable<PoolRow[]>;
|
||||||
|
pools: { [id: number]: SinglePoolStats } = {};
|
||||||
|
poolsReady: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private stateService: StateService,
|
||||||
|
private websocketService: WebsocketService,
|
||||||
|
private miningService: MiningService,
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.websocketService.want(['stats', 'blocks', 'mempool-blocks']);
|
||||||
|
this.miningService.getPools().subscribe(pools => {
|
||||||
|
this.pools = {};
|
||||||
|
for (const pool of pools) {
|
||||||
|
this.pools[pool.unique_id] = pool;
|
||||||
|
}
|
||||||
|
this.poolsReady = true;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
this.rows$ = this.stateService.stratumJobs$.pipe(
|
||||||
|
map((jobs) => this.processJobs(jobs)),
|
||||||
|
);
|
||||||
|
this.websocketService.startTrackStratum('all');
|
||||||
|
}
|
||||||
|
|
||||||
|
processJobs(rawJobs: Record<string, StratumJob>): PoolRow[] {
|
||||||
|
const numBranches = Math.max(...Object.values(rawJobs).map(job => job.merkleBranches.length));
|
||||||
|
const jobs: Record<string, TaggedStratumJob> = {};
|
||||||
|
for (const [id, job] of Object.entries(rawJobs)) {
|
||||||
|
jobs[id] = { ...job, tag: parseTag(job.scriptsig), merkleBranchIds: getMerkleBranchIds(job.merkleBranches, numBranches) };
|
||||||
|
}
|
||||||
|
if (Object.keys(jobs).length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let trees: MerkleTree[] = Object.keys(jobs).map(job => ({
|
||||||
|
job,
|
||||||
|
size: 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// build tree from bottom up
|
||||||
|
for (let col = numBranches - 1; col >= 0; col--) {
|
||||||
|
const groups: Record<string, MerkleTree[]> = {};
|
||||||
|
for (const tree of trees) {
|
||||||
|
const branchId = jobs[tree.job].merkleBranchIds[col];
|
||||||
|
if (!groups[branchId]) {
|
||||||
|
groups[branchId] = [];
|
||||||
|
}
|
||||||
|
groups[branchId].push(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
trees = Object.values(groups).map(group => ({
|
||||||
|
hash: jobs[group[0].job].merkleBranches[col],
|
||||||
|
job: group[0].job,
|
||||||
|
children: group,
|
||||||
|
size: group.reduce((acc, tree) => acc + tree.size, 0),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize grid of cells
|
||||||
|
const rows: (MerkleCell | null)[][] = [];
|
||||||
|
for (let i = 0; i < Object.keys(jobs).length; i++) {
|
||||||
|
const row: (MerkleCell | null)[] = [];
|
||||||
|
for (let j = 0; j <= numBranches; j++) {
|
||||||
|
row.push(null);
|
||||||
|
}
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill in the cells
|
||||||
|
let colTrees = [trees.sort((a, b) => {
|
||||||
|
if (a.size !== b.size) {
|
||||||
|
return b.size - a.size;
|
||||||
|
}
|
||||||
|
return a.job.localeCompare(b.job);
|
||||||
|
})];
|
||||||
|
for (let col = 0; col <= numBranches; col++) {
|
||||||
|
let row = 0;
|
||||||
|
const nextTrees: MerkleTree[][] = [];
|
||||||
|
for (let g = 0; g < colTrees.length; g++) {
|
||||||
|
for (let t = 0; t < colTrees[g].length; t++) {
|
||||||
|
const tree = colTrees[g][t];
|
||||||
|
const isFirstTree = (t === 0);
|
||||||
|
const isLastTree = (t === colTrees[g].length - 1);
|
||||||
|
for (let i = 0; i < tree.size; i++) {
|
||||||
|
const isFirstCell = (i === 0);
|
||||||
|
const isLeaf = (col === numBranches);
|
||||||
|
rows[row][col] = {
|
||||||
|
hash: tree.hash,
|
||||||
|
job: isLeaf ? jobs[tree.job] : undefined,
|
||||||
|
type: 'leaf',
|
||||||
|
};
|
||||||
|
if (col > 0) {
|
||||||
|
rows[row][col - 1].type = getCellType(isFirstCell, isFirstTree, isLastTree);
|
||||||
|
}
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
if (tree.children) {
|
||||||
|
nextTrees.push(tree.children.sort((a, b) => {
|
||||||
|
if (a.size !== b.size) {
|
||||||
|
return b.size - a.size;
|
||||||
|
}
|
||||||
|
return a.job.localeCompare(b.job);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colTrees = nextTrees;
|
||||||
|
}
|
||||||
|
return rows.map(row => ({
|
||||||
|
job: row[row.length - 1].job,
|
||||||
|
merkleCells: row.slice(0, -1),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeToClass(type: MerkleCellType): string {
|
||||||
|
return {
|
||||||
|
' ': 'empty',
|
||||||
|
'┬': 'branch-top',
|
||||||
|
'├': 'branch-mid',
|
||||||
|
'└': 'branch-end',
|
||||||
|
'│': 'vertical',
|
||||||
|
'─': 'horizontal',
|
||||||
|
'leaf': 'leaf'
|
||||||
|
}[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.websocketService.stopTrackStratum();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCellType(isFirstCell, isFirstTree, isLastTree): MerkleCellType {
|
||||||
|
if (isFirstCell) {
|
||||||
|
if (isFirstTree) {
|
||||||
|
if (isLastTree) {
|
||||||
|
return '─';
|
||||||
|
} else {
|
||||||
|
return '┬';
|
||||||
|
}
|
||||||
|
} else if (isLastTree) {
|
||||||
|
return '└';
|
||||||
|
} else {
|
||||||
|
return '├';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isLastTree) {
|
||||||
|
return ' ';
|
||||||
|
} else {
|
||||||
|
return '│';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -240,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),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -202,12 +202,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
for (const address of this.addresses) {
|
for (const address of this.addresses) {
|
||||||
switch (address.length) {
|
switch (address.length) {
|
||||||
case 130: {
|
case 130: {
|
||||||
if (v.scriptpubkey === '21' + address + 'ac') {
|
if (v.scriptpubkey === '41' + address + 'ac') {
|
||||||
return v.value;
|
return v.value;
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
case 66: {
|
case 66: {
|
||||||
if (v.scriptpubkey === '41' + address + 'ac') {
|
if (v.scriptpubkey === '21' + address + 'ac') {
|
||||||
return v.value;
|
return v.value;
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
@@ -224,12 +224,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
for (const address of this.addresses) {
|
for (const address of this.addresses) {
|
||||||
switch (address.length) {
|
switch (address.length) {
|
||||||
case 130: {
|
case 130: {
|
||||||
if (v.prevout?.scriptpubkey === '21' + address + 'ac') {
|
if (v.prevout?.scriptpubkey === '41' + address + 'ac') {
|
||||||
return v.prevout?.value;
|
return v.prevout?.value;
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
case 66: {
|
case 66: {
|
||||||
if (v.prevout?.scriptpubkey === '41' + address + 'ac') {
|
if (v.prevout?.scriptpubkey === '21' + address + 'ac') {
|
||||||
return v.prevout?.value;
|
return v.prevout?.value;
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
@@ -258,6 +258,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
const hasAnnex = tx.vin[i].witness?.[tx.vin[i].witness.length - 1].startsWith('50');
|
const hasAnnex = tx.vin[i].witness?.[tx.vin[i].witness.length - 1].startsWith('50');
|
||||||
if (tx.vin[i].witness.length > (hasAnnex ? 2 : 1) && tx.vin[i].witness[tx.vin[i].witness.length - (hasAnnex ? 3 : 2)].includes('0063036f7264')) {
|
if (tx.vin[i].witness.length > (hasAnnex ? 2 : 1) && tx.vin[i].witness[tx.vin[i].witness.length - (hasAnnex ? 3 : 2)].includes('0063036f7264')) {
|
||||||
tx.vin[i].isInscription = true;
|
tx.vin[i].isInscription = true;
|
||||||
|
tx.largeInput = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,6 +269,9 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tx.largeInput = tx.largeInput || tx.vin.some(vin => (vin?.prevout?.value > 1000000000));
|
||||||
|
tx.largeOutput = tx.vout.some(vout => (vout?.value > 1000000000));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.blockTime && this.transactions?.length && this.currency) {
|
if (this.blockTime && this.transactions?.length && this.currency) {
|
||||||
@@ -351,8 +355,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
this.electrsApiService.getTransaction$(tx.txid)
|
this.electrsApiService.getTransaction$(tx.txid)
|
||||||
.subscribe((newTx) => {
|
.subscribe((newTx) => {
|
||||||
tx['@vinLoaded'] = true;
|
tx['@vinLoaded'] = true;
|
||||||
|
let temp = tx.vin;
|
||||||
tx.vin = newTx.vin;
|
tx.vin = newTx.vin;
|
||||||
tx.fee = newTx.fee;
|
tx.fee = newTx.fee;
|
||||||
|
for (const [index, vin] of temp.entries()) {
|
||||||
|
newTx.vin[index].isInscription = vin.isInscription;
|
||||||
|
}
|
||||||
this.ref.markForCheck();
|
this.ref.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="container-xl" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
|
<div class="container-xl" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
|
||||||
<div class="title-address">
|
<div class="title-address">
|
||||||
<h1 i18n="shared.wallet">Wallet</h1>
|
<h1>{{ walletName }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
@@ -74,6 +74,36 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="title-tx">
|
||||||
|
<h2 class="text-left" i18n="address.transactions">Transactions</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" [addresses]="addressStrings" (loadMore)="loadMore()"></app-transactions-list>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<ng-template [ngIf]="isLoadingTransactions">
|
||||||
|
<div class="header-bg box">
|
||||||
|
<div class="row" style="height: 107px;">
|
||||||
|
<div class="col-sm">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template [ngIf]="retryLoadMore">
|
||||||
|
<br>
|
||||||
|
<button type="button" class="btn btn-outline-info btn-sm" (click)="loadMore()"><fa-icon [icon]="['fas', 'redo-alt']" [fixedWidth]="true"></fa-icon></button>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<ng-template #loadingTemplate>
|
<ng-template #loadingTemplate>
|
||||||
|
|
||||||
<div class="box" *ngIf="!error; else errorTemplate">
|
<div class="box" *ngIf="!error; else errorTemplate">
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { of, Observable, Subscription } from 'rxjs';
|
|||||||
import { SeoService } from '@app/services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||||
import { WalletAddress } from '@interfaces/node-api.interface';
|
import { WalletAddress } from '@interfaces/node-api.interface';
|
||||||
|
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||||
|
import { AudioService } from '@app/services/audio.service';
|
||||||
|
|
||||||
class WalletStats implements ChainStats {
|
class WalletStats implements ChainStats {
|
||||||
addresses: string[];
|
addresses: string[];
|
||||||
@@ -24,6 +26,7 @@ class WalletStats implements ChainStats {
|
|||||||
acc.funded_txo_sum += stat.funded_txo_sum;
|
acc.funded_txo_sum += stat.funded_txo_sum;
|
||||||
acc.spent_txo_count += stat.spent_txo_count;
|
acc.spent_txo_count += stat.spent_txo_count;
|
||||||
acc.spent_txo_sum += stat.spent_txo_sum;
|
acc.spent_txo_sum += stat.spent_txo_sum;
|
||||||
|
acc.tx_count += stat.tx_count;
|
||||||
return acc;
|
return acc;
|
||||||
}, {
|
}, {
|
||||||
funded_txo_count: 0,
|
funded_txo_count: 0,
|
||||||
@@ -109,12 +112,17 @@ export class WalletComponent implements OnInit, OnDestroy {
|
|||||||
addressStrings: string[] = [];
|
addressStrings: string[] = [];
|
||||||
walletName: string;
|
walletName: string;
|
||||||
isLoadingWallet = true;
|
isLoadingWallet = true;
|
||||||
|
isLoadingTransactions = true;
|
||||||
|
transactions: Transaction[];
|
||||||
|
totalTransactionCount: number;
|
||||||
|
retryLoadMore = false;
|
||||||
wallet$: Observable<Record<string, WalletAddress>>;
|
wallet$: Observable<Record<string, WalletAddress>>;
|
||||||
walletAddresses$: Observable<Record<string, Address>>;
|
walletAddresses$: Observable<Record<string, Address>>;
|
||||||
walletSummary$: Observable<AddressTxSummary[]>;
|
walletSummary$: Observable<AddressTxSummary[]>;
|
||||||
walletStats$: Observable<WalletStats>;
|
walletStats$: Observable<WalletStats>;
|
||||||
error: any;
|
error: any;
|
||||||
walletSubscription: Subscription;
|
walletSubscription: Subscription;
|
||||||
|
transactionSubscription: Subscription;
|
||||||
|
|
||||||
collapseAddresses: boolean = true;
|
collapseAddresses: boolean = true;
|
||||||
|
|
||||||
@@ -129,6 +137,8 @@ export class WalletComponent implements OnInit, OnDestroy {
|
|||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
|
private electrsApiService: ElectrsApiService,
|
||||||
|
private audioService: AudioService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@@ -172,6 +182,21 @@ export class WalletComponent implements OnInit, OnDestroy {
|
|||||||
}),
|
}),
|
||||||
switchMap(initial => this.stateService.walletTransactions$.pipe(
|
switchMap(initial => this.stateService.walletTransactions$.pipe(
|
||||||
startWith(null),
|
startWith(null),
|
||||||
|
tap((transactions) => {
|
||||||
|
if (!transactions?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const transaction of transactions) {
|
||||||
|
const tx = this.transactions.find((t) => t.txid === transaction.txid);
|
||||||
|
if (tx) {
|
||||||
|
tx.status = transaction.status;
|
||||||
|
} else {
|
||||||
|
this.transactions.unshift(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.transactions = this.transactions.slice();
|
||||||
|
this.audioService.playSound('magic');
|
||||||
|
}),
|
||||||
scan((wallet, walletTransactions) => {
|
scan((wallet, walletTransactions) => {
|
||||||
for (const tx of (walletTransactions || [])) {
|
for (const tx of (walletTransactions || [])) {
|
||||||
const funded: Record<string, number> = {};
|
const funded: Record<string, number> = {};
|
||||||
@@ -267,8 +292,57 @@ export class WalletComponent implements OnInit, OnDestroy {
|
|||||||
return stats;
|
return stats;
|
||||||
}, walletStats),
|
}, walletStats),
|
||||||
);
|
);
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.transactionSubscription = this.wallet$.pipe(
|
||||||
|
switchMap(wallet => {
|
||||||
|
const addresses = Object.keys(wallet).map(addr => this.normalizeAddress(addr));
|
||||||
|
return this.electrsApiService.getAddressesTransactions$(addresses);
|
||||||
|
}),
|
||||||
|
map(transactions => {
|
||||||
|
// only confirmed transactions supported for now
|
||||||
|
return transactions.filter(tx => tx.status.confirmed).sort((a, b) => b.status.block_height - a.status.block_height);
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
console.log(error);
|
||||||
|
this.error = error;
|
||||||
|
this.seoService.logSoft404();
|
||||||
|
this.isLoadingWallet = false;
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
).subscribe((transactions: Transaction[] | null) => {
|
||||||
|
if (!transactions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.transactions = transactions;
|
||||||
|
this.isLoadingTransactions = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMore(): void {
|
||||||
|
if (this.isLoadingTransactions || this.fullyLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isLoadingTransactions = true;
|
||||||
|
this.retryLoadMore = false;
|
||||||
|
this.electrsApiService.getAddressesTransactions$(this.addressStrings, this.transactions[this.transactions.length - 1].txid)
|
||||||
|
.subscribe((transactions: Transaction[]) => {
|
||||||
|
if (transactions && transactions.length) {
|
||||||
|
this.transactions = this.transactions.concat(transactions.sort((a, b) => b.status.block_height - a.status.block_height));
|
||||||
|
} else {
|
||||||
|
this.fullyLoaded = true;
|
||||||
|
}
|
||||||
|
this.isLoadingTransactions = false;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.isLoadingTransactions = false;
|
||||||
|
this.retryLoadMore = true;
|
||||||
|
// In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page.
|
||||||
|
if (error.status === 422) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
|
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
|
||||||
@@ -299,5 +373,6 @@ export class WalletComponent implements OnInit, OnDestroy {
|
|||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.websocketService.stopTrackingWallet();
|
this.websocketService.stopTrackingWallet();
|
||||||
this.walletSubscription.unsubscribe();
|
this.walletSubscription.unsubscribe();
|
||||||
|
this.transactionSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||||
import { Env, StateService } from '@app/services/state.service';
|
import { Env, StateService } from '@app/services/state.service';
|
||||||
import { restApiDocsData } from '@app/docs/api-docs/api-docs-data';
|
import { restApiDocsData, wsApiDocsData } from '@app/docs/api-docs/api-docs-data';
|
||||||
import { faqData } from '@app/docs/api-docs/api-docs-data';
|
import { faqData } from '@app/docs/api-docs/api-docs-data';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -28,6 +28,8 @@ export class ApiDocsNavComponent implements OnInit {
|
|||||||
this.auditEnabled = this.env.AUDIT;
|
this.auditEnabled = this.env.AUDIT;
|
||||||
if (this.whichTab === 'rest') {
|
if (this.whichTab === 'rest') {
|
||||||
this.tabData = restApiDocsData;
|
this.tabData = restApiDocsData;
|
||||||
|
} else if (this.whichTab === 'websocket') {
|
||||||
|
this.tabData = wsApiDocsData;
|
||||||
} else if (this.whichTab === 'faq') {
|
} else if (this.whichTab === 'faq') {
|
||||||
this.tabData = faqData;
|
this.tabData = faqData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,18 +108,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="websocketAPI" *ngIf="( whichTab === 'websocket' )">
|
<div id="websocketAPI" *ngIf="whichTab === 'websocket'">
|
||||||
<div class="api-category">
|
|
||||||
<div class="websocket">
|
<div id="doc-nav-desktop" class="hide-on-mobile" [ngClass]="desktopDocsNavPosition">
|
||||||
<div class="endpoint">
|
<app-api-docs-nav (navLinkClickEvent)="anchorLinkClick( $event )" [network]="{ val: network$ | async }" [whichTab]="whichTab"></app-api-docs-nav>
|
||||||
<div class="subtitle" i18n="Api docs endpoint">Endpoint</div>
|
</div>
|
||||||
{{ wrapUrl(network.val, wsDocs, true) }}
|
|
||||||
|
<div class="doc-content">
|
||||||
|
|
||||||
|
<div id="enterprise-cta-mobile" *ngIf="officialMempoolInstance && showMobileEnterpriseUpsell">
|
||||||
|
<p>Get higher API limits with <span class="no-line-break">Mempool Enterprise®</span></p>
|
||||||
|
<div class="button-group">
|
||||||
|
<a class="btn btn-small btn-secondary" (click)="showMobileEnterpriseUpsell = false">No Thanks</a>
|
||||||
|
<a class="btn btn-small btn-purple" href="https://mempool.space/enterprise">More Info <fa-icon [icon]="['fas', 'angle-right']" [styles]="{'font-size': '12px'}"></fa-icon></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
</div>
|
||||||
<div class="subtitle" i18n>Description</div>
|
|
||||||
<div i18n="api-docs.websocket.websocket">Default push: <code>{{ '{' }} action: 'want', data: ['blocks', ...] {{ '}' }}</code> to express what you want pushed. Available: <code>blocks</code>, <code>mempool-blocks</code>, <code>live-2h-chart</code>, and <code>stats</code>.<br><br>Push transactions related to address: <code>{{ '{' }} 'track-address': '3PbJ...bF9B' {{ '}' }}</code> to receive all new transactions containing that address as input or output. Returns an array of transactions. <code>address-transactions</code> for new mempool transactions, and <code>block-transactions</code> for new block confirmed transactions.</div>
|
<p class="doc-welcome-note">Below is a reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} <ng-container i18n="api-docs.title-websocket">Websocket service</ng-container> running at {{ websocketUrl(network.val) }}.</p>
|
||||||
|
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that usage limits apply to our WebSocket API. Consider an <a href="https://mempool.space/enterprise">enterprise sponsorship</a> if you need higher API limits, such as higher tracking limits.</p>
|
||||||
|
|
||||||
|
<div class="doc-item-container" *ngFor="let item of wsDocs">
|
||||||
|
<div *ngIf="!item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance )">
|
||||||
|
<h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3>
|
||||||
|
<div *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )" class="endpoint-container" id="{{ item.fragment }}">
|
||||||
|
<a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick({event: $event, fragment: item.fragment})">{{ item.title }} <span>{{ item.category }}</span></a>
|
||||||
|
<div class="endpoint-content">
|
||||||
|
<div class="description">
|
||||||
|
<div class="subtitle" i18n>Description</div>
|
||||||
|
<div [innerHTML]="item.description.default" i18n></div>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<div class="subtitle" i18n>Payload</div>
|
||||||
|
<pre><code [innerText]="item.payload"></code></pre>
|
||||||
|
</div>
|
||||||
|
<app-code-template [hostname]="hostname" [baseNetworkUrl]="baseNetworkUrl" [method]="item.httpRequestMethod" [code]="item.codeExample.default" [network]="network.val" [showCodeExample]="item.showJsExamples"></app-code-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-code-template [method]="'websocket'" [hostname]="hostname" [code]="wsDocs" [network]="network.val" [showCodeExample]="wsDocs.showJsExamples"></app-code-template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -470,3 +470,21 @@ dd {
|
|||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: var(--bg);
|
||||||
|
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
display: block;
|
||||||
|
font-size: 87.5%;
|
||||||
|
color: #f18920;
|
||||||
|
background-color: var(--bg);
|
||||||
|
padding: 30px;
|
||||||
|
code{
|
||||||
|
background-color: transparent;
|
||||||
|
white-space: break-spaces;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -145,7 +145,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
|||||||
if (document.getElementById( targetId + "-tab-header" )) {
|
if (document.getElementById( targetId + "-tab-header" )) {
|
||||||
tabHeaderHeight = document.getElementById( targetId + "-tab-header" ).scrollHeight;
|
tabHeaderHeight = document.getElementById( targetId + "-tab-header" ).scrollHeight;
|
||||||
}
|
}
|
||||||
if( ( window.innerWidth <= 992 ) && ( ( this.whichTab === 'rest' ) || ( this.whichTab === 'faq' ) ) && targetId ) {
|
if( ( window.innerWidth <= 992 ) && ( ( this.whichTab === 'rest' ) || ( this.whichTab === 'faq' ) || ( this.whichTab === 'websocket' ) ) && targetId ) {
|
||||||
const endpointContainerEl = document.querySelector<HTMLElement>( "#" + targetId );
|
const endpointContainerEl = document.querySelector<HTMLElement>( "#" + targetId );
|
||||||
const endpointContentEl = document.querySelector<HTMLElement>( "#" + targetId + " .endpoint-content" );
|
const endpointContentEl = document.querySelector<HTMLElement>( "#" + targetId + " .endpoint-content" );
|
||||||
const endPointContentElHeight = endpointContentEl.clientHeight;
|
const endPointContentElHeight = endpointContentEl.clientHeight;
|
||||||
@@ -207,13 +207,29 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
|||||||
text = text.replace('%{' + indexNumber + '}', curlText);
|
text = text.replace('%{' + indexNumber + '}', curlText);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (websocket) {
|
|
||||||
const wsHostname = this.hostname.replace('https://', 'wss://');
|
|
||||||
wsHostname.replace('http://', 'ws://');
|
|
||||||
return `${wsHostname}${curlNetwork}${text}`;
|
|
||||||
}
|
|
||||||
return `${this.hostname}${curlNetwork}${text}`;
|
return `${this.hostname}${curlNetwork}${text}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
websocketUrl(network: string) {
|
||||||
|
let curlNetwork = '';
|
||||||
|
if (this.env.BASE_MODULE === 'mempool') {
|
||||||
|
if (!['', 'mainnet'].includes(network)) {
|
||||||
|
curlNetwork = `/${network}`;
|
||||||
|
}
|
||||||
|
} else if (this.env.BASE_MODULE === 'liquid') {
|
||||||
|
if (!['', 'liquid'].includes(network)) {
|
||||||
|
curlNetwork = `/${network}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (network === this.env.ROOT_NETWORK) {
|
||||||
|
curlNetwork = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let wsHostname = this.hostname.replace('https://', 'wss://');
|
||||||
|
wsHostname = wsHostname.replace('http://', 'ws://');
|
||||||
|
return `${wsHostname}${curlNetwork}/api/v1/ws`;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export interface Transaction {
|
|||||||
price?: Price;
|
price?: Price;
|
||||||
sigops?: number;
|
sigops?: number;
|
||||||
flags?: bigint;
|
flags?: bigint;
|
||||||
|
largeInput?: boolean;
|
||||||
|
largeOutput?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionChannels {
|
export interface TransactionChannels {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AddressTxSummary, Block, ChainStats, Transaction } from "./electrs.interface";
|
import { AddressTxSummary, Block, ChainStats } from "./electrs.interface";
|
||||||
|
|
||||||
export interface OptimizedMempoolStats {
|
export interface OptimizedMempoolStats {
|
||||||
added: number;
|
added: number;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export interface WebsocketResponse {
|
|||||||
rbfInfo?: RbfTree;
|
rbfInfo?: RbfTree;
|
||||||
rbfLatest?: RbfTree[];
|
rbfLatest?: RbfTree[];
|
||||||
rbfLatestSummary?: ReplacementInfo[];
|
rbfLatestSummary?: ReplacementInfo[];
|
||||||
|
stratumJob?: StratumJob;
|
||||||
|
stratumJobs?: Record<number, StratumJob>;
|
||||||
utxoSpent?: object;
|
utxoSpent?: object;
|
||||||
transactions?: TransactionStripped[];
|
transactions?: TransactionStripped[];
|
||||||
loadingIndicators?: ILoadingIndicators;
|
loadingIndicators?: ILoadingIndicators;
|
||||||
@@ -37,6 +39,7 @@ export interface WebsocketResponse {
|
|||||||
'track-rbf-summary'?: boolean;
|
'track-rbf-summary'?: boolean;
|
||||||
'track-accelerations'?: boolean;
|
'track-accelerations'?: boolean;
|
||||||
'track-wallet'?: string;
|
'track-wallet'?: string;
|
||||||
|
'track-stratum'?: string | number;
|
||||||
'watch-mempool'?: boolean;
|
'watch-mempool'?: boolean;
|
||||||
'refresh-blocks'?: boolean;
|
'refresh-blocks'?: boolean;
|
||||||
}
|
}
|
||||||
@@ -150,3 +153,24 @@ export interface HealthCheckHost {
|
|||||||
electrs?: string;
|
electrs?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StratumJob {
|
||||||
|
pool: number;
|
||||||
|
height: number;
|
||||||
|
coinbase: string;
|
||||||
|
scriptsig: string;
|
||||||
|
reward: number;
|
||||||
|
jobId: string;
|
||||||
|
extraNonce: string;
|
||||||
|
extraNonce2Size: number;
|
||||||
|
prevHash: string;
|
||||||
|
coinbase1: string;
|
||||||
|
coinbase2: string;
|
||||||
|
merkleBranches: string[];
|
||||||
|
version: string;
|
||||||
|
bits: string;
|
||||||
|
time: string;
|
||||||
|
timestamp: number;
|
||||||
|
cleanJobs: boolean;
|
||||||
|
received: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="lightning.created">Created</td>
|
<td i18n="lightning.created">Created</td>
|
||||||
<td>{{ channel.created | date:'yyyy-MM-dd HH:mm' }}</td>
|
<td><app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="channel.created" [hideTimeSince]="true"></app-timestamp></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="lightning.capacity">Capacity</td>
|
<td i18n="lightning.capacity">Capacity</td>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<ng-container *ngFor="let channel of channels;">
|
<ng-container *ngFor="let channel of channels;">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="timestamp">
|
<td class="timestamp">
|
||||||
‎{{ channel.closing_date | date:'yyyy-MM-dd HH:mm' }}
|
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm'" [unixTime]="channel.closing_date" [hideTimeSince]="true"></app-timestamp>
|
||||||
</td>
|
</td>
|
||||||
<td class="capacity text-right">
|
<td class="capacity text-right">
|
||||||
<app-amount *ngIf="channel.capacity > 100000000; else smallnode" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
<app-amount *ngIf="channel.capacity > 100000000; else smallnode" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import { TestTransactionsComponent } from '@components/test-transactions/test-tr
|
|||||||
import { CalculatorComponent } from '@components/calculator/calculator.component';
|
import { CalculatorComponent } from '@components/calculator/calculator.component';
|
||||||
import { BlocksList } from '@components/blocks-list/blocks-list.component';
|
import { BlocksList } from '@components/blocks-list/blocks-list.component';
|
||||||
import { RbfList } from '@components/rbf-list/rbf-list.component';
|
import { RbfList } from '@components/rbf-list/rbf-list.component';
|
||||||
|
import { StratumList } from '@components/stratum/stratum-list/stratum-list.component';
|
||||||
import { ServerHealthComponent } from '@components/server-health/server-health.component';
|
import { ServerHealthComponent } from '@components/server-health/server-health.component';
|
||||||
import { ServerStatusComponent } from '@components/server-health/server-status.component';
|
import { ServerStatusComponent } from '@components/server-health/server-status.component';
|
||||||
import { FaucetComponent } from '@components/faucet/faucet.component'
|
import { FaucetComponent } from '@components/faucet/faucet.component';
|
||||||
|
|
||||||
const browserWindow = window || {};
|
const browserWindow = window || {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -56,6 +57,16 @@ const routes: Routes = [
|
|||||||
path: 'rbf',
|
path: 'rbf',
|
||||||
component: RbfList,
|
component: RbfList,
|
||||||
},
|
},
|
||||||
|
...(browserWindowEnv.STRATUM_ENABLED ? [{
|
||||||
|
path: 'stratum',
|
||||||
|
component: StartComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: StratumList,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}] : []),
|
||||||
{
|
{
|
||||||
path: 'terms-of-service',
|
path: 'terms-of-service',
|
||||||
loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),
|
loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),
|
||||||
|
|||||||
@@ -142,12 +142,16 @@ export class ElectrsApiService {
|
|||||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
|
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddressesTransactions$(addresses: string[], txid?: string): Observable<Transaction[]> {
|
getAddressesTransactions$(addresses: string[], txid?: string): Observable<Transaction[]> {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
if (txid) {
|
if (txid) {
|
||||||
params = params.append('after_txid', txid);
|
params = params.append('after_txid', txid);
|
||||||
}
|
}
|
||||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs?addresses=${addresses.join(',')}`, { params });
|
return this.httpClient.post<Transaction[]>(
|
||||||
|
this.apiBaseUrl + this.apiBasePath + '/api/addresses/txs',
|
||||||
|
addresses,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddressSummary$(address: string, txid?: string): Observable<AddressTxSummary[]> {
|
getAddressSummary$(address: string, txid?: string): Observable<AddressTxSummary[]> {
|
||||||
@@ -163,7 +167,7 @@ export class ElectrsApiService {
|
|||||||
if (txid) {
|
if (txid) {
|
||||||
params = params.append('after_txid', txid);
|
params = params.append('after_txid', txid);
|
||||||
}
|
}
|
||||||
return this.httpClient.get<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs/summary?addresses=${addresses.join(',')}`, { params });
|
return this.httpClient.post<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + '/api/addresses/txs/summary', addresses, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
getScriptHashTransactions$(script: string, txid?: string): Observable<Transaction[]> {
|
getScriptHashTransactions$(script: string, txid?: string): Observable<Transaction[]> {
|
||||||
@@ -182,7 +186,7 @@ export class ElectrsApiService {
|
|||||||
params = params.append('after_txid', txid);
|
params = params.append('after_txid', txid);
|
||||||
}
|
}
|
||||||
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
|
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
|
||||||
switchMap(scriptHashes => this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs?scripthashes=${scriptHashes.join(',')}`, { params })),
|
switchMap(scriptHashes => this.httpClient.post<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthashes/txs', scriptHashes, { params })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +216,7 @@ export class ElectrsApiService {
|
|||||||
params = params.append('after_txid', txid);
|
params = params.append('after_txid', txid);
|
||||||
}
|
}
|
||||||
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
|
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
|
||||||
switchMap(scriptHashes => this.httpClient.get<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs/summary?scripthashes=${scriptHashes.join(',')}`, { params })),
|
switchMap(scriptHashes => this.httpClient.post<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthashes/txs/summary', scriptHashes, { params })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export class EtaService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
hashratePercentage: acceleratingHashrateFraction * 100,
|
hashratePercentage: acceleratingHashrateFraction * 100,
|
||||||
ETA: Date.now() + da.timeAvg * mempoolPosition.block,
|
ETA: Date.now() + da.adjustedTimeAvg * mempoolPosition.block,
|
||||||
acceleratedETA: this.calculateETAFromShares([
|
acceleratedETA: this.calculateETAFromShares([
|
||||||
{ block: mempoolPosition.block, hashrateShare: (1 - acceleratingHashrateFraction) },
|
{ block: mempoolPosition.block, hashrateShare: (1 - acceleratingHashrateFraction) },
|
||||||
{ block: 0, hashrateShare: acceleratingHashrateFraction },
|
{ block: 0, hashrateShare: acceleratingHashrateFraction },
|
||||||
@@ -216,7 +216,7 @@ export class EtaService {
|
|||||||
}
|
}
|
||||||
// at max depth, the transaction is guaranteed to be mined in the next block if it hasn't already
|
// at max depth, the transaction is guaranteed to be mined in the next block if it hasn't already
|
||||||
Q += ((max + 1) * (1-tailProb));
|
Q += ((max + 1) * (1-tailProb));
|
||||||
const eta = da.timeAvg * Q; // T x Q
|
const eta = da.adjustedTimeAvg * Q; // T x Q
|
||||||
|
|
||||||
return {
|
return {
|
||||||
now,
|
now,
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ export class MiningService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get names and slugs of all pools
|
* Get names and slugs of all pools
|
||||||
*/
|
*/
|
||||||
public getPools(): Observable<any[]> {
|
public getPools(): Observable<any[]> {
|
||||||
@@ -75,7 +75,6 @@ export class MiningService {
|
|||||||
return this.poolsData;
|
return this.poolsData;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Set the hashrate power of ten we want to display
|
* Set the hashrate power of ten we want to display
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export interface IUser {
|
|||||||
subscription_tag: string;
|
subscription_tag: string;
|
||||||
status: 'pending' | 'verified' | 'disabled';
|
status: 'pending' | 'verified' | 'disabled';
|
||||||
features: string | null;
|
features: string | null;
|
||||||
fullName: string | null;
|
|
||||||
countryCode: string | null;
|
countryCode: string | null;
|
||||||
imageMd5: string;
|
imageMd5: string;
|
||||||
ogRank: number | null;
|
ogRank: number | null;
|
||||||
@@ -143,8 +142,8 @@ export class ServicesApiServices {
|
|||||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
|
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
|
||||||
}
|
}
|
||||||
|
|
||||||
accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
|
accelerateWithGooglePay$(txInput: string, token: string, verificationToken: string, cardTag: string, referenceId: string, userApprovedUSD: number, userChallenged: boolean) {
|
||||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
|
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged });
|
||||||
}
|
}
|
||||||
|
|
||||||
getAccelerations$(): Observable<Acceleration[]> {
|
getAccelerations$(): Observable<Acceleration[]> {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
||||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
|
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
|
||||||
import { AddressTxSummary, Transaction } from '@interfaces/electrs.interface';
|
import { Transaction } from '@interfaces/electrs.interface';
|
||||||
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '@interfaces/websocket.interface';
|
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, StratumJob, isMempoolState } from '@interfaces/websocket.interface';
|
||||||
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '@interfaces/node-api.interface';
|
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '@interfaces/node-api.interface';
|
||||||
import { Router, NavigationStart } from '@angular/router';
|
import { Router, NavigationStart } from '@angular/router';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
@@ -81,6 +81,7 @@ export interface Env {
|
|||||||
ADDITIONAL_CURRENCIES: boolean;
|
ADDITIONAL_CURRENCIES: boolean;
|
||||||
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
|
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
|
||||||
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
|
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
|
||||||
|
STRATUM_ENABLED: boolean;
|
||||||
SERVICES_API?: string;
|
SERVICES_API?: string;
|
||||||
customize?: Customization;
|
customize?: Customization;
|
||||||
PROD_DOMAINS: string[];
|
PROD_DOMAINS: string[];
|
||||||
@@ -123,6 +124,7 @@ const defaultEnv: Env = {
|
|||||||
'ACCELERATOR_BUTTON': true,
|
'ACCELERATOR_BUTTON': true,
|
||||||
'PUBLIC_ACCELERATIONS': false,
|
'PUBLIC_ACCELERATIONS': false,
|
||||||
'ADDITIONAL_CURRENCIES': false,
|
'ADDITIONAL_CURRENCIES': false,
|
||||||
|
'STRATUM_ENABLED': false,
|
||||||
'SERVICES_API': 'https://mempool.space/api/v1/services',
|
'SERVICES_API': 'https://mempool.space/api/v1/services',
|
||||||
'PROD_DOMAINS': [],
|
'PROD_DOMAINS': [],
|
||||||
};
|
};
|
||||||
@@ -159,6 +161,8 @@ export class StateService {
|
|||||||
liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
|
liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
|
||||||
accelerations$ = new Subject<AccelerationDelta>();
|
accelerations$ = new Subject<AccelerationDelta>();
|
||||||
liveAccelerations$: Observable<Acceleration[]>;
|
liveAccelerations$: Observable<Acceleration[]>;
|
||||||
|
stratumJobUpdate$ = new Subject<{ state: Record<string, StratumJob> } | { job: StratumJob }>();
|
||||||
|
stratumJobs$ = new BehaviorSubject<Record<string, StratumJob>>({});
|
||||||
txConfirmed$ = new Subject<[string, BlockExtended]>();
|
txConfirmed$ = new Subject<[string, BlockExtended]>();
|
||||||
txReplaced$ = new Subject<ReplacedTransaction>();
|
txReplaced$ = new Subject<ReplacedTransaction>();
|
||||||
txRbfInfo$ = new Subject<RbfTree>();
|
txRbfInfo$ = new Subject<RbfTree>();
|
||||||
@@ -186,6 +190,7 @@ export class StateService {
|
|||||||
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
||||||
|
|
||||||
viewAmountMode$: BehaviorSubject<'btc' | 'sats' | 'fiat'>;
|
viewAmountMode$: BehaviorSubject<'btc' | 'sats' | 'fiat'>;
|
||||||
|
timezone$: BehaviorSubject<string>;
|
||||||
connectionState$ = new BehaviorSubject<0 | 1 | 2>(2);
|
connectionState$ = new BehaviorSubject<0 | 1 | 2>(2);
|
||||||
isTabHidden$: Observable<boolean>;
|
isTabHidden$: Observable<boolean>;
|
||||||
|
|
||||||
@@ -302,6 +307,24 @@ export class StateService {
|
|||||||
map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added))
|
map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.stratumJobUpdate$.pipe(
|
||||||
|
scan((acc: Record<string, StratumJob>, update: { state: Record<string, StratumJob> } | { job: StratumJob }) => {
|
||||||
|
if ('state' in update) {
|
||||||
|
// Replace the entire state
|
||||||
|
return update.state;
|
||||||
|
} else {
|
||||||
|
// Update or create a single job entry
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[update.job.pool]: update.job
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, {}),
|
||||||
|
shareReplay(1)
|
||||||
|
).subscribe(val => {
|
||||||
|
this.stratumJobs$.next(val);
|
||||||
|
});
|
||||||
|
|
||||||
this.networkChanged$.subscribe((network) => {
|
this.networkChanged$.subscribe((network) => {
|
||||||
this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
|
this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
|
||||||
this.blocksSubject$.next([]);
|
this.blocksSubject$.next([]);
|
||||||
@@ -347,6 +370,9 @@ export class StateService {
|
|||||||
const viewAmountModePreference = this.storageService.getValue('view-amount-mode') as 'btc' | 'sats' | 'fiat';
|
const viewAmountModePreference = this.storageService.getValue('view-amount-mode') as 'btc' | 'sats' | 'fiat';
|
||||||
this.viewAmountMode$ = new BehaviorSubject<'btc' | 'sats' | 'fiat'>(viewAmountModePreference || 'btc');
|
this.viewAmountMode$ = new BehaviorSubject<'btc' | 'sats' | 'fiat'>(viewAmountModePreference || 'btc');
|
||||||
|
|
||||||
|
const timezonePreference = this.storageService.getValue('timezone-preference');
|
||||||
|
this.timezone$ = new BehaviorSubject<string>(timezonePreference || 'local');
|
||||||
|
|
||||||
this.backend$.subscribe(backend => {
|
this.backend$.subscribe(backend => {
|
||||||
this.backend = backend;
|
this.backend = backend;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export class WebsocketService {
|
|||||||
private isTrackingAccelerations: boolean = false;
|
private isTrackingAccelerations: boolean = false;
|
||||||
private isTrackingWallet: boolean = false;
|
private isTrackingWallet: boolean = false;
|
||||||
private trackingWalletName: string;
|
private trackingWalletName: string;
|
||||||
|
private isTrackingStratum: string | number | false = false;
|
||||||
private trackingMempoolBlock: number;
|
private trackingMempoolBlock: number;
|
||||||
private trackingMempoolBlockNetwork: string;
|
private trackingMempoolBlockNetwork: string;
|
||||||
private stoppingTrackMempoolBlock: any | null = null;
|
private stoppingTrackMempoolBlock: any | null = null;
|
||||||
@@ -143,6 +144,9 @@ export class WebsocketService {
|
|||||||
if (this.isTrackingWallet) {
|
if (this.isTrackingWallet) {
|
||||||
this.startTrackingWallet(this.trackingWalletName);
|
this.startTrackingWallet(this.trackingWalletName);
|
||||||
}
|
}
|
||||||
|
if (this.isTrackingStratum !== false) {
|
||||||
|
this.startTrackStratum(this.isTrackingStratum);
|
||||||
|
}
|
||||||
this.stateService.connectionState$.next(2);
|
this.stateService.connectionState$.next(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,6 +293,18 @@ export class WebsocketService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startTrackStratum(pool: number | string) {
|
||||||
|
this.websocketSubject.next({ 'track-stratum': pool });
|
||||||
|
this.isTrackingStratum = pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTrackStratum() {
|
||||||
|
if (this.isTrackingStratum) {
|
||||||
|
this.websocketSubject.next({ 'track-stratum': null });
|
||||||
|
this.isTrackingStratum = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fetchStatistics(historicalDate: string) {
|
fetchStatistics(historicalDate: string) {
|
||||||
this.websocketSubject.next({ historicalDate });
|
this.websocketSubject.next({ historicalDate });
|
||||||
}
|
}
|
||||||
@@ -512,6 +528,14 @@ export class WebsocketService {
|
|||||||
this.stateService.previousRetarget$.next(response.previousRetarget);
|
this.stateService.previousRetarget$.next(response.previousRetarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.stratumJobs) {
|
||||||
|
this.stateService.stratumJobUpdate$.next({ state: response.stratumJobs });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.stratumJob) {
|
||||||
|
this.stateService.stratumJobUpdate$.next({ job: response.stratumJob });
|
||||||
|
}
|
||||||
|
|
||||||
if (response['tomahawk']) {
|
if (response['tomahawk']) {
|
||||||
this.stateService.serverHealth$.next(response['tomahawk']);
|
this.stateService.serverHealth$.next(response['tomahawk']);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
|
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
|
||||||
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
|
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && removed">
|
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && (removed || cached)">
|
||||||
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
|
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !removed">
|
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !(removed || cached)">
|
||||||
<button type="button" class="btn btn-sm btn-danger no-cursor {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
<button type="button" class="btn btn-sm btn-danger no-cursor {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -12,6 +12,7 @@ export class ConfirmationsComponent implements OnChanges {
|
|||||||
@Input() height: number;
|
@Input() height: number;
|
||||||
@Input() replaced: boolean = false;
|
@Input() replaced: boolean = false;
|
||||||
@Input() removed: boolean = false;
|
@Input() removed: boolean = false;
|
||||||
|
@Input() cached: boolean = false;
|
||||||
@Input() hideUnconfirmed: boolean = false;
|
@Input() hideUnconfirmed: boolean = false;
|
||||||
@Input() buttonClass: string = '';
|
@Input() buttonClass: string = '';
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<app-fiat-selector></app-fiat-selector>
|
<app-fiat-selector></app-fiat-selector>
|
||||||
</div>
|
</div>
|
||||||
<div class="selector">
|
<div class="selector">
|
||||||
<app-rate-unit-selector></app-rate-unit-selector>
|
<app-timezone-selector></app-timezone-selector>
|
||||||
</div>
|
</div>
|
||||||
<div class="selector d-none" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'">
|
<div class="selector d-none" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'">
|
||||||
<app-amount-selector></app-amount-selector>
|
<app-amount-selector></app-amount-selector>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<span *ngIf="seconds === undefined">-</span>
|
<span *ngIf="seconds === undefined">-</span>
|
||||||
<span *ngIf="seconds !== undefined">
|
<span *ngIf="seconds !== undefined">
|
||||||
‎{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }}
|
‎{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' : (stateService.timezone$ | async) }}
|
||||||
<div class="lg-inline" *ngIf="!hideTimeSince">
|
<div class="lg-inline" *ngIf="!hideTimeSince">
|
||||||
<i class="symbol">(<app-time kind="since" [time]="seconds" [fastRender]="true" [precision]="precision" [minUnit]="minUnit"></app-time>)</i>
|
<i class="symbol">(<app-time kind="since" [time]="seconds" [fastRender]="true" [precision]="precision" [minUnit]="minUnit"></app-time>)</i>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
|
||||||
|
import { StateService } from '@app/services/state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-timestamp',
|
selector: 'app-timestamp',
|
||||||
@@ -16,6 +17,10 @@ export class TimestampComponent implements OnChanges {
|
|||||||
|
|
||||||
seconds: number | undefined = undefined;
|
seconds: number | undefined = undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public stateService: StateService,
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
if (this.unixTime) {
|
if (this.unixTime) {
|
||||||
this.seconds = this.unixTime;
|
this.seconds = this.unixTime;
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
|
|||||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||||
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
|
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
|
||||||
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
|
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
|
||||||
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck } from '@fortawesome/free-solid-svg-icons';
|
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft,
|
||||||
|
faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
|
||||||
|
faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline,
|
||||||
|
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||||
import { MenuComponent } from '@components/menu/menu.component';
|
import { MenuComponent } from '@components/menu/menu.component';
|
||||||
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
|
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
|
||||||
@@ -36,6 +39,7 @@ import { FiatSelectorComponent } from '@components/fiat-selector/fiat-selector.c
|
|||||||
import { RateUnitSelectorComponent } from '@components/rate-unit-selector/rate-unit-selector.component';
|
import { RateUnitSelectorComponent } from '@components/rate-unit-selector/rate-unit-selector.component';
|
||||||
import { ThemeSelectorComponent } from '@components/theme-selector/theme-selector.component';
|
import { ThemeSelectorComponent } from '@components/theme-selector/theme-selector.component';
|
||||||
import { AmountSelectorComponent } from '@components/amount-selector/amount-selector.component';
|
import { AmountSelectorComponent } from '@components/amount-selector/amount-selector.component';
|
||||||
|
import { TimezoneSelectorComponent } from '@components/timezone-selector/timezone-selector.component';
|
||||||
import { BrowserOnlyDirective } from '@app/shared/directives/browser-only.directive';
|
import { BrowserOnlyDirective } from '@app/shared/directives/browser-only.directive';
|
||||||
import { ServerOnlyDirective } from '@app/shared/directives/server-only.directive';
|
import { ServerOnlyDirective } from '@app/shared/directives/server-only.directive';
|
||||||
import { ColoredPriceDirective } from '@app/shared/directives/colored-price.directive';
|
import { ColoredPriceDirective } from '@app/shared/directives/colored-price.directive';
|
||||||
@@ -79,6 +83,7 @@ import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
|
|||||||
import { DifficultyAdjustmentsTable } from '@components/difficulty-adjustments-table/difficulty-adjustments-table.components';
|
import { DifficultyAdjustmentsTable } from '@components/difficulty-adjustments-table/difficulty-adjustments-table.components';
|
||||||
import { BlocksList } from '@components/blocks-list/blocks-list.component';
|
import { BlocksList } from '@components/blocks-list/blocks-list.component';
|
||||||
import { RbfList } from '@components/rbf-list/rbf-list.component';
|
import { RbfList } from '@components/rbf-list/rbf-list.component';
|
||||||
|
import { StratumList } from '@components/stratum/stratum-list/stratum-list.component';
|
||||||
import { RewardStatsComponent } from '@components/reward-stats/reward-stats.component';
|
import { RewardStatsComponent } from '@components/reward-stats/reward-stats.component';
|
||||||
import { DataCyDirective } from '@app/data-cy.directive';
|
import { DataCyDirective } from '@app/data-cy.directive';
|
||||||
import { LoadingIndicatorComponent } from '@components/loading-indicator/loading-indicator.component';
|
import { LoadingIndicatorComponent } from '@components/loading-indicator/loading-indicator.component';
|
||||||
@@ -134,6 +139,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
|
|||||||
ThemeSelectorComponent,
|
ThemeSelectorComponent,
|
||||||
RateUnitSelectorComponent,
|
RateUnitSelectorComponent,
|
||||||
AmountSelectorComponent,
|
AmountSelectorComponent,
|
||||||
|
TimezoneSelectorComponent,
|
||||||
ScriptpubkeyTypePipe,
|
ScriptpubkeyTypePipe,
|
||||||
RelativeUrlPipe,
|
RelativeUrlPipe,
|
||||||
NoSanitizePipe,
|
NoSanitizePipe,
|
||||||
@@ -196,6 +202,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
|
|||||||
DifficultyAdjustmentsTable,
|
DifficultyAdjustmentsTable,
|
||||||
BlocksList,
|
BlocksList,
|
||||||
RbfList,
|
RbfList,
|
||||||
|
StratumList,
|
||||||
DataCyDirective,
|
DataCyDirective,
|
||||||
RewardStatsComponent,
|
RewardStatsComponent,
|
||||||
LoadingIndicatorComponent,
|
LoadingIndicatorComponent,
|
||||||
@@ -283,6 +290,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
|
|||||||
RateUnitSelectorComponent,
|
RateUnitSelectorComponent,
|
||||||
ThemeSelectorComponent,
|
ThemeSelectorComponent,
|
||||||
AmountSelectorComponent,
|
AmountSelectorComponent,
|
||||||
|
TimezoneSelectorComponent,
|
||||||
ScriptpubkeyTypePipe,
|
ScriptpubkeyTypePipe,
|
||||||
RelativeUrlPipe,
|
RelativeUrlPipe,
|
||||||
Hex2asciiPipe,
|
Hex2asciiPipe,
|
||||||
@@ -339,6 +347,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
|
|||||||
AmountShortenerPipe,
|
AmountShortenerPipe,
|
||||||
DifficultyAdjustmentsTable,
|
DifficultyAdjustmentsTable,
|
||||||
BlocksList,
|
BlocksList,
|
||||||
|
StratumList,
|
||||||
DataCyDirective,
|
DataCyDirective,
|
||||||
RewardStatsComponent,
|
RewardStatsComponent,
|
||||||
LoadingIndicatorComponent,
|
LoadingIndicatorComponent,
|
||||||
@@ -448,5 +457,8 @@ export class SharedModule {
|
|||||||
library.addIcons(faTimeline);
|
library.addIcons(faTimeline);
|
||||||
library.addIcons(faCircleXmark);
|
library.addIcons(faCircleXmark);
|
||||||
library.addIcons(faCalendarCheck);
|
library.addIcons(faCalendarCheck);
|
||||||
|
library.addIcons(faMoneyBillTrendUp);
|
||||||
|
library.addIcons(faRobot);
|
||||||
|
library.addIcons(faShareNodes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>River | Invest in Bitcoin with confidence</title>
|
|
||||||
<script src="/resources/config.js"></script>
|
|
||||||
<script src="/resources/customize.js"></script>
|
|
||||||
<base href="/">
|
|
||||||
|
|
||||||
<meta name="description" content="Easily buy Bitcoin in minutes. Zero fees on recurring buys. Invest in Bitcoin with confidence with world-class security." />
|
|
||||||
<meta property="og:image" content="https://mempool.space/resources/river/river-preview.jpg" />
|
|
||||||
<meta property="og:image:type" content="image/jpeg" />
|
|
||||||
<meta property="og:image:width" content="2000" />
|
|
||||||
<meta property="og:image:height" content="1000" />
|
|
||||||
<meta property="og:description" content="Easily buy Bitcoin in minutes. Zero fees on recurring buys. Invest in Bitcoin with confidence with world-class security." />
|
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
|
||||||
<meta name="twitter:site" content="@mempool">
|
|
||||||
<meta name="twitter:creator" content="@mempool">
|
|
||||||
<meta name="twitter:title" content="River | Invest in Bitcoin with confidence">
|
|
||||||
<meta name="twitter:description" content="Easily buy Bitcoin in minutes. Zero fees on recurring buys. Invest in Bitcoin with confidence with world-class security." />
|
|
||||||
<meta name="twitter:image" content="https://mempool.space/resources/river/river-preview.jpg" />
|
|
||||||
<meta name="twitter:domain" content="river.mempool.space">
|
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/resources/river/favicons/apple-touch-icon.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/resources/river/favicons/favicon-32x32.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/resources/river/favicons/favicon-16x16.png">
|
|
||||||
<link rel="manifest" href="/resources/river/favicons/site.webmanifest">
|
|
||||||
<link rel="shortcut icon" href="/resources/river/favicons/favicon.ico">
|
|
||||||
<link id="canonical" rel="canonical" href="https://river.mempool.space">
|
|
||||||
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
|
||||||
<meta name="msapplication-TileColor" content="#000000">
|
|
||||||
<meta name="msapplication-config" content="/resources/favicons/browserconfig.xml">
|
|
||||||
<meta name="theme-color" content="#1d1f31">
|
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<app-root></app-root>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 378 B |
|
Before Width: | Height: | Size: 772 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@@ -1 +0,0 @@
|
|||||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<svg width="160" height="40" viewBox="0 0 160 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M75.8865 31.2087H70.2185L66.2029 23.3666H59.531V31.2087H54.5116V8.52042H66.8515C69.4627 8.52042 71.4874 9.22873 72.9256 10.6425C74.3609 12.0591 75.08 13.8144 75.08 15.9082C75.08 17.3135 74.7388 18.5975 74.0592 19.7658C73.3796 20.9341 72.4011 21.8286 71.1293 22.4551L75.8893 31.2059L75.8865 31.2087ZM59.531 18.3407H67.1109C68.0612 18.3407 68.7888 18.1291 69.2964 17.7086C69.8039 17.2881 70.0577 16.7096 70.0577 15.9759C70.0577 15.2422 69.8039 14.6525 69.2964 14.2094C68.7888 13.7664 68.0612 13.5463 67.1109 13.5463H59.531V18.3435V18.3407ZM84.2954 31.2087H79.276V8.52042H84.2954V31.2087ZM99.709 31.0083L106.192 8.52042H111.699L105.135 31.2087H94.0862L87.7949 8.52042H93.3022L99.7119 31.0083H99.709ZM134.718 26.1857V31.2087H114.638V8.52042H134.557V13.5434H119.66V17.3671H134.397V22.3591H119.66V26.1828H134.721L134.718 26.1857ZM159.914 31.2087H154.246L150.23 23.3666H143.559V31.2087H138.539V8.52042H150.879C153.49 8.52042 155.515 9.22873 156.953 10.6425C158.388 12.0591 159.108 13.8144 159.108 15.9082C159.108 17.3135 158.766 18.5975 158.087 19.7658C157.407 20.9341 156.429 21.8286 155.157 22.4551L159.917 31.2059L159.914 31.2087ZM143.559 18.3407H151.138C152.089 18.3407 152.816 18.1291 153.324 17.7086C153.831 17.2881 154.085 16.7096 154.085 15.9759C154.085 15.2422 153.831 14.6525 153.324 14.2094C152.816 13.7664 152.089 13.5463 151.138 13.5463H143.559V18.3435V18.3407Z" fill="#F9F9F9"/>
|
|
||||||
<path d="M6.50834 35.4049L19.1838 7.60612C19.2346 7.49324 19.4038 7.56097 19.3643 7.67949L10.2673 34.0024C11.6265 33.5734 13.0195 33.2207 14.4407 32.9442L20.481 8.00683C20.5092 7.88549 20.6896 7.92218 20.6727 8.04352L17.1591 32.5124C18.3181 32.3713 19.4968 32.281 20.684 32.2415L21.8289 8.15357C21.8345 8.02941 22.0178 8.02941 22.0235 8.15357L23.1683 32.2415C24.3583 32.2782 25.5342 32.3685 26.6932 32.5124L23.1796 8.04352C23.1627 7.92218 23.3404 7.88549 23.3714 8.00683L29.4116 32.9442C30.8328 33.2207 32.2259 33.5763 33.5851 34.0024L24.4881 7.67949C24.4486 7.56379 24.6178 7.49324 24.6685 7.60612L37.344 35.4049C39.6338 36.3982 41.8107 37.6031 43.8495 39L21.9248 1L0 39C2.03597 37.6031 4.21294 36.3982 6.50552 35.4049" fill="#C5A063"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -131,7 +131,7 @@ export NVM_DIR="${HOME}/.nvm"
|
|||||||
source "${NVM_DIR}/nvm.sh"
|
source "${NVM_DIR}/nvm.sh"
|
||||||
|
|
||||||
# what to look for
|
# what to look for
|
||||||
frontends=(mainnet liquid onbtc bitb meta river)
|
frontends=(mainnet liquid onbtc bitb meta)
|
||||||
backends=(mainnet testnet testnet4 signet liquid liquidtestnet onbtc bitb)
|
backends=(mainnet testnet testnet4 signet liquid liquidtestnet onbtc bitb)
|
||||||
frontend_repos=()
|
frontend_repos=()
|
||||||
backend_repos=()
|
backend_repos=()
|
||||||
|
|||||||
@@ -153,6 +153,10 @@
|
|||||||
},
|
},
|
||||||
"WALLETS": {
|
"WALLETS": {
|
||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
"WALLETS": ["BITB", "3350", "RIVER"]
|
"WALLETS": ["BITB", "3350"]
|
||||||
|
},
|
||||||
|
"STRATUM": {
|
||||||
|
"ENABLED": true,
|
||||||
|
"API": "http://127.0.0.1:81/api/v1/stratum/ws"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
"TESTNET4_ENABLED": true,
|
"TESTNET4_ENABLED": true,
|
||||||
"LIQUID_ENABLED": false,
|
"LIQUID_ENABLED": false,
|
||||||
"LIQUID_TESTNET_ENABLED": false,
|
"LIQUID_TESTNET_ENABLED": false,
|
||||||
"BISQ_ENABLED": true,
|
"STRATUM_ENABLED": true,
|
||||||
"BISQ_SEPARATE_BACKEND": true,
|
|
||||||
"SIGNET_ENABLED": true,
|
"SIGNET_ENABLED": true,
|
||||||
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
|
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
|
||||||
"LIQUID_WEBSITE_URL": "https://liquid.network",
|
"LIQUID_WEBSITE_URL": "https://liquid.network",
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"OFFICIAL_MEMPOOL_SPACE": true,
|
|
||||||
"TESTNET_ENABLED": true,
|
|
||||||
"TESTNET4_ENABLED": true,
|
|
||||||
"LIQUID_ENABLED": true,
|
|
||||||
"LIQUID_TESTNET_ENABLED": true,
|
|
||||||
"BISQ_ENABLED": true,
|
|
||||||
"BISQ_SEPARATE_BACKEND": true,
|
|
||||||
"SIGNET_ENABLED": true,
|
|
||||||
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
|
|
||||||
"LIQUID_WEBSITE_URL": "https://liquid.network",
|
|
||||||
"BISQ_WEBSITE_URL": "https://bisq.markets",
|
|
||||||
"ITEMS_PER_PAGE": 25,
|
|
||||||
"LIGHTNING": true,
|
|
||||||
"ACCELERATOR": true,
|
|
||||||
"PUBLIC_ACCELERATIONS": true,
|
|
||||||
"AUDIT": true,
|
|
||||||
"CUSTOMIZATION": "custom-river-config.json"
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ screen -dmS x startx
|
|||||||
sleep 3
|
sleep 3
|
||||||
|
|
||||||
# start unfurlers for each frontend
|
# start unfurlers for each frontend
|
||||||
for site in mainnet liquid onbtc bitb meta river;do
|
for site in mainnet liquid onbtc bitb meta;do
|
||||||
cd "$HOME/${site}/unfurler" && \
|
cd "$HOME/${site}/unfurler" && \
|
||||||
echo "starting mempool unfurler: ${site}" && \
|
echo "starting mempool unfurler: ${site}" && \
|
||||||
screen -dmS "unfurler-${site}" sh -c 'while true;do npm run unfurler;sleep 2;done'
|
screen -dmS "unfurler-${site}" sh -c 'while true;do npm run unfurler;sleep 2;done'
|
||||||
|
|||||||