Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
251a1af442 | ||
|
|
9bdf42530a | ||
|
|
8525fbb177 | ||
|
|
63a3568481 | ||
|
|
e4941740de | ||
|
|
25bd33f7da | ||
|
|
2d007b9100 | ||
|
|
b71330c606 | ||
|
|
844b640c8c | ||
|
|
1277e58e68 | ||
|
|
fde6fe324a | ||
|
|
4ed114a4d5 | ||
|
|
8c2dfea6a6 | ||
|
|
a0624df06b | ||
|
|
1eedcf900b | ||
|
|
0b077d6fda | ||
|
|
80047313e7 | ||
|
|
71229b94c8 | ||
|
|
c256daf8c8 | ||
|
|
ba0fb996d2 | ||
|
|
5977e96034 | ||
|
|
a151c5cddd | ||
|
|
0323fd966d | ||
|
|
beb834bc30 | ||
|
|
ad6503c7b3 | ||
|
|
f8c11c8b6b | ||
|
|
ba5421e77b | ||
|
|
20fa803cee | ||
|
|
393fa78a43 | ||
|
|
3f290dae06 | ||
|
|
24d18b9f2f | ||
|
|
79ef8ca371 | ||
|
|
ec12f21113 | ||
|
|
2e8ecc7277 | ||
|
|
fc28b06a0f | ||
|
|
8fdbfdc04c | ||
|
|
bdfcfc96a8 | ||
|
|
bb8649bc81 | ||
|
|
777e3d58b7 | ||
|
|
c552f1aab6 | ||
|
|
c0f2fa3042 | ||
|
|
05936f82bd | ||
|
|
c7db81c97c | ||
|
|
bd1a37b8ef | ||
|
|
efc4e6a8ed | ||
|
|
dd5d87e91e | ||
|
|
ca6df488c5 | ||
|
|
1e018a6aa5 | ||
|
|
0a627f96be | ||
|
|
17a8e67d8a | ||
|
|
815c2c5ad5 | ||
|
|
4376de85ff | ||
|
|
7e89de4612 | ||
|
|
4b72a14706 | ||
|
|
b34f6fedb6 | ||
|
|
58af0d78af | ||
|
|
ca13d9109c | ||
|
|
e103fb5876 | ||
|
|
58178f4563 | ||
|
|
04f1879fd1 | ||
|
|
f5bc9ced0a | ||
|
|
7fe9993f91 | ||
|
|
7c95339324 | ||
|
|
006442f9de | ||
|
|
e20100e437 | ||
|
|
d7cf2b37d5 | ||
|
|
278c2b9aae | ||
|
|
944246fcf5 | ||
|
|
03d87f4993 | ||
|
|
cf8cab5f77 | ||
|
|
49d1376647 | ||
|
|
de5518d262 | ||
|
|
7e4c51f47f | ||
|
|
fe1d153632 | ||
|
|
a98f9ab80e | ||
|
|
867afaf265 | ||
|
|
3d2ec64b14 | ||
|
|
bb407c0b42 | ||
|
|
83c3d901c7 | ||
|
|
901cee903c | ||
|
|
250ea09c7e | ||
|
|
648d59631b | ||
|
|
ed06e3c491 | ||
|
|
3e8d646edd | ||
|
|
9c2c698575 | ||
|
|
e2b0a286a4 | ||
|
|
154809f0f9 | ||
|
|
8d9a51a7c4 | ||
|
|
b3294369d4 | ||
|
|
53730920e3 | ||
|
|
d73b814277 | ||
|
|
dd0050c066 | ||
|
|
ae51ee3e26 | ||
|
|
4b16e5d65f | ||
|
|
4f73bba132 | ||
|
|
3c229602e4 | ||
|
|
c74c902ebc | ||
|
|
8bfd315ba3 | ||
|
|
9d75c47792 | ||
|
|
e183be1a5c | ||
|
|
7e273ce63d | ||
|
|
6d070e75b0 | ||
|
|
f4f96fd18e | ||
|
|
ddd6420d9b | ||
|
|
d76f42296a | ||
|
|
47a6118ffb | ||
|
|
dbd205b73f | ||
|
|
7ef4be26ed | ||
|
|
c6b1979391 | ||
|
|
0f390e65a4 | ||
|
|
5dc0f4e270 | ||
|
|
223288cc52 | ||
|
|
e1f07884b9 | ||
|
|
e00e61edfa | ||
|
|
4f988e186a | ||
|
|
1aa54faa35 | ||
|
|
0bb9247609 | ||
|
|
d841933b21 | ||
|
|
bc8b78a01b | ||
|
|
c6e72be483 | ||
|
|
ef7dd6c8fb | ||
|
|
e6b90385b2 | ||
|
|
61181c6791 | ||
|
|
d2cccd2422 | ||
|
|
b05ebe1598 | ||
|
|
d061f7589c | ||
|
|
15903faf49 | ||
|
|
1908b1a5a6 | ||
|
|
037f472f8c | ||
|
|
a32c1f40b1 | ||
|
|
a00aa27ae4 | ||
|
|
544be77bdc | ||
|
|
b8a110a772 | ||
|
|
7788a2d6bd | ||
|
|
da17fd16fa | ||
|
|
e670f80fed | ||
|
|
2de28b9926 | ||
|
|
d7586af392 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
|||||||
liberapay: # Replace with a single Liberapay username
|
liberapay: # Replace with a single Liberapay username
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
otechie: # Replace with a single Otechie username
|
otechie: # Replace with a single Otechie username
|
||||||
custom: ['https://mempool.space/about'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
custom: ['https://mempool.space/sponsor'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
|
|||||||
49
.github/workflows/cypress.yml
vendored
49
.github/workflows/cypress.yml
vendored
@@ -15,17 +15,60 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: ${{ matrix.browser }} browser tests
|
- name: ${{ matrix.browser }} browser tests (Mempool)
|
||||||
uses: cypress-io/github-action@v2
|
uses: cypress-io/github-action@v2
|
||||||
with:
|
with:
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
build: npm run config:defaults
|
build: npm run config:defaults:mempool
|
||||||
start: npm run start:local-prod
|
start: npm run start:local-prod
|
||||||
wait-on: 'http://localhost:4200'
|
wait-on: 'http://localhost:4200'
|
||||||
wait-on-timeout: 120
|
wait-on-timeout: 120
|
||||||
record: true
|
record: true
|
||||||
parallel: true
|
parallel: true
|
||||||
group: Tests on ${{ matrix.browser }}
|
env: BASE_MODULE=mempool
|
||||||
|
group: Tests on ${{ matrix.browser }} (Mempool)
|
||||||
|
browser: ${{ matrix.browser }}
|
||||||
|
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
||||||
|
env:
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
|
- name: ${{ matrix.browser }} browser tests (Liquid)
|
||||||
|
uses: cypress-io/github-action@v2
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
working-directory: frontend
|
||||||
|
build: npm run config:defaults:liquid
|
||||||
|
start: npm run start:local-prod
|
||||||
|
wait-on: 'http://localhost:4200'
|
||||||
|
wait-on-timeout: 120
|
||||||
|
record: true
|
||||||
|
parallel: true
|
||||||
|
spec: cypress/integration/liquid/liquid.spec.ts
|
||||||
|
env: BASE_MODULE=liquid
|
||||||
|
group: Tests on ${{ matrix.browser }} (Liquid)
|
||||||
|
browser: ${{ matrix.browser }}
|
||||||
|
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
||||||
|
env:
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
|
- name: ${{ matrix.browser }} browser tests (Bisq)
|
||||||
|
uses: cypress-io/github-action@v2
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
working-directory: frontend
|
||||||
|
build: npm run config:defaults:bisq
|
||||||
|
start: npm run start:local-prod
|
||||||
|
wait-on: 'http://localhost:4200'
|
||||||
|
wait-on-timeout: 120
|
||||||
|
record: true
|
||||||
|
parallel: true
|
||||||
|
spec: cypress/integration/bisq/bisq.spec.ts
|
||||||
|
env: BASE_MODULE=bisq
|
||||||
|
group: Tests on ${{ matrix.browser }} (Bisq)
|
||||||
browser: ${{ matrix.browser }}
|
browser: ${{ matrix.browser }}
|
||||||
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
|
||||||
env:
|
env:
|
||||||
|
|||||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"typescript.tsdk": "./backend/node_modules/typescript/lib"
|
||||||
|
}
|
||||||
4
backend/.vscode/settings.json
vendored
Normal file
4
backend/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"typescript.tsdk": "../backend/node_modules/typescript/lib"
|
||||||
|
}
|
||||||
@@ -8,7 +8,11 @@
|
|||||||
"POLL_RATE_MS": 2000,
|
"POLL_RATE_MS": 2000,
|
||||||
"CACHE_DIR": "./cache",
|
"CACHE_DIR": "./cache",
|
||||||
"CLEAR_PROTECTION_MINUTES": 20,
|
"CLEAR_PROTECTION_MINUTES": 20,
|
||||||
"RECOMMENDED_FEE_PERCENTILE": 50
|
"RECOMMENDED_FEE_PERCENTILE": 50,
|
||||||
|
"BLOCK_WEIGHT_UNITS": 4000000,
|
||||||
|
"INITIAL_BLOCKS_AMOUNT": 8,
|
||||||
|
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
||||||
|
"PRICE_FEED_UPDATE_INTERVAL": 3600
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
|
|||||||
22
backend/package-lock.json
generated
22
backend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "2.2.1",
|
"version": "2.2.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "2.2.1",
|
"version": "2.2.2",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mempool/bitcoin": "^3.0.3",
|
"@mempool/bitcoin": "^3.0.3",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"@types/locutus": "^0.0.6",
|
"@types/locutus": "^0.0.6",
|
||||||
"@types/ws": "^7.4.4",
|
"@types/ws": "^7.4.4",
|
||||||
"tslint": "^6.1.0",
|
"tslint": "^6.1.0",
|
||||||
"typescript": "^4.1.5"
|
"typescript": "4.4.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -1473,10 +1473,11 @@
|
|||||||
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "4.2.3",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
|
||||||
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
|
"integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -2770,9 +2771,9 @@
|
|||||||
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "4.2.3",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
|
||||||
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
|
"integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"unpipe": {
|
"unpipe": {
|
||||||
@@ -2820,7 +2821,8 @@
|
|||||||
"ws": {
|
"ws": {
|
||||||
"version": "7.4.6",
|
"version": "7.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="
|
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "2.2.1",
|
"version": "2.2.2",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
@@ -45,6 +45,6 @@
|
|||||||
"@types/locutus": "^0.0.6",
|
"@types/locutus": "^0.0.6",
|
||||||
"@types/ws": "^7.4.4",
|
"@types/ws": "^7.4.4",
|
||||||
"tslint": "^6.1.0",
|
"tslint": "^6.1.0",
|
||||||
"typescript": "^4.1.5"
|
"typescript": "4.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class BackendInfo {
|
|||||||
try {
|
try {
|
||||||
this.gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim();
|
this.gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Could not load git commit info: ' + e.message || e);
|
logger.err('Could not load git commit info: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ class BackendInfo {
|
|||||||
const packageJson = fs.readFileSync('package.json').toString();
|
const packageJson = fs.readFileSync('package.json').toString();
|
||||||
this.version = JSON.parse(packageJson).version;
|
this.version = JSON.parse(packageJson).version;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(e);
|
throw new Error(e instanceof Error ? e.message : 'Error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ class Bisq {
|
|||||||
this.buildIndex();
|
this.buildIndex();
|
||||||
this.calculateStats();
|
this.calculateStats();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.info('loadBisqDumpFile() error.' + e.message || e);
|
logger.info('loadBisqDumpFile() error.' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class Bisq {
|
|||||||
logger.debug('Bisq market data updated in ' + time + ' ms');
|
logger.debug('Bisq market data updated in ' + time + ' ms');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('loadBisqMarketDataDumpFile() error.' + e.message || e);
|
logger.err('loadBisqMarketDataDumpFile() error.' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,12 +98,15 @@ export namespace IBitcoinApi {
|
|||||||
|
|
||||||
export interface AddressInformation {
|
export interface AddressInformation {
|
||||||
isvalid: boolean; // (boolean) If the address is valid or not. If not, this is the only property returned.
|
isvalid: boolean; // (boolean) If the address is valid or not. If not, this is the only property returned.
|
||||||
|
isvalid_parent?: boolean; // (boolean) Elements only
|
||||||
address: string; // (string) The bitcoin address validated
|
address: string; // (string) The bitcoin address validated
|
||||||
scriptPubKey: string; // (string) The hex-encoded scriptPubKey generated by the address
|
scriptPubKey: string; // (string) The hex-encoded scriptPubKey generated by the address
|
||||||
isscript: boolean; // (boolean) If the key is a script
|
isscript: boolean; // (boolean) If the key is a script
|
||||||
iswitness: boolean; // (boolean) If the address is a witness
|
iswitness: boolean; // (boolean) If the address is a witness
|
||||||
witness_version?: boolean; // (numeric, optional) The version number of the witness program
|
witness_version?: boolean; // (numeric, optional) The version number of the witness program
|
||||||
witness_program: string; // (string, optional) The hex value of the witness program
|
witness_program: string; // (string, optional) The hex value of the witness program
|
||||||
|
confidential_key?: string; // (string) Elements only
|
||||||
|
unconfidential?: string; // (string) Elements only
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChainTips {
|
export interface ChainTips {
|
||||||
|
|||||||
@@ -60,13 +60,12 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
return this.bitcoindClient.getBlock(hash, 0);
|
return this.bitcoindClient.getBlock(hash, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$getBlockHash(height: number): Promise<string> {
|
$getBlockHash(height: number): Promise<string> {
|
||||||
return this.bitcoindClient.getBlockHash(height);
|
return this.bitcoindClient.getBlockHash(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getBlockHeader(hash: string): Promise<string> {
|
$getBlockHeader(hash: string): Promise<string> {
|
||||||
return this.bitcoindClient.getBlockHeader(hash,false);
|
return this.bitcoindClient.getBlockHeader(hash, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $getBlock(hash: string): Promise<IEsploraApi.Block> {
|
async $getBlock(hash: string): Promise<IEsploraApi.Block> {
|
||||||
@@ -238,10 +237,6 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected $validateAddress(address: string): Promise<IBitcoinApi.AddressInformation> {
|
|
||||||
return this.bitcoindClient.validateAddress(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
private $getMempoolEntry(txid: string): Promise<IBitcoinApi.MempoolEntry> {
|
private $getMempoolEntry(txid: string): Promise<IBitcoinApi.MempoolEntry> {
|
||||||
return this.bitcoindClient.getMempoolEntry(txid);
|
return this.bitcoindClient.getMempoolEntry(txid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ class BitcoinBaseApi {
|
|||||||
$getBlockchainInfo(): Promise<IBitcoinApi.BlockchainInfo> {
|
$getBlockchainInfo(): Promise<IBitcoinApi.BlockchainInfo> {
|
||||||
return this.bitcoindClient.getBlockchainInfo();
|
return this.bitcoindClient.getBlockchainInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$validateAddress(address: string): Promise<IBitcoinApi.AddressInformation> {
|
||||||
|
return this.bitcoindClient.validateAddress(address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new BitcoinBaseApi();
|
export default new BitcoinBaseApi();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import * as sha256 from 'crypto-js/sha256';
|
|||||||
import * as hexEnc from 'crypto-js/enc-hex';
|
import * as hexEnc from 'crypto-js/enc-hex';
|
||||||
import loadingIndicators from '../loading-indicators';
|
import loadingIndicators from '../loading-indicators';
|
||||||
import memoryCache from '../memory-cache';
|
import memoryCache from '../memory-cache';
|
||||||
|
import bitcoinBaseApi from './bitcoin-base.api';
|
||||||
|
|
||||||
class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
||||||
private electrumClient: any;
|
private electrumClient: any;
|
||||||
@@ -44,7 +45,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async $getAddress(address: string): Promise<IEsploraApi.Address> {
|
async $getAddress(address: string): Promise<IEsploraApi.Address> {
|
||||||
const addressInfo = await this.$validateAddress(address);
|
const addressInfo = await bitcoinBaseApi.$validateAddress(address);
|
||||||
if (!addressInfo || !addressInfo.isvalid) {
|
if (!addressInfo || !addressInfo.isvalid) {
|
||||||
return ({
|
return ({
|
||||||
'address': address,
|
'address': address,
|
||||||
@@ -93,12 +94,12 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
|||||||
if (e === 'failed to get confirmed status') {
|
if (e === 'failed to get confirmed status') {
|
||||||
e = 'The number of transactions on this address exceeds the Electrum server limit';
|
e = 'The number of transactions on this address exceeds the Electrum server limit';
|
||||||
}
|
}
|
||||||
throw new Error(e);
|
throw new Error(typeof e === 'string' ? e : 'Error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
|
async $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
|
||||||
const addressInfo = await this.$validateAddress(address);
|
const addressInfo = await bitcoinBaseApi.$validateAddress(address);
|
||||||
if (!addressInfo || !addressInfo.isvalid) {
|
if (!addressInfo || !addressInfo.isvalid) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -131,7 +132,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
|||||||
if (e === 'failed to get confirmed status') {
|
if (e === 'failed to get confirmed status') {
|
||||||
e = 'The number of transactions on this address exceeds the Electrum server limit';
|
e = 'The number of transactions on this address exceeds the Electrum server limit';
|
||||||
}
|
}
|
||||||
throw new Error(e);
|
throw new Error(typeof e === 'string' ? e : 'Error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import transactionUtils from './transaction-utils';
|
|||||||
import bitcoinBaseApi from './bitcoin/bitcoin-base.api';
|
import bitcoinBaseApi from './bitcoin/bitcoin-base.api';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private static INITIAL_BLOCK_AMOUNT = 8;
|
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
private currentBlockHeight = 0;
|
private currentBlockHeight = 0;
|
||||||
private currentDifficulty = 0;
|
private currentDifficulty = 0;
|
||||||
@@ -35,14 +34,14 @@ class Blocks {
|
|||||||
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
|
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
|
||||||
|
|
||||||
if (this.blocks.length === 0) {
|
if (this.blocks.length === 0) {
|
||||||
this.currentBlockHeight = blockHeightTip - Blocks.INITIAL_BLOCK_AMOUNT;
|
this.currentBlockHeight = blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT;
|
||||||
} else {
|
} else {
|
||||||
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
|
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blockHeightTip - this.currentBlockHeight > Blocks.INITIAL_BLOCK_AMOUNT * 2) {
|
if (blockHeightTip - this.currentBlockHeight > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 2) {
|
||||||
logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${Blocks.INITIAL_BLOCK_AMOUNT} recent blocks`);
|
logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${config.MEMPOOL.INITIAL_BLOCKS_AMOUNT} recent blocks`);
|
||||||
this.currentBlockHeight = blockHeightTip - Blocks.INITIAL_BLOCK_AMOUNT;
|
this.currentBlockHeight = blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.lastDifficultyAdjustmentTime) {
|
if (!this.lastDifficultyAdjustmentTime) {
|
||||||
@@ -90,7 +89,7 @@ class Blocks {
|
|||||||
const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
|
const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
|
||||||
transactions.push(tx);
|
transactions.push(tx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error fetching block tx: ' + e.message || e);
|
logger.debug('Error fetching block tx: ' + (e instanceof Error ? e.message : e));
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
throw new Error('Failed to fetch Coinbase transaction: ' + txIds[i]);
|
throw new Error('Failed to fetch Coinbase transaction: ' + txIds[i]);
|
||||||
}
|
}
|
||||||
@@ -111,8 +110,8 @@ class Blocks {
|
|||||||
blockExtended.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
|
blockExtended.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
|
||||||
transactions.shift();
|
transactions.shift();
|
||||||
transactions.sort((a, b) => b.effectiveFeePerVsize - a.effectiveFeePerVsize);
|
transactions.sort((a, b) => b.effectiveFeePerVsize - a.effectiveFeePerVsize);
|
||||||
blockExtended.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.effectiveFeePerVsize)) : 0;
|
blockExtended.medianFee = transactions.length > 0 ? Common.median(transactions.map((tx) => tx.effectiveFeePerVsize)) : 0;
|
||||||
blockExtended.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions, 8) : [0, 0];
|
blockExtended.feeRange = transactions.length > 0 ? Common.getFeesInRange(transactions, 8) : [0, 0];
|
||||||
|
|
||||||
if (block.height % 2016 === 0) {
|
if (block.height % 2016 === 0) {
|
||||||
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
|
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
|
||||||
@@ -121,8 +120,8 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.blocks.push(blockExtended);
|
this.blocks.push(blockExtended);
|
||||||
if (this.blocks.length > Blocks.INITIAL_BLOCK_AMOUNT * 4) {
|
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
|
||||||
this.blocks = this.blocks.slice(-Blocks.INITIAL_BLOCK_AMOUNT * 4);
|
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.newBlockCallbacks.length) {
|
if (this.newBlockCallbacks.length) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
||||||
|
import config from '../config';
|
||||||
export class Common {
|
export class Common {
|
||||||
static median(numbers: number[]) {
|
static median(numbers: number[]) {
|
||||||
let medianNr = 0;
|
let medianNr = 0;
|
||||||
@@ -105,7 +105,7 @@ export class Common {
|
|||||||
totalFees += tx.bestDescendant.fee;
|
totalFees += tx.bestDescendant.fee;
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.effectiveFeePerVsize = Math.max(1, totalFees / (totalWeight / 4));
|
tx.effectiveFeePerVsize = Math.max(config.MEMPOOL.NETWORK === 'liquid' ? 0.1 : 1, totalFees / (totalWeight / 4));
|
||||||
tx.cpfpChecked = true;
|
tx.cpfpChecked = true;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class DiskCache {
|
|||||||
logger.debug('Mempool and blocks data saved to disk cache');
|
logger.debug('Mempool and blocks data saved to disk cache');
|
||||||
this.isWritingCache = false;
|
this.isWritingCache = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Error writing to cache file: ' + e.message || e);
|
logger.warn('Error writing to cache file: ' + (e instanceof Error ? e.message : e));
|
||||||
this.isWritingCache = false;
|
this.isWritingCache = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { IConversionRates } from '../mempool.interfaces';
|
import { IConversionRates } from '../mempool.interfaces';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
class FiatConversion {
|
class FiatConversion {
|
||||||
private conversionRates: IConversionRates = {
|
private conversionRates: IConversionRates = {
|
||||||
@@ -16,7 +17,7 @@ class FiatConversion {
|
|||||||
|
|
||||||
public startService() {
|
public startService() {
|
||||||
logger.info('Starting currency rates service');
|
logger.info('Starting currency rates service');
|
||||||
setInterval(this.updateCurrency.bind(this), 1000 * 60);
|
setInterval(this.updateCurrency.bind(this), 1000 * config.MEMPOOL.PRICE_FEED_UPDATE_INTERVAL);
|
||||||
this.updateCurrency();
|
this.updateCurrency();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ class FiatConversion {
|
|||||||
this.ratesChangedCallback(this.conversionRates);
|
this.ratesChangedCallback(this.conversionRates);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Error updating fiat conversion rates: ' + e);
|
logger.err('Error updating fiat conversion rates: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Common } from './common';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
|
||||||
class MempoolBlocks {
|
class MempoolBlocks {
|
||||||
private static DEFAULT_PROJECTED_BLOCKS_AMOUNT = 8;
|
|
||||||
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
@@ -72,29 +71,29 @@ class MempoolBlocks {
|
|||||||
|
|
||||||
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
|
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
|
||||||
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||||
let blockVSize = 0;
|
let blockWeight = 0;
|
||||||
let blockSize = 0;
|
let blockSize = 0;
|
||||||
let transactions: TransactionExtended[] = [];
|
let transactions: TransactionExtended[] = [];
|
||||||
transactionsSorted.forEach((tx) => {
|
transactionsSorted.forEach((tx) => {
|
||||||
if (blockVSize + tx.vsize <= 1000000 || mempoolBlocks.length === MempoolBlocks.DEFAULT_PROJECTED_BLOCKS_AMOUNT - 1) {
|
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS || mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT) {
|
||||||
blockVSize += tx.vsize;
|
blockWeight += tx.weight;
|
||||||
blockSize += tx.size;
|
blockSize += tx.size;
|
||||||
transactions.push(tx);
|
transactions.push(tx);
|
||||||
} else {
|
} else {
|
||||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockVSize, mempoolBlocks.length));
|
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
|
||||||
blockVSize = tx.vsize;
|
blockWeight = tx.weight;
|
||||||
blockSize = tx.size;
|
blockSize = tx.size;
|
||||||
transactions = [tx];
|
transactions = [tx];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (transactions.length) {
|
if (transactions.length) {
|
||||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockVSize, mempoolBlocks.length));
|
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
|
||||||
}
|
}
|
||||||
return mempoolBlocks;
|
return mempoolBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private dataToMempoolBlocks(transactions: TransactionExtended[],
|
private dataToMempoolBlocks(transactions: TransactionExtended[],
|
||||||
blockSize: number, blockVSize: number, blocksIndex: number): MempoolBlockWithTransactions {
|
blockSize: number, blockWeight: number, blocksIndex: number): MempoolBlockWithTransactions {
|
||||||
let rangeLength = 4;
|
let rangeLength = 4;
|
||||||
if (blocksIndex === 0) {
|
if (blocksIndex === 0) {
|
||||||
rangeLength = 8;
|
rangeLength = 8;
|
||||||
@@ -106,7 +105,7 @@ class MempoolBlocks {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
blockSize: blockSize,
|
blockSize: blockSize,
|
||||||
blockVSize: blockVSize,
|
blockVSize: blockWeight / 4,
|
||||||
nTx: transactions.length,
|
nTx: transactions.length,
|
||||||
totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
|
totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
|
||||||
medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
|
medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
newTransactions.push(transaction);
|
newTransactions.push(transaction);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return result.insertId;
|
return result.insertId;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$create() error' + e.message || e);
|
logger.err('$create() error' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ class Statistics {
|
|||||||
return this.mapStatisticToOptimizedStatistic([rows[0]])[0];
|
return this.mapStatisticToOptimizedStatistic([rows[0]])[0];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list2H() error' + e.message || e);
|
logger.err('$list2H() error' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list2H() error' + e.message || e);
|
logger.err('$list2H() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,7 +338,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list24h() error' + e.message || e);
|
logger.err('$list24h() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,7 +351,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list1W() error' + e);
|
logger.err('$list1W() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,7 +364,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list1M() error' + e);
|
logger.err('$list1M() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,7 +377,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list3M() error' + e);
|
logger.err('$list3M() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,7 +390,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list6M() error' + e);
|
logger.err('$list6M() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,7 +403,7 @@ class Statistics {
|
|||||||
connection.release();
|
connection.release();
|
||||||
return this.mapStatisticToOptimizedStatistic(rows);
|
return this.mapStatisticToOptimizedStatistic(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$list6M() error' + e);
|
logger.err('$list6M() error' + (e instanceof Error ? e.message : e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
import logger from '../logger';
|
|
||||||
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
||||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
class TransactionUtils {
|
class TransactionUtils {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@@ -31,7 +31,7 @@ class TransactionUtils {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
const feePerVbytes = Math.max(1, (transaction.fee || 0) / (transaction.weight / 4));
|
const feePerVbytes = Math.max(config.MEMPOOL.NETWORK === 'liquid' ? 0.1 : 1, (transaction.fee || 0) / (transaction.weight / 4));
|
||||||
const transactionExtended: TransactionExtended = Object.assign({
|
const transactionExtended: TransactionExtended = Object.assign({
|
||||||
vsize: Math.round(transaction.weight / 4),
|
vsize: Math.round(transaction.weight / 4),
|
||||||
feePerVsize: feePerVbytes,
|
feePerVsize: feePerVbytes,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class WebsocketHandler {
|
|||||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||||
response['tx'] = fullTx;
|
response['tx'] = fullTx;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction: ' + e.message || e);
|
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -69,7 +69,7 @@ class WebsocketHandler {
|
|||||||
const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
|
const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
|
||||||
response['tx'] = fullTx;
|
response['tx'] = fullTx;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction. ' + e.message || e);
|
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
|
||||||
client['track-mempool-tx'] = parsedMessage['track-tx'];
|
client['track-mempool-tx'] = parsedMessage['track-tx'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,9 +80,13 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessage && parsedMessage['track-address']) {
|
if (parsedMessage && parsedMessage['track-address']) {
|
||||||
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87})$/
|
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/
|
||||||
.test(parsedMessage['track-address'])) {
|
.test(parsedMessage['track-address'])) {
|
||||||
client['track-address'] = parsedMessage['track-address'];
|
let matchedAddress = parsedMessage['track-address'];
|
||||||
|
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) {
|
||||||
|
matchedAddress = matchedAddress.toLowerCase();
|
||||||
|
}
|
||||||
|
client['track-address'] = matchedAddress;
|
||||||
} else {
|
} else {
|
||||||
client['track-address'] = null;
|
client['track-address'] = null;
|
||||||
}
|
}
|
||||||
@@ -97,7 +101,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessage.action === 'init') {
|
if (parsedMessage.action === 'init') {
|
||||||
const _blocks = blocks.getBlocks().slice(-8);
|
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||||
if (!_blocks) {
|
if (!_blocks) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -124,7 +128,7 @@ class WebsocketHandler {
|
|||||||
client.send(JSON.stringify(response));
|
client.send(JSON.stringify(response));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error parsing websocket message: ' + e.message || e);
|
logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -173,7 +177,7 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
getInitData(_blocks?: BlockExtended[]) {
|
getInitData(_blocks?: BlockExtended[]) {
|
||||||
if (!_blocks) {
|
if (!_blocks) {
|
||||||
_blocks = blocks.getBlocks().slice(-8);
|
_blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'mempoolInfo': memPool.getMempoolInfo(),
|
'mempoolInfo': memPool.getMempoolInfo(),
|
||||||
@@ -252,7 +256,7 @@ class WebsocketHandler {
|
|||||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||||
response['tx'] = fullTx;
|
response['tx'] = fullTx;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
response['tx'] = tx;
|
response['tx'] = tx;
|
||||||
@@ -272,7 +276,7 @@ class WebsocketHandler {
|
|||||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||||
foundTransactions.push(fullTx);
|
foundTransactions.push(fullTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
foundTransactions.push(tx);
|
foundTransactions.push(tx);
|
||||||
@@ -286,7 +290,7 @@ class WebsocketHandler {
|
|||||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||||
foundTransactions.push(fullTx);
|
foundTransactions.push(fullTx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
foundTransactions.push(tx);
|
foundTransactions.push(tx);
|
||||||
@@ -337,7 +341,7 @@ class WebsocketHandler {
|
|||||||
const fullTx = await transactionUtils.$getTransactionExtended(rbfTransaction, true);
|
const fullTx = await transactionUtils.$getTransactionExtended(rbfTransaction, true);
|
||||||
response['rbfTransaction'] = fullTx;
|
response['rbfTransaction'] = fullTx;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
response['rbfTransaction'] = rbfTx;
|
response['rbfTransaction'] = rbfTx;
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ interface IConfig {
|
|||||||
CACHE_DIR: string;
|
CACHE_DIR: string;
|
||||||
CLEAR_PROTECTION_MINUTES: number;
|
CLEAR_PROTECTION_MINUTES: number;
|
||||||
RECOMMENDED_FEE_PERCENTILE: number;
|
RECOMMENDED_FEE_PERCENTILE: number;
|
||||||
|
BLOCK_WEIGHT_UNITS: number;
|
||||||
|
INITIAL_BLOCKS_AMOUNT: number;
|
||||||
|
MEMPOOL_BLOCKS_AMOUNT: number;
|
||||||
|
PRICE_FEED_UPDATE_INTERVAL: number;
|
||||||
};
|
};
|
||||||
ESPLORA: {
|
ESPLORA: {
|
||||||
REST_API_URL: string;
|
REST_API_URL: string;
|
||||||
@@ -69,6 +73,10 @@ const defaults: IConfig = {
|
|||||||
'CACHE_DIR': './cache',
|
'CACHE_DIR': './cache',
|
||||||
'CLEAR_PROTECTION_MINUTES': 20,
|
'CLEAR_PROTECTION_MINUTES': 20,
|
||||||
'RECOMMENDED_FEE_PERCENTILE': 50,
|
'RECOMMENDED_FEE_PERCENTILE': 50,
|
||||||
|
'BLOCK_WEIGHT_UNITS': 4000000,
|
||||||
|
'INITIAL_BLOCKS_AMOUNT': 8,
|
||||||
|
'MEMPOOL_BLOCKS_AMOUNT': 8,
|
||||||
|
'PRICE_FEED_UPDATE_INTERVAL': 3600,
|
||||||
},
|
},
|
||||||
'ESPLORA': {
|
'ESPLORA': {
|
||||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export async function checkDbConnection() {
|
|||||||
logger.info('Database connection established.');
|
logger.info('Database connection established.');
|
||||||
connection.release();
|
connection.release();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Could not connect to database: ' + e.message || e);
|
logger.err('Could not connect to database: ' + (e instanceof Error ? e.message : e));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class Server {
|
|||||||
try {
|
try {
|
||||||
await memPool.$updateMemPoolInfo();
|
await memPool.$updateMemPoolInfo();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = `updateMempoolInfo: ${(e.message || e)}`;
|
const msg = `updateMempoolInfo: ${(e instanceof Error ? e.message : e)}`;
|
||||||
if (config.CORE_RPC_MINFEE.ENABLED) {
|
if (config.CORE_RPC_MINFEE.ENABLED) {
|
||||||
logger.warn(msg);
|
logger.warn(msg);
|
||||||
} else {
|
} else {
|
||||||
@@ -123,7 +123,7 @@ class Server {
|
|||||||
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
|
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
|
||||||
this.currentBackendRetryInterval = 5;
|
this.currentBackendRetryInterval = 5;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const loggerMsg = `runMainLoop error: ${(e.message || e)}. Retrying in ${this.currentBackendRetryInterval} sec.`;
|
const loggerMsg = `runMainLoop error: ${(e instanceof Error ? e.message : e)}. Retrying in ${this.currentBackendRetryInterval} sec.`;
|
||||||
if (this.currentBackendRetryInterval > 5) {
|
if (this.currentBackendRetryInterval > 5) {
|
||||||
logger.warn(loggerMsg);
|
logger.warn(loggerMsg);
|
||||||
mempool.setOutOfSync();
|
mempool.setOutOfSync();
|
||||||
@@ -158,6 +158,7 @@ class Server {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', routes.getMempoolBlocks)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', routes.getMempoolBlocks)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', routes.getBackendInfo)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', routes.getBackendInfo)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', routes.getInitData)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', routes.getInitData)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', routes.validateAddress)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('https://mempool.space/api/v1/donations', { responseType: 'stream', timeout: 10000 });
|
const response = await axios.get('https://mempool.space/api/v1/donations', { responseType: 'stream', timeout: 10000 });
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import transactionUtils from './api/transaction-utils';
|
|||||||
import blocks from './api/blocks';
|
import blocks from './api/blocks';
|
||||||
import loadingIndicators from './api/loading-indicators';
|
import loadingIndicators from './api/loading-indicators';
|
||||||
import { Common } from './api/common';
|
import { Common } from './api/common';
|
||||||
|
import bitcoinBaseApi from './api/bitcoin/bitcoin-base.api';
|
||||||
|
|
||||||
class Routes {
|
class Routes {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
@@ -55,7 +56,7 @@ class Routes {
|
|||||||
const result = websocketHandler.getInitData();
|
const result = websocketHandler.getInitData();
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ class Routes {
|
|||||||
const result = mempoolBlocks.getMempoolBlocks();
|
const result = mempoolBlocks.getMempoolBlocks();
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,10 +478,10 @@ class Routes {
|
|||||||
res.json(transaction);
|
res.json(transaction);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let statusCode = 500;
|
let statusCode = 500;
|
||||||
if (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;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e.message || e);
|
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,10 +492,10 @@ class Routes {
|
|||||||
res.send(transaction.hex);
|
res.send(transaction.hex);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let statusCode = 500;
|
let statusCode = 500;
|
||||||
if (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;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e.message || e);
|
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,10 +505,10 @@ class Routes {
|
|||||||
res.json(transaction.status);
|
res.json(transaction.status);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let statusCode = 500;
|
let statusCode = 500;
|
||||||
if (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;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e.message || e);
|
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +517,7 @@ class Routes {
|
|||||||
const result = await bitcoinApi.$getBlock(req.params.hash);
|
const result = await bitcoinApi.$getBlock(req.params.hash);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,7 +527,7 @@ class Routes {
|
|||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
res.send(blockHeader);
|
res.send(blockHeader);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,7 +564,7 @@ class Routes {
|
|||||||
res.json(returnBlocks);
|
res.json(returnBlocks);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadingIndicators.setProgress('blocks', 100);
|
loadingIndicators.setProgress('blocks', 100);
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,13 +583,13 @@ class Routes {
|
|||||||
transactions.push(transaction);
|
transactions.push(transaction);
|
||||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i + 1) / endIndex * 100);
|
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i + 1) / endIndex * 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('getBlockTransactions error: ' + e.message || e);
|
logger.debug('getBlockTransactions error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
|
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,7 +598,7 @@ class Routes {
|
|||||||
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) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,10 +612,10 @@ class Routes {
|
|||||||
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.message && e.message.indexOf('exceeds') > 0) {
|
if (e instanceof Error && e.message && e.message.indexOf('exceeds') > 0) {
|
||||||
return res.status(413).send(e.message);
|
return res.status(413).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,10 +629,10 @@ class Routes {
|
|||||||
const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId);
|
const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId);
|
||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message && e.message.indexOf('exceeds') > 0) {
|
if (e instanceof Error && e.message && e.message.indexOf('exceeds') > 0) {
|
||||||
return res.status(413).send(e.message);
|
return res.status(413).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,7 +645,7 @@ class Routes {
|
|||||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||||
res.send(blockHash);
|
res.send(blockHash);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,7 +666,7 @@ class Routes {
|
|||||||
const rawMempool = await bitcoinApi.$getRawMempool();
|
const rawMempool = await bitcoinApi.$getRawMempool();
|
||||||
res.send(rawMempool);
|
res.send(rawMempool);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,7 +675,7 @@ class Routes {
|
|||||||
const result = await bitcoinApi.$getBlockHeightTip();
|
const result = await bitcoinApi.$getBlockHeightTip();
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,7 +684,16 @@ class Routes {
|
|||||||
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) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async validateAddress(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const result = await bitcoinBaseApi.$validateAddress(req.params.address);
|
||||||
|
res.json(result);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,42 +703,55 @@ class Routes {
|
|||||||
|
|
||||||
public getDifficultyChange(req: Request, res: Response) {
|
public getDifficultyChange(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const now = new Date().getTime() / 1000;
|
|
||||||
const DATime = blocks.getLastDifficultyAdjustmentTime();
|
const DATime = blocks.getLastDifficultyAdjustmentTime();
|
||||||
const previousRetarget = blocks.getPreviousDifficultyRetarget();
|
const previousRetarget = blocks.getPreviousDifficultyRetarget();
|
||||||
const diff = now - DATime;
|
|
||||||
const blockHeight = blocks.getCurrentBlockHeight();
|
const blockHeight = blocks.getCurrentBlockHeight();
|
||||||
|
|
||||||
|
const now = new Date().getTime() / 1000;
|
||||||
|
const diff = now - DATime;
|
||||||
const blocksInEpoch = blockHeight % 2016;
|
const blocksInEpoch = blockHeight % 2016;
|
||||||
const difficultyChange = (600 / (diff / blocksInEpoch) - 1) * 100;
|
const progressPercent = (blocksInEpoch >= 0) ? blocksInEpoch / 2016 * 100 : 100;
|
||||||
|
const remainingBlocks = 2016 - blocksInEpoch;
|
||||||
|
const nextRetargetHeight = blockHeight + remainingBlocks;
|
||||||
|
|
||||||
|
let difficultyChange = 0;
|
||||||
|
if (blocksInEpoch > 0) {
|
||||||
|
difficultyChange = (600 / (diff / blocksInEpoch ) - 1) * 100;
|
||||||
|
}
|
||||||
|
if (difficultyChange > 300) {
|
||||||
|
difficultyChange = 300;
|
||||||
|
}
|
||||||
|
if (difficultyChange < -75) {
|
||||||
|
difficultyChange = -75;
|
||||||
|
}
|
||||||
|
|
||||||
const timeAvgDiff = difficultyChange * 0.1;
|
const timeAvgDiff = difficultyChange * 0.1;
|
||||||
|
|
||||||
let timeAvgMins = 10;
|
let timeAvgMins = 10;
|
||||||
if (timeAvgDiff > 0 ){
|
if (timeAvgDiff > 0) {
|
||||||
timeAvgMins -= Math.abs(timeAvgDiff);
|
timeAvgMins -= Math.abs(timeAvgDiff);
|
||||||
} else {
|
} else {
|
||||||
timeAvgMins += Math.abs(timeAvgDiff);
|
timeAvgMins += Math.abs(timeAvgDiff);
|
||||||
}
|
}
|
||||||
|
|
||||||
const remainingBlocks = 2016 - blocksInEpoch;
|
const timeAvg = timeAvgMins * 60;
|
||||||
const timeAvgSeconds = timeAvgMins * 60;
|
const remainingTime = remainingBlocks * timeAvg;
|
||||||
const remainingTime = remainingBlocks * timeAvgSeconds;
|
const estimatedRetargetDate = remainingTime + now;
|
||||||
const estimatedRetargetDate = (remainingTime + now);
|
|
||||||
const totalTime = estimatedRetargetDate-DATime;
|
|
||||||
const progressPercent = 100 - ((remainingTime * 100) / totalTime);
|
|
||||||
|
|
||||||
const result={
|
const result = {
|
||||||
progressPercent,
|
progressPercent,
|
||||||
difficultyChange,
|
difficultyChange,
|
||||||
estimatedRetargetDate,
|
estimatedRetargetDate,
|
||||||
remainingBlocks,
|
remainingBlocks,
|
||||||
remainingTime,
|
remainingTime,
|
||||||
previousRetarget,
|
previousRetarget,
|
||||||
}
|
nextRetargetHeight,
|
||||||
|
timeAvg,
|
||||||
|
};
|
||||||
res.json(result);
|
res.json(result);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message || e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ COPY . .
|
|||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install -y build-essential python3 pkg-config
|
RUN apt-get install -y build-essential python3 pkg-config
|
||||||
RUN npm ci --production
|
RUN npm install
|
||||||
RUN npm i typescript
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:12-buster-slim
|
FROM node:12-buster-slim
|
||||||
|
|||||||
18
docker/scripts/get_image_digest.sh
Executable file
18
docker/scripts/get_image_digest.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
IMAGE=""
|
||||||
|
|
||||||
|
if [ -z "${VERSION}" ]; then
|
||||||
|
echo "no version provided (i.e, v2.2.0), using latest tag"
|
||||||
|
VERSION="latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for package in frontend backend; do
|
||||||
|
PACKAGE=mempool/"$package"
|
||||||
|
IMAGE="$PACKAGE":"$VERSION"
|
||||||
|
HASH=`docker pull $IMAGE > /dev/null && docker inspect $IMAGE | sed -n '/RepoDigests/{n;p;}' | grep -o '[0-9a-f]\{64\}'`
|
||||||
|
if [ -n "${HASH}" ]; then
|
||||||
|
echo "$IMAGE"@sha256:"$HASH"
|
||||||
|
fi
|
||||||
|
done
|
||||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@@ -59,3 +59,6 @@ generated-config.js
|
|||||||
cypress/videos
|
cypress/videos
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
|
|
||||||
|
# Base index
|
||||||
|
src/index.html
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,7 @@
|
|||||||
"verbose": true
|
"verbose": true
|
||||||
},
|
},
|
||||||
"local-prod": {
|
"local-prod": {
|
||||||
"proxyConfig": "proxy.prod.conf.json",
|
"proxyConfig": "proxy.conf.js",
|
||||||
"disableHostCheck": true,
|
"disableHostCheck": true,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"verbose": false
|
"verbose": false
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
describe('Bisq', () => {
|
describe('Bisq', () => {
|
||||||
|
let baseModule;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
baseModule = (Cypress.env('BASE_MODULE') && Cypress.env('BASE_MODULE') === 'bisq') ? '' : '/bisq';
|
||||||
|
|
||||||
cy.intercept('/sockjs-node/info*').as('socket');
|
cy.intercept('/sockjs-node/info*').as('socket');
|
||||||
cy.intercept('/bisq/api/markets/hloc?market=btc_usd&interval=day').as('hloc');
|
cy.intercept('/bisq/api/markets/hloc?market=btc_usd&interval=day').as('hloc');
|
||||||
cy.intercept('/bisq/api/markets/ticker').as('ticker');
|
cy.intercept('/bisq/api/markets/ticker').as('ticker');
|
||||||
@@ -20,62 +23,66 @@ describe('Bisq', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads the dashboard', () => {
|
if (Cypress.env("BASE_MODULE") === '' || Cypress.env("BASE_MODULE") !== 'liquid') {
|
||||||
cy.visit('/bisq');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the transactions screen', () => {
|
it('loads the dashboard', () => {
|
||||||
cy.visit('/bisq');
|
cy.visit(`${baseModule}`);
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
|
||||||
cy.get('.table > tr').should('have.length', 50);
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the blocks screen', () => {
|
it('loads the transactions screen', () => {
|
||||||
cy.visit('/bisq');
|
cy.visit(`${baseModule}`);
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
||||||
cy.wait('@blocks');
|
cy.get('.table > tr').should('have.length', 50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the blocks screen', () => {
|
||||||
|
cy.visit(`${baseModule}`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||||
|
cy.wait('@blocks');
|
||||||
|
cy.get('tbody tr').should('have.length', 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the stats screen', () => {
|
||||||
|
cy.visit(`${baseModule}`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||||
|
cy.wait('@stats');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the api screen', () => {
|
||||||
|
cy.visit(`${baseModule}`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||||
|
cy.get('.card').should('have.length.at.least', 1);
|
||||||
|
cy.get('.card').first().click();
|
||||||
|
cy.get('.card-body');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows blocks pagination with 5 pages (desktop)', () => {
|
||||||
|
cy.viewport(760, 800);
|
||||||
|
cy.visit(`${baseModule}/blocks`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
cy.get('tbody tr').should('have.length', 10);
|
cy.get('tbody tr').should('have.length', 10);
|
||||||
|
// 5 pages + 4 buttons = 9 buttons
|
||||||
|
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 9);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the stats screen', () => {
|
it('shows blocks pagination with 3 pages (mobile)', () => {
|
||||||
cy.visit('/bisq');
|
cy.viewport(669, 800);
|
||||||
cy.waitForSkeletonGone();
|
cy.visit(`${baseModule}/blocks`);
|
||||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
cy.waitForSkeletonGone();
|
||||||
cy.wait('@stats');
|
cy.get('tbody tr').should('have.length', 10);
|
||||||
|
// 3 pages + 4 buttons = 7 buttons
|
||||||
|
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 7);
|
||||||
});
|
});
|
||||||
});
|
} else {
|
||||||
|
it.skip("Tests cannot be run on the selected BASE_MODULE");
|
||||||
it('loads the api screen', () => {
|
}
|
||||||
cy.visit('/bisq');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
|
||||||
cy.get('.card').should('have.length.at.least', 1);
|
|
||||||
cy.get('.card').first().click();
|
|
||||||
cy.get('.card-body');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows blocks pagination with 5 pages (desktop)', () => {
|
|
||||||
cy.viewport(760, 800);
|
|
||||||
cy.visit('/bisq/blocks');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('tbody tr').should('have.length', 10);
|
|
||||||
// 5 pages + 4 buttons = 9 buttons
|
|
||||||
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 9);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows blocks pagination with 3 pages (mobile)', () => {
|
|
||||||
cy.viewport(669, 800);
|
|
||||||
cy.visit('/bisq/blocks');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('tbody tr').should('have.length', 10);
|
|
||||||
// 3 pages + 4 buttons = 7 buttons
|
|
||||||
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 7);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
describe('Liquid', () => {
|
describe('Liquid', () => {
|
||||||
|
let baseModule;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
baseModule = (Cypress.env('BASE_MODULE') && Cypress.env('BASE_MODULE') === 'liquid') ? '' : '/liquid';
|
||||||
|
|
||||||
cy.intercept('/liquid/api/block/**').as('block');
|
cy.intercept('/liquid/api/block/**').as('block');
|
||||||
cy.intercept('/liquid/api/blocks/').as('blocks');
|
cy.intercept('/liquid/api/blocks/').as('blocks');
|
||||||
cy.intercept('/liquid/api/tx/**/outspends').as('outspends');
|
cy.intercept('/liquid/api/tx/**/outspends').as('outspends');
|
||||||
@@ -13,137 +16,132 @@ describe('Liquid', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads the dashboard', () => {
|
if (Cypress.env("BASE_MODULE") === '' || Cypress.env("BASE_MODULE") !== 'bisq') {
|
||||||
cy.visit('/liquid');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the blocks page', () => {
|
it('loads the dashboard', () => {
|
||||||
cy.visit('/liquid/blocks');
|
cy.visit(`${baseModule}`);
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads a specific block page', () => {
|
|
||||||
cy.visit('/liquid/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the graphs page', () => {
|
|
||||||
cy.visit('/liquid/graphs');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the tv page - desktop', () => {
|
|
||||||
cy.visit('/liquid');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the graphs page - mobile', () => {
|
|
||||||
cy.visit('/liquid');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
|
||||||
cy.viewport('iphone-6');
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.get('.tv-only').should('not.exist');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('assets', () => {
|
|
||||||
it('shows the assets screen', () => {
|
|
||||||
cy.visit('/liquid');
|
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
});
|
||||||
cy.get('table tr').should('have.length', 5);
|
|
||||||
|
it('loads the blocks page', () => {
|
||||||
|
cy.visit(`${baseModule}/blocks`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads a specific block page', () => {
|
||||||
|
cy.visit(`${baseModule}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the graphs page', () => {
|
||||||
|
cy.visit(`${baseModule}/graphs`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the tv page - desktop', () => {
|
||||||
|
cy.visit(`${baseModule}`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows searching assets', () => {
|
it('loads the graphs page - mobile', () => {
|
||||||
cy.visit('/liquid');
|
cy.visit(`${baseModule}`)
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||||
|
cy.viewport('iphone-6');
|
||||||
|
cy.wait(1000);
|
||||||
|
cy.get('.tv-only').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('assets', () => {
|
||||||
|
it('shows the assets screen', () => {
|
||||||
|
cy.visit(`${baseModule}/assets`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('table tr').should('have.length.at.least', 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows searching assets', () => {
|
||||||
|
cy.visit(`${baseModule}/assets`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
|
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
|
||||||
cy.get('table tr').should('have.length', 1);
|
cy.get('table tr').should('have.length', 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('shows a specific asset ID', () => {
|
it('shows a specific asset ID', () => {
|
||||||
cy.visit('/liquid');
|
cy.visit(`${baseModule}/assets`);
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
cy.get('.container-xl input').click().type('Liquid AUD').then(() => {
|
||||||
cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
|
cy.get('table tr td:nth-of-type(1) a').click();
|
||||||
cy.get('table tr td:nth-of-type(4) a').click();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a specific asset issuance TX', () => {
|
|
||||||
cy.visit('/liquid');
|
describe('unblinded TX', () => {
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
it('should not show an unblinding error message for regular txs', () => {
|
||||||
cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
|
cy.visit(`${baseModule}/tx/82a479043ec3841e0d3f829afc8df4f0e2bbd675a13f013ea611b2fde0027d45`);
|
||||||
cy.get('table tr td:nth-of-type(5) a').click();
|
cy.waitForSkeletonGone();
|
||||||
});
|
cy.get('.error-unblinded' ).should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show unblinded TX', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc,2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3a`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('#table-tx-vin tr').should('have.class', 'assetBox');
|
||||||
|
cy.get('#table-tx-vout tr').should('have.class', 'assetBox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show empty unblinded TX', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('#table-tx-vin tr').should('have.class', '');
|
||||||
|
cy.get('#table-tx-vout tr').should('have.class', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show invalid unblinded TX hex', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=123`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('#table-tx-vin tr').should('have.class', '');
|
||||||
|
cy.get('#table-tx-vout tr').should('have.class', '');
|
||||||
|
cy.get('.error-unblinded' ).contains('Error: Invalid blinding data (invalid hex)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show first unblinded vout', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('#table-tx-vout tr:first-child()').should('have.class', 'assetBox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show second unblinded vout', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3a`);
|
||||||
|
cy.get('#table-tx-vout tr').should('have.class', 'assetBox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show invalid error unblinded TX', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc,2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3c`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('#table-tx-vout tr').should('have.class', 'assetBox');
|
||||||
|
cy.get('.error-unblinded' ).contains('Error: Invalid blinding data.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows asset peg in/out and burn transactions', () => {
|
||||||
|
cy.visit(`${baseModule}/asset/6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('#table-tx-vout tr').not('.assetBox');
|
||||||
|
cy.get('#table-tx-vin tr').not('.assetBox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prevents regressing issue #644', () => {
|
||||||
|
cy.visit(`${baseModule}/tx/393b890966f305e7c440fcfb12a13f51a7a9011cc59ff5f14f6f93214261bd82`);
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
} else {
|
||||||
|
it.skip("Tests cannot be run on the selected BASE_MODULE");
|
||||||
|
}
|
||||||
describe('unblinded TX', () => {
|
|
||||||
it('show unblinded TX', () => {
|
|
||||||
cy.visit('/liquid/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc,2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3a');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('#table-tx-vin tr').should('have.class', 'assetBox');
|
|
||||||
cy.get('#table-tx-vout tr').should('have.class', 'assetBox');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('show empty unblinded TX', () => {
|
|
||||||
cy.visit('/liquid/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('#table-tx-vin tr').should('have.class', '');
|
|
||||||
cy.get('#table-tx-vout tr').should('have.class', '');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('show invalid unblinded TX hex', () => {
|
|
||||||
cy.visit('/liquid/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=123');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('#table-tx-vin tr').should('have.class', '');
|
|
||||||
cy.get('#table-tx-vout tr').should('have.class', '');
|
|
||||||
cy.get('.error-unblinded' ).contains('Error: Invalid blinding data (invalid hex)');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('show first unblinded vout', () => {
|
|
||||||
cy.visit('/liquid/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('#table-tx-vout tr:first-child()').should('have.class', 'assetBox');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('show second unblinded vout', () => {
|
|
||||||
cy.visit('/liquid/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3a');
|
|
||||||
cy.get('#table-tx-vout tr').should('have.class', 'assetBox');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('show invalid error unblinded TX', () => {
|
|
||||||
cy.visit('/liquid/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc,2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3c');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('#table-tx-vout tr').should('have.class', 'assetBox');
|
|
||||||
cy.get('.error-unblinded' ).contains('Error: Invalid blinding data.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows asset peg in/out and burn transactions', () => {
|
|
||||||
cy.visit('/liquid/asset/6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('#table-tx-vout tr').not('.assetBox');
|
|
||||||
cy.get('#table-tx-vin tr').not('.assetBox');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('prevents regressing issue #644', () => {
|
|
||||||
cy.visit('/liquid/tx/393b890966f305e7c440fcfb12a13f51a7a9011cc59ff5f14f6f93214261bd82');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,190 +9,360 @@ describe('Mainnet', () => {
|
|||||||
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
||||||
cy.intercept('/resources/pools.json').as('pools');
|
cy.intercept('/resources/pools.json').as('pools');
|
||||||
|
|
||||||
|
// Search Auto Complete
|
||||||
|
cy.intercept('/api/address-prefix/1wiz').as('search-1wiz');
|
||||||
|
cy.intercept('/api/address-prefix/1wizS').as('search-1wizS');
|
||||||
|
cy.intercept('/api/address-prefix/1wizSA').as('search-1wizSA');
|
||||||
|
|
||||||
Cypress.Commands.add('waitForBlockData', () => {
|
Cypress.Commands.add('waitForBlockData', () => {
|
||||||
cy.wait('@tx-outspends');
|
cy.wait('@tx-outspends');
|
||||||
cy.wait('@pools');
|
cy.wait('@pools');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads the status screen', () => {
|
if (Cypress.env("BASE_MODULE") === '' || Cypress.env("BASE_MODULE") === 'mempool') {
|
||||||
cy.visit('/status');
|
|
||||||
cy.get('#mempool-block-0').should('be.visible');
|
|
||||||
cy.get('[id^="bitcoin-block-"]').should('have.length', 8);
|
|
||||||
cy.get('.footer').should('be.visible');
|
|
||||||
cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
|
|
||||||
expect(text).to.match(/Tx vBytes per second:.* vB\/s/);
|
|
||||||
});
|
|
||||||
cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
|
|
||||||
expect(text).to.match(/Unconfirmed:(.*)/);
|
|
||||||
});
|
|
||||||
cy.get('.row > :nth-child(3)').invoke('text').then((text) => {
|
|
||||||
expect(text).to.match(/Mempool size:(.*) (kB|MB) \((\d+) (block|blocks)\)/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads dashboard, drop websocket and reconnect', () => {
|
it('loads the status screen', () => {
|
||||||
cy.viewport('macbook-16');
|
cy.visit('/status');
|
||||||
cy.mockMempoolSocket();
|
|
||||||
cy.visit('/');
|
|
||||||
cy.get('.badge').should('not.exist');
|
|
||||||
dropWebSocket();
|
|
||||||
cy.get('.badge').should('be.visible');
|
|
||||||
cy.get('.badge', {timeout: 25000}).should('not.exist');
|
|
||||||
emitMempoolInfo({
|
|
||||||
'params': {
|
|
||||||
loaded: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
|
|
||||||
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
|
|
||||||
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the dashboard', () => {
|
|
||||||
cy.visit('/');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads skeleton when changes between networks', () => {
|
|
||||||
cy.visit('/');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
|
|
||||||
cy.changeNetwork("testnet");
|
|
||||||
cy.changeNetwork("signet");
|
|
||||||
cy.changeNetwork("liquid");
|
|
||||||
cy.changeNetwork("mainnet");
|
|
||||||
cy.changeNetwork("bisq");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the dashboard with the skeleton blocks', () => {
|
|
||||||
cy.mockMempoolSocket();
|
|
||||||
cy.visit("/");
|
|
||||||
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
|
|
||||||
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
|
|
||||||
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
|
|
||||||
cy.get('#mempool-block-0').should('be.visible');
|
|
||||||
cy.get('#mempool-block-1').should('be.visible');
|
|
||||||
cy.get('#mempool-block-2').should('be.visible');
|
|
||||||
|
|
||||||
emitMempoolInfo({
|
|
||||||
'params': {
|
|
||||||
loaded: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
|
|
||||||
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
|
|
||||||
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the blocks screen', () => {
|
|
||||||
cy.visit('/');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
|
||||||
cy.waitForPageIdle();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the graphs screen', () => {
|
|
||||||
cy.visit('/');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the tv screen - desktop', () => {
|
|
||||||
cy.viewport('macbook-16');
|
|
||||||
cy.visit('/');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
|
||||||
cy.viewport('macbook-16');
|
|
||||||
cy.get('.chart-holder');
|
|
||||||
cy.get('.blockchain-wrapper').should('be.visible');
|
|
||||||
cy.get('#mempool-block-0').should('be.visible');
|
cy.get('#mempool-block-0').should('be.visible');
|
||||||
});
|
cy.get('[id^="bitcoin-block-"]').should('have.length', 8);
|
||||||
});
|
cy.get('.footer').should('be.visible');
|
||||||
|
cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
|
||||||
it('loads the tv screen - mobile', () => {
|
expect(text).to.match(/Tx vBytes per second:.* vB\/s/);
|
||||||
cy.viewport('iphone-6');
|
|
||||||
cy.visit('/tv');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('.chart-holder');
|
|
||||||
cy.get('.blockchain-wrapper').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the api screen', () => {
|
|
||||||
cy.visit('/');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('blocks', () => {
|
|
||||||
it('shows empty blocks properly', () => {
|
|
||||||
cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.waitForPageIdle();
|
|
||||||
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('expands and collapses the block details', () => {
|
|
||||||
cy.visit('/block/0');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.waitForPageIdle();
|
|
||||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
|
||||||
cy.get('#details').should('be.visible');
|
|
||||||
});
|
});
|
||||||
|
cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
|
||||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
expect(text).to.match(/Unconfirmed:(.*)/);
|
||||||
cy.get('#details').should('not.be.visible');
|
});
|
||||||
|
cy.get('.row > :nth-child(3)').invoke('text').then((text) => {
|
||||||
|
expect(text).to.match(/Mempool size:(.*) (kB|MB) \((\d+) (block|blocks)\)/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('shows blocks with no pagination', () => {
|
|
||||||
cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4');
|
it('loads dashboard, drop websocket and reconnect', () => {
|
||||||
cy.waitForSkeletonGone();
|
cy.viewport('macbook-16');
|
||||||
cy.waitForPageIdle();
|
cy.mockMempoolSocket();
|
||||||
cy.get('.block-tx-title h2').invoke('text').should('equal', '19 transactions');
|
cy.visit('/');
|
||||||
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 5);
|
cy.get('.badge').should('not.exist');
|
||||||
|
dropWebSocket();
|
||||||
|
cy.get('.badge').should('be.visible');
|
||||||
|
cy.get('.badge', {timeout: 25000}).should('not.exist');
|
||||||
|
emitMempoolInfo({
|
||||||
|
'params': {
|
||||||
|
loaded: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports pagination on the block screen', () => {
|
it('loads the dashboard', () => {
|
||||||
// 41 txs
|
cy.visit('/');
|
||||||
cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8');
|
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.get('.pagination-container a').invoke('text').then((text1) => {
|
});
|
||||||
cy.get('.active + li').first().click().then(() => {
|
|
||||||
|
describe('search', () => {
|
||||||
|
it('allows searching for partial Bitcoin addresses', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get('.search-box-container > .form-control').type('1wiz').then(() => {
|
||||||
|
cy.wait('@search-1wiz');
|
||||||
|
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('.search-box-container > .form-control').type('S').then(() => {
|
||||||
|
cy.wait('@search-1wizS');
|
||||||
|
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('.search-box-container > .form-control').type('A').then(() => {
|
||||||
|
cy.wait('@search-1wizSA');
|
||||||
|
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 1)
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('ngb-typeahead-window button.dropdown-item.active').click().then(() => {
|
||||||
|
cy.url().should('include', '/address/1wizSAYSbuyXbt9d8JV8ytm5acqq2TorC');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
cy.waitForPageIdle();
|
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
|
||||||
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
|
});
|
||||||
expect(text1).not.to.eq(text2);
|
});
|
||||||
|
|
||||||
|
['BC1PQYQSZQ', 'bc1PqYqSzQ'].forEach((searchTerm) => {
|
||||||
|
it(`allows searching for partial case insensitive bc1 addresses: ${searchTerm}`, () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
|
||||||
|
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 1);
|
||||||
|
cy.get('ngb-typeahead-window button.dropdown-item.active').click().then(() => {
|
||||||
|
cy.url().should('include', '/address/bc1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs3wf0qm');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows blocks pagination with 5 pages (desktop)', () => {
|
describe('blocks navigation', () => {
|
||||||
cy.viewport(760, 800);
|
|
||||||
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
|
describe('keyboard events', () => {
|
||||||
cy.waitForSkeletonGone();
|
it('loads first blockchain blocks visible and keypress arrow right', () => {
|
||||||
cy.waitForPageIdle();
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.blockchain-blocks-0 > a').click().then(() => {
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.document().right();
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads first blockchain blocks visible and keypress arrow left', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.blockchain-blocks-0 > a').click().then(() => {
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.document().left();
|
||||||
|
cy.get('.title-block h1').invoke('text').should('equal', 'Next block');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads last blockchain blocks and keypress arrow right', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.blockchain-blocks-4 > a').click().then(() => {
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
|
||||||
|
// block 6
|
||||||
|
cy.document().right();
|
||||||
|
cy.wait(5000);
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
|
||||||
|
// block 7
|
||||||
|
cy.document().right();
|
||||||
|
cy.wait(5000);
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
|
||||||
|
// block 8 - last visible block
|
||||||
|
cy.document().right();
|
||||||
|
cy.wait(5000);
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
|
||||||
|
// block 9 - not visible at the blochchain blocks visible block
|
||||||
|
cy.document().right();
|
||||||
|
cy.wait(5000);
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads genesis block and keypress arrow right', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
|
||||||
|
cy.document().right();
|
||||||
|
cy.wait(5000);
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads genesis block and keypress arrow left', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
|
||||||
|
cy.document().left();
|
||||||
|
cy.wait(5000);
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('mouse events', () => {
|
||||||
|
it('loads first blockchain blocks visible and click on the arrow right', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.blockchain-blocks-0 > a').click().then(() => {
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').click().then(() => {
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads genesis block and click on the arrow left', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').click().then(() => {
|
||||||
|
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5 pages + 4 buttons = 9 buttons
|
|
||||||
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 9);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows blocks pagination with 3 pages (mobile)', () => {
|
|
||||||
cy.viewport(669, 800);
|
it('loads skeleton when changes between networks', () => {
|
||||||
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
|
cy.visit('/');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
|
|
||||||
|
cy.changeNetwork("testnet");
|
||||||
|
cy.changeNetwork("signet");
|
||||||
|
cy.changeNetwork("liquid");
|
||||||
|
cy.changeNetwork("mainnet");
|
||||||
|
cy.changeNetwork("bisq");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the dashboard with the skeleton blocks', () => {
|
||||||
|
cy.mockMempoolSocket();
|
||||||
|
cy.visit("/");
|
||||||
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
cy.get('#mempool-block-1').should('be.visible');
|
||||||
|
cy.get('#mempool-block-2').should('be.visible');
|
||||||
|
|
||||||
|
emitMempoolInfo({
|
||||||
|
'params': {
|
||||||
|
loaded: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the blocks screen', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
||||||
cy.waitForPageIdle();
|
cy.waitForPageIdle();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3 pages + 4 buttons = 7 buttons
|
|
||||||
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 7);
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
it('loads the graphs screen', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the tv screen - desktop', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.get('.chart-holder');
|
||||||
|
cy.get('.blockchain-wrapper').should('be.visible');
|
||||||
|
cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the tv screen - mobile', () => {
|
||||||
|
cy.viewport('iphone-6');
|
||||||
|
cy.visit('/tv');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.chart-holder');
|
||||||
|
cy.get('.blockchain-wrapper').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the api screen', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('blocks', () => {
|
||||||
|
it('shows empty blocks properly', () => {
|
||||||
|
cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expands and collapses the block details', () => {
|
||||||
|
cy.visit('/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
|
cy.get('#details').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
|
cy.get('#details').should('not.be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('shows blocks with no pagination', () => {
|
||||||
|
cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.block-tx-title h2').invoke('text').should('equal', '19 transactions');
|
||||||
|
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports pagination on the block screen', () => {
|
||||||
|
// 41 txs
|
||||||
|
cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.pagination-container a').invoke('text').then((text1) => {
|
||||||
|
cy.get('.active + li').first().click().then(() => {
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
|
||||||
|
expect(text1).not.to.eq(text2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows blocks pagination with 5 pages (desktop)', () => {
|
||||||
|
cy.viewport(760, 800);
|
||||||
|
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5 pages + 4 buttons = 9 buttons
|
||||||
|
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows blocks pagination with 3 pages (mobile)', () => {
|
||||||
|
cy.viewport(669, 800);
|
||||||
|
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.waitForPageIdle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3 pages + 4 buttons = 7 buttons
|
||||||
|
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 7);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
it.skip("Tests cannot be run on the selected BASE_MODULE");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,119 +8,124 @@ describe('Signet', () => {
|
|||||||
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads the dashboard', () => {
|
|
||||||
cy.visit('/signet');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the dashboard with the skeleton blocks', () => {
|
if (Cypress.env("BASE_MODULE") === '' || Cypress.env("BASE_MODULE") === 'mempool') {
|
||||||
cy.mockMempoolSocket();
|
it('loads the dashboard', () => {
|
||||||
cy.visit("/signet");
|
cy.visit('/signet');
|
||||||
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
|
cy.waitForSkeletonGone();
|
||||||
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
|
|
||||||
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
|
|
||||||
cy.get('#mempool-block-0').should('be.visible');
|
|
||||||
cy.get('#mempool-block-1').should('be.visible');
|
|
||||||
cy.get('#mempool-block-2').should('be.visible');
|
|
||||||
|
|
||||||
emitMempoolInfo({
|
|
||||||
'params': {
|
|
||||||
"network": "signet"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
|
|
||||||
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
|
|
||||||
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the blocks screen', () => {
|
|
||||||
cy.visit('/signet');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the graphs screen', () => {
|
|
||||||
cy.visit('/signet');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('tv mode', () => {
|
|
||||||
it('loads the tv screen - desktop', () => {
|
|
||||||
cy.viewport('macbook-16');
|
|
||||||
cy.visit('/signet');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
|
||||||
cy.get('.chart-holder').should('be.visible');
|
|
||||||
cy.get('#mempool-block-0').should('be.visible');
|
|
||||||
cy.get('.tv-only').should('not.exist');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the tv screen - mobile', () => {
|
|
||||||
cy.visit('/signet');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
|
||||||
cy.viewport('iphone-8');
|
|
||||||
cy.get('.chart-holder').should('be.visible');
|
|
||||||
//TODO: Remove comment when the bug is fixed
|
|
||||||
//cy.get('#mempool-block-0').should('be.visible');
|
|
||||||
cy.get('.tv-only').should('not.exist');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('loads the api screen', () => {
|
|
||||||
cy.visit('/signet');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('blocks', () => {
|
|
||||||
it('shows empty blocks properly', () => {
|
|
||||||
cy.visit('/signet/block/00000133d54e4589f6436703b067ec23209e0a21b8a9b12f57d0592fd85f7a42');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('expands and collapses the block details', () => {
|
|
||||||
cy.visit('/signet/block/0');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
|
||||||
cy.get('#details').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
|
||||||
cy.get('#details').should('not.be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows blocks with no pagination', () => {
|
|
||||||
cy.visit('/signet/block/00000078f920a96a69089877b934ce7fd009ab55e3170920a021262cb258e7cc');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('h2').invoke('text').should('equal', '13 transactions');
|
|
||||||
cy.get('ul.pagination').first().children().should('have.length', 5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('supports pagination on the block screen', () => {
|
|
||||||
// 43 txs
|
|
||||||
cy.visit('/signet/block/00000094bd52f73bdbfc4bece3a94c21fec2dc968cd54210496e69e4059d66a6');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
|
||||||
cy.get('.active + li').first().click().then(() => {
|
|
||||||
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
|
|
||||||
expect(text1).not.to.eq(text2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
it('loads the dashboard with the skeleton blocks', () => {
|
||||||
|
cy.mockMempoolSocket();
|
||||||
|
cy.visit("/signet");
|
||||||
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
|
||||||
|
cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
cy.get('#mempool-block-1').should('be.visible');
|
||||||
|
cy.get('#mempool-block-2').should('be.visible');
|
||||||
|
|
||||||
|
emitMempoolInfo({
|
||||||
|
'params': {
|
||||||
|
"network": "signet"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
|
||||||
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the blocks screen', () => {
|
||||||
|
cy.visit('/signet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the graphs screen', () => {
|
||||||
|
cy.visit('/signet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tv mode', () => {
|
||||||
|
it('loads the tv screen - desktop', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/signet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||||
|
cy.get('.chart-holder').should('be.visible');
|
||||||
|
cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
cy.get('.tv-only').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the tv screen - mobile', () => {
|
||||||
|
cy.visit('/signet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||||
|
cy.viewport('iphone-8');
|
||||||
|
cy.get('.chart-holder').should('be.visible');
|
||||||
|
//TODO: Remove comment when the bug is fixed
|
||||||
|
//cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
cy.get('.tv-only').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('loads the api screen', () => {
|
||||||
|
cy.visit('/signet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('blocks', () => {
|
||||||
|
it('shows empty blocks properly', () => {
|
||||||
|
cy.visit('/signet/block/00000133d54e4589f6436703b067ec23209e0a21b8a9b12f57d0592fd85f7a42');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expands and collapses the block details', () => {
|
||||||
|
cy.visit('/signet/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
|
cy.get('#details').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
|
cy.get('#details').should('not.be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows blocks with no pagination', () => {
|
||||||
|
cy.visit('/signet/block/00000078f920a96a69089877b934ce7fd009ab55e3170920a021262cb258e7cc');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('h2').invoke('text').should('equal', '13 transactions');
|
||||||
|
cy.get('ul.pagination').first().children().should('have.length', 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports pagination on the block screen', () => {
|
||||||
|
// 43 txs
|
||||||
|
cy.visit('/signet/block/00000094bd52f73bdbfc4bece3a94c21fec2dc968cd54210496e69e4059d66a6');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
||||||
|
cy.get('.active + li').first().click().then(() => {
|
||||||
|
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
|
||||||
|
expect(text1).not.to.eq(text2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
it.skip("Tests cannot be run on the selected BASE_MODULE");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { emitMempoolInfo } from "../../support/websocket";
|
import { confirmAddress, emitMempoolInfo, sendWsMock, showNewTx, startTrackingAddress } from "../../support/websocket";
|
||||||
|
|
||||||
describe('Testnet', () => {
|
describe('Testnet', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -8,115 +8,121 @@ describe('Testnet', () => {
|
|||||||
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads the dashboard', () => {
|
if (Cypress.env("BASE_MODULE") === '' || Cypress.env("BASE_MODULE") === 'mempool') {
|
||||||
cy.visit('/testnet');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the dashboard with the skeleton blocks', () => {
|
it('loads the dashboard', () => {
|
||||||
cy.mockMempoolSocket();
|
cy.visit('/testnet');
|
||||||
cy.visit("/signet");
|
cy.waitForSkeletonGone();
|
||||||
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
|
|
||||||
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
|
|
||||||
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
|
|
||||||
cy.get('#mempool-block-0').should('be.visible');
|
|
||||||
cy.get('#mempool-block-1').should('be.visible');
|
|
||||||
cy.get('#mempool-block-2').should('be.visible');
|
|
||||||
|
|
||||||
emitMempoolInfo({
|
|
||||||
'params': {
|
|
||||||
loaded: true
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
|
|
||||||
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
|
|
||||||
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the blocks screen', () => {
|
it('loads the dashboard with the skeleton blocks', () => {
|
||||||
cy.visit('/testnet');
|
cy.mockMempoolSocket();
|
||||||
cy.waitForSkeletonGone();
|
cy.visit("/testnet");
|
||||||
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
|
||||||
cy.wait(1000);
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
|
||||||
});
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
|
||||||
});
|
cy.get('#mempool-block-0').should('be.visible');
|
||||||
|
cy.get('#mempool-block-1').should('be.visible');
|
||||||
|
cy.get('#mempool-block-2').should('be.visible');
|
||||||
|
|
||||||
it('loads the graphs screen', () => {
|
emitMempoolInfo({
|
||||||
cy.visit('/testnet');
|
'params': {
|
||||||
cy.waitForSkeletonGone();
|
loaded: true
|
||||||
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
}
|
||||||
cy.wait(1000);
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('tv mode', () => {
|
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
|
||||||
it('loads the tv screen - desktop', () => {
|
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
|
||||||
cy.viewport('macbook-16');
|
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
|
||||||
cy.visit('/testnet');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.get('.tv-only').should('not.exist');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the tv screen - mobile', () => {
|
|
||||||
cy.visit('/testnet');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
|
||||||
cy.viewport('iphone-6');
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.get('.tv-only').should('not.exist');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('loads the api screen', () => {
|
|
||||||
cy.visit('/testnet');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('blocks', () => {
|
|
||||||
it('shows empty blocks properly', () => {
|
|
||||||
cy.visit('/testnet/block/0');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('expands and collapses the block details', () => {
|
|
||||||
cy.visit('/testnet/block/0');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
|
||||||
cy.get('#details').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get('.btn.btn-outline-info').click().then(() => {
|
|
||||||
cy.get('#details').should('not.be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows blocks with no pagination', () => {
|
|
||||||
cy.visit('/testnet/block/000000000000002f8ce27716e74ecc7ad9f7b5101fed12d09e28bb721b9460ea');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('h2').invoke('text').should('equal', '11 transactions');
|
|
||||||
cy.get('ul.pagination').first().children().should('have.length', 5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('supports pagination on the block screen', () => {
|
|
||||||
// 48 txs
|
|
||||||
cy.visit('/testnet/block/000000000000002ca3878ebd98b313a1c2d531f2e70a6575d232ca7564dea7a9');
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
|
||||||
cy.get('.active + li').first().click().then(() => {
|
|
||||||
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
|
|
||||||
expect(text1).not.to.eq(text2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('loads the blocks screen', () => {
|
||||||
|
cy.visit('/testnet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(2) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the graphs screen', () => {
|
||||||
|
cy.visit('/testnet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(3) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tv mode', () => {
|
||||||
|
it('loads the tv screen - desktop', () => {
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.visit('/testnet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
cy.get('.tv-only').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the tv screen - mobile', () => {
|
||||||
|
cy.visit('/testnet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(4) > a').click().then(() => {
|
||||||
|
cy.viewport('iphone-6');
|
||||||
|
cy.wait(1000);
|
||||||
|
cy.get('.tv-only').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('loads the api screen', () => {
|
||||||
|
cy.visit('/testnet');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('li:nth-of-type(5) > a').click().then(() => {
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('blocks', () => {
|
||||||
|
it('shows empty blocks properly', () => {
|
||||||
|
cy.visit('/testnet/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('h2').invoke('text').should('equal', '1 transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expands and collapses the block details', () => {
|
||||||
|
cy.visit('/testnet/block/0');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
|
cy.get('#details').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('.btn.btn-outline-info').click().then(() => {
|
||||||
|
cy.get('#details').should('not.be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows blocks with no pagination', () => {
|
||||||
|
cy.visit('/testnet/block/000000000000002f8ce27716e74ecc7ad9f7b5101fed12d09e28bb721b9460ea');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('h2').invoke('text').should('equal', '11 transactions');
|
||||||
|
cy.get('ul.pagination').first().children().should('have.length', 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports pagination on the block screen', () => {
|
||||||
|
// 48 txs
|
||||||
|
cy.visit('/testnet/block/000000000000002ca3878ebd98b313a1c2d531f2e70a6575d232ca7564dea7a9');
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
|
||||||
|
cy.get('.active + li').first().click().then(() => {
|
||||||
|
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
|
||||||
|
expect(text1).not.to.eq(text2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
it.skip("Tests cannot be run on the selected BASE_MODULE");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,14 +42,24 @@
|
|||||||
// -- This will overwrite an existing command --
|
// -- This will overwrite an existing command --
|
||||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
import 'cypress-wait-until';
|
import 'cypress-wait-until';
|
||||||
import { PageIdleDetector } from './PageIdleDetector';
|
import { PageIdleDetector } from './PageIdleDetector';
|
||||||
import { mockWebSocket } from './websocket';
|
import { mockWebSocket } from './websocket';
|
||||||
|
|
||||||
|
/* global Cypress */
|
||||||
|
const codes = {
|
||||||
|
ArrowLeft: 37,
|
||||||
|
ArrowUp: 38,
|
||||||
|
ArrowRight: 39,
|
||||||
|
ArrowDown: 40
|
||||||
|
}
|
||||||
|
|
||||||
Cypress.Commands.add('waitForSkeletonGone', () => {
|
Cypress.Commands.add('waitForSkeletonGone', () => {
|
||||||
cy.waitUntil(() => {
|
cy.waitUntil(() => {
|
||||||
return Cypress.$('.skeleton-loader').length === 0;
|
return Cypress.$('.skeleton-loader').length === 0;
|
||||||
}, { verbose: true, description: "waitForSkeletonGone", errorMsg: "skeleton loaders never went away", timeout: 7000, interval: 50});
|
}, { verbose: true, description: "waitForSkeletonGone", errorMsg: "skeleton loaders never went away", timeout: 15000, interval: 50});
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
@@ -75,3 +85,63 @@ Cypress.Commands.add('changeNetwork', (network: "testnet"|"signet"|"liquid"|"bis
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://github.com/bahmutov/cypress-arrows/blob/8f0303842a343550fbeaf01528d01d1ff213b70c/src/index.js
|
||||||
|
function keydownCommand ($el, key) {
|
||||||
|
const message = `sending the "${key}" keydown event`
|
||||||
|
const log = Cypress.log({
|
||||||
|
name: `keydown: ${key}`,
|
||||||
|
message: message,
|
||||||
|
consoleProps: function () {
|
||||||
|
return {
|
||||||
|
Subject: $el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const e = $el.createEvent('KeyboardEvent')
|
||||||
|
|
||||||
|
Object.defineProperty(e, 'key', {
|
||||||
|
get: function () {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty(e, 'keyCode', {
|
||||||
|
get: function () {
|
||||||
|
return this.keyCodeVal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Object.defineProperty(e, 'which', {
|
||||||
|
get: function () {
|
||||||
|
return this.keyCodeVal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var metaKey = false
|
||||||
|
|
||||||
|
Object.defineProperty(e, 'metaKey', {
|
||||||
|
get: function () {
|
||||||
|
return metaKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty(e, 'shiftKey', {
|
||||||
|
get: function () {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
e.keyCodeVal = codes[key]
|
||||||
|
|
||||||
|
e.initKeyboardEvent('keydown', true, true,
|
||||||
|
$el.defaultView, false, false, false, false, e.keyCodeVal, e.keyCodeVal)
|
||||||
|
|
||||||
|
$el.dispatchEvent(e)
|
||||||
|
log.snapshot().end()
|
||||||
|
return $el
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add('keydown', { prevSubject: "dom" }, keydownCommand)
|
||||||
|
Cypress.Commands.add('left', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowLeft'))
|
||||||
|
Cypress.Commands.add('right', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowRight'))
|
||||||
|
Cypress.Commands.add('up', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowUp'))
|
||||||
|
Cypress.Commands.add('down', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowDown'))
|
||||||
@@ -21,6 +21,16 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const indexFilePath = configContent.BASE_MODULE ? 'src/index.' + configContent.BASE_MODULE + '.html' : 'src/index.mempool.html';
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.copyFileSync(indexFilePath, 'src/index.html');
|
||||||
|
console.log('Copied ' + indexFilePath + ' to src/index.html');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error copying the index file');
|
||||||
|
throw new Error(e);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const packageJson = fs.readFileSync('package.json');
|
const packageJson = fs.readFileSync('package.json');
|
||||||
packetJsonVersion = JSON.parse(packageJson).version;
|
packetJsonVersion = JSON.parse(packageJson).version;
|
||||||
|
|||||||
@@ -8,5 +8,8 @@
|
|||||||
"KEEP_BLOCKS_AMOUNT": 8,
|
"KEEP_BLOCKS_AMOUNT": 8,
|
||||||
"NGINX_PROTOCOL": "http",
|
"NGINX_PROTOCOL": "http",
|
||||||
"NGINX_HOSTNAME": "127.0.0.1",
|
"NGINX_HOSTNAME": "127.0.0.1",
|
||||||
"NGINX_PORT": "80"
|
"NGINX_PORT": "80",
|
||||||
|
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
||||||
|
"BLOCK_WEIGHT_UNITS": 4000000,
|
||||||
|
"BASE_MODULE": "mempool"
|
||||||
}
|
}
|
||||||
|
|||||||
489
frontend/package-lock.json
generated
489
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-frontend",
|
"name": "mempool-frontend",
|
||||||
"version": "2.2.1",
|
"version": "2.2.2",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
@@ -37,9 +37,11 @@
|
|||||||
"build-mempool.js": "tsc | browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index.js --standalone mempoolJS > ./dist/mempool/browser/en-US/mempool.js",
|
"build-mempool.js": "tsc | browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index.js --standalone mempoolJS > ./dist/mempool/browser/en-US/mempool.js",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "ng e2e",
|
"e2e": "npm run generate-config && ng e2e",
|
||||||
"e2e:ci": "npm run cypress:run:ci",
|
"e2e:ci": "npm run cypress:run:ci",
|
||||||
"config:defaults": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config",
|
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool && npm run generate-config",
|
||||||
|
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid && npm run generate-config",
|
||||||
|
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq && npm run generate-config",
|
||||||
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
|
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
|
||||||
"serve:ssr": "node server.run.js",
|
"serve:ssr": "node server.run.js",
|
||||||
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
|
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
|
||||||
@@ -110,7 +112,7 @@
|
|||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@cypress/schematic": "^1.3.0",
|
"@cypress/schematic": "^1.3.0",
|
||||||
"cypress": "^7.7.0",
|
"cypress": "^8.3.1",
|
||||||
"cypress-fail-on-console-error": "^2.1.0",
|
"cypress-fail-on-console-error": "^2.1.0",
|
||||||
"cypress-wait-until": "^1.7.1",
|
"cypress-wait-until": "^1.7.1",
|
||||||
"mock-socket": "^9.0.3",
|
"mock-socket": "^9.0.3",
|
||||||
|
|||||||
64
frontend/proxy.conf.js
Normal file
64
frontend/proxy.conf.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
let PROXY_CONFIG;
|
||||||
|
let configContent;
|
||||||
|
|
||||||
|
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawConfig = fs.readFileSync(CONFIG_FILE_NAME);
|
||||||
|
configContent = JSON.parse(rawConfig);
|
||||||
|
console.log(`${CONFIG_FILE_NAME} file found, using provided config`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
if (e.code !== 'ENOENT') {
|
||||||
|
throw new Error(e);
|
||||||
|
} else {
|
||||||
|
console.log(`${CONFIG_FILE_NAME} file not found, using default config`);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PROXY_CONFIG = [
|
||||||
|
{
|
||||||
|
context: ['*',
|
||||||
|
'/api/**', '!/api/v1/ws',
|
||||||
|
'!/bisq', '!/bisq/**', '!/bisq/',
|
||||||
|
'!/liquid', '!/liquid/**', '!/liquid/',
|
||||||
|
'/testnet/api/**', '/signet/api/**'
|
||||||
|
],
|
||||||
|
target: "https://mempool.space",
|
||||||
|
ws: true,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/api/v1/ws'],
|
||||||
|
target: "https://mempool.space",
|
||||||
|
ws: true,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/api/bisq**', '/bisq/api/**'],
|
||||||
|
target: "https://bisq.markets",
|
||||||
|
pathRewrite: {
|
||||||
|
"^/api/bisq/": "/bisq/api"
|
||||||
|
},
|
||||||
|
ws: true,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/api/liquid**', '/liquid/api/**'],
|
||||||
|
target: "https://liquid.network",
|
||||||
|
pathRewrite: {
|
||||||
|
"^/api/liquid/": "/liquid/api"
|
||||||
|
},
|
||||||
|
ws: true,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = PROXY_CONFIG;
|
||||||
@@ -67,11 +67,18 @@
|
|||||||
"^/liquid/api": "/api/v1/ws"
|
"^/liquid/api": "/api/v1/ws"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/liquid/api/": {
|
"/liquid/api/v1/": {
|
||||||
"target": "http://localhost:50001/",
|
"target": "http://localhost:8999/",
|
||||||
"secure": false,
|
"secure": false,
|
||||||
"pathRewrite": {
|
"pathRewrite": {
|
||||||
"^/liquid/api/": ""
|
"^/liquid/api/": "/api/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/liquid/api/": {
|
||||||
|
"target": "http://localhost:8999/",
|
||||||
|
"secure": false,
|
||||||
|
"pathRewrite": {
|
||||||
|
"^/liquid/api/": "/api/v1/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/bisq/api/": {
|
"/bisq/api/": {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-poli
|
|||||||
import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component';
|
import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component';
|
||||||
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
||||||
import { SponsorComponent } from './components/sponsor/sponsor.component';
|
import { SponsorComponent } from './components/sponsor/sponsor.component';
|
||||||
|
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
|
||||||
|
|
||||||
let routes: Routes = [
|
let routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -303,7 +304,7 @@ const browserWindow = window || {};
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const browserWindowEnv = browserWindow.__env || {};
|
const browserWindowEnv = browserWindow.__env || {};
|
||||||
|
|
||||||
if (browserWindowEnv && browserWindowEnv.OFFICIAL_BISQ_MARKETS) {
|
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') {
|
||||||
routes = [{
|
routes = [{
|
||||||
path: '',
|
path: '',
|
||||||
component: BisqMasterPageComponent,
|
component: BisqMasterPageComponent,
|
||||||
@@ -311,6 +312,93 @@ if (browserWindowEnv && browserWindowEnv.OFFICIAL_BISQ_MARKETS) {
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||||
|
routes = [{
|
||||||
|
path: '',
|
||||||
|
component: LiquidMasterPageComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: StartComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: DashboardComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tx/:id',
|
||||||
|
component: TransactionComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'block/:id',
|
||||||
|
component: BlockComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'mempool-block/:id',
|
||||||
|
component: MempoolBlockComponent
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'blocks',
|
||||||
|
component: LatestBlocksComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'graphs',
|
||||||
|
component: StatisticsComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'address/:id',
|
||||||
|
component: AddressComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'asset/:id',
|
||||||
|
component: AssetComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'assets',
|
||||||
|
component: AssetsComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'api',
|
||||||
|
component: ApiDocsComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'about',
|
||||||
|
component: AboutComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'terms-of-service',
|
||||||
|
component: TermsOfServiceComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'privacy-policy',
|
||||||
|
component: PrivacyPolicyComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'trademark-policy',
|
||||||
|
component: TrademarkPolicyComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sponsor',
|
||||||
|
component: SponsorComponent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tv',
|
||||||
|
component: TelevisionComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'status',
|
||||||
|
component: StatusViewComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
redirectTo: ''
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes, {
|
imports: [RouterModule.forRoot(routes, {
|
||||||
initialNavigation: 'enabled',
|
initialNavigation: 'enabled',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { AddressLabelsComponent } from './components/address-labels/address-labe
|
|||||||
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
|
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
|
||||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||||
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
||||||
|
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
|
||||||
import { AboutComponent } from './components/about/about.component';
|
import { AboutComponent } from './components/about/about.component';
|
||||||
import { TelevisionComponent } from './components/television/television.component';
|
import { TelevisionComponent } from './components/television/television.component';
|
||||||
import { StatisticsComponent } from './components/statistics/statistics.component';
|
import { StatisticsComponent } from './components/statistics/statistics.component';
|
||||||
@@ -44,7 +45,7 @@ import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
|
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
|
||||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||||
import { faAngleDown, faAngleUp, faBolt, faChartArea, faCogs, faCubes, faDatabase, faExchangeAlt, faInfoCircle,
|
import { faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faDatabase, faExchangeAlt, faInfoCircle,
|
||||||
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
|
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
|
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
|
||||||
import { CodeTemplateComponent } from './components/api-docs/code-template.component';
|
import { CodeTemplateComponent } from './components/api-docs/code-template.component';
|
||||||
@@ -61,6 +62,7 @@ import { SponsorComponent } from './components/sponsor/sponsor.component';
|
|||||||
AboutComponent,
|
AboutComponent,
|
||||||
MasterPageComponent,
|
MasterPageComponent,
|
||||||
BisqMasterPageComponent,
|
BisqMasterPageComponent,
|
||||||
|
LiquidMasterPageComponent,
|
||||||
TelevisionComponent,
|
TelevisionComponent,
|
||||||
BlockchainComponent,
|
BlockchainComponent,
|
||||||
StartComponent,
|
StartComponent,
|
||||||
@@ -145,5 +147,7 @@ export class AppModule {
|
|||||||
library.addIcons(faSortUp);
|
library.addIcons(faSortUp);
|
||||||
library.addIcons(faCaretUp);
|
library.addIcons(faCaretUp);
|
||||||
library.addIcons(faCaretDown);
|
library.addIcons(faCaretDown);
|
||||||
|
library.addIcons(faAngleRight);
|
||||||
|
library.addIcons(faAngleLeft);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,13 @@
|
|||||||
<th i18n="Asset ticker header">Ticker</th>
|
<th i18n="Asset ticker header">Ticker</th>
|
||||||
<th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th>
|
<th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th>
|
||||||
<th i18n="Asset ID header">Asset ID</th>
|
<th i18n="Asset ID header">Asset ID</th>
|
||||||
<th class="d-none d-lg-block" i18n="Asset issuance transaction header">Issuance TX</th>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset">
|
<tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset">
|
||||||
<td class="td-name">{{ asset.name }}</td>
|
<td class="td-name"><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.name }}</a></td>
|
||||||
<td>{{ asset.ticker }}</td>
|
<td>{{ asset.ticker }}</td>
|
||||||
<td class="d-none d-md-block"><a *ngIf="asset.entity" target="_blank" href="{{ 'http://' + asset.entity.domain }}">{{ asset.entity.domain }}</a></td>
|
<td class="d-none d-md-block">{{ asset.entity && asset.entity.domain }}</td>
|
||||||
<td><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.asset_id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.asset_id"></app-clipboard></td>
|
<td><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.asset_id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.asset_id"></app-clipboard></td>
|
||||||
<td class="d-none d-lg-block"><ng-template [ngIf]="asset.issuance_txin"><a [routerLink]="['/tx/' | relativeUrl, asset.issuance_txin.txid]">{{ asset.issuance_txin.txid | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.issuance_txin.txid"></app-clipboard></ng-template></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -47,15 +45,13 @@
|
|||||||
<th i18n="Asset ticker header">Ticker</th>
|
<th i18n="Asset ticker header">Ticker</th>
|
||||||
<th i18n="Asset Issuer Domain header">Issuer domain</th>
|
<th i18n="Asset Issuer Domain header">Issuer domain</th>
|
||||||
<th i18n="Asset ID header">Asset ID</th>
|
<th i18n="Asset ID header">Asset ID</th>
|
||||||
<th i18n="Asset issuance transaction header">Issuance TX</th>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let dummy of [0,0,0]">
|
<tr *ngFor="let dummy of [0,0,0,0,0,0,0,0,0,0]">
|
||||||
<td><span class="skeleton-loader"></span></td>
|
<td><span class="skeleton-loader"></span></td>
|
||||||
<td><span class="skeleton-loader"></span></td>
|
<td><span class="skeleton-loader"></span></td>
|
||||||
<td class="d-none d-md-block"><span class="skeleton-loader"></span></td>
|
<td class="d-none d-md-block"><span class="skeleton-loader"></span></td>
|
||||||
<td><span class="skeleton-loader"></span></td>
|
<td><span class="skeleton-loader"></span></td>
|
||||||
<td class="d-none d-lg-block"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export class AssetsComponent implements OnInit {
|
|||||||
const start = (this.page - 1) * this.itemsPerPage;
|
const start = (this.page - 1) * this.itemsPerPage;
|
||||||
if (searchText.length ) {
|
if (searchText.length ) {
|
||||||
const filteredAssets = this.assetsCache.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
|
const filteredAssets = this.assetsCache.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
|
||||||
|| asset.ticker.toLowerCase().indexOf(searchText.toLowerCase()) > -1);
|
|| (asset.ticker || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1);
|
||||||
this.assets = filteredAssets;
|
this.assets = filteredAssets;
|
||||||
return filteredAssets.slice(start, this.itemsPerPage + start);
|
return filteredAssets.slice(start, this.itemsPerPage + start);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<div class="container-info">
|
<div class="container-info">
|
||||||
<h1>
|
<h1>
|
||||||
<ng-template [ngIf]="stateService.env.OFFICIAL_BISQ_MARKETS" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
|
<ng-template [ngIf]="stateService.env.BASE_MODULE === 'bisq'" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
|
||||||
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
|
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
|
||||||
</h1>
|
</h1>
|
||||||
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
|
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
|
||||||
|
|||||||
@@ -17,19 +17,6 @@
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
|
||||||
overflow: scroll;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
font-size: 13px;
|
|
||||||
@media(min-width: 576px){
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-info{
|
.container-info{
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ export class BisqDashboardComponent implements OnInit {
|
|||||||
const newTickers = [];
|
const newTickers = [];
|
||||||
for (const t in tickers) {
|
for (const t in tickers) {
|
||||||
|
|
||||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
if (this.stateService.env.BASE_MODULE !== 'bisq') {
|
||||||
const pair = t.split('_');
|
const pair = t.split('_');
|
||||||
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
|
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
|
||||||
continue;
|
continue;
|
||||||
@@ -106,7 +106,7 @@ export class BisqDashboardComponent implements OnInit {
|
|||||||
])
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([trades, markets]) => {
|
map(([trades, markets]) => {
|
||||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
if (this.stateService.env.BASE_MODULE !== 'bisq') {
|
||||||
trades = trades.filter((trade) => {
|
trades = trades.filter((trade) => {
|
||||||
const pair = trade.market.split('_');
|
const pair = trade.market.split('_');
|
||||||
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
|
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title text-center">
|
<h5 class="card-title text-center">
|
||||||
<ng-template [ngIf]="stateService.env.OFFICIAL_BISQ_MARKETS" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
|
<ng-template [ngIf]="stateService.env.BASE_MODULE === 'bisq'" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
|
||||||
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
|
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-language-selector *ngIf="!stateService.env.OFFICIAL_BISQ_MARKETS"></app-language-selector>
|
<app-language-selector *ngIf="stateService.env.BASE_MODULE !== 'bisq'"></app-language-selector>
|
||||||
|
|
||||||
<div class="text-small text-center mt-3">
|
<div class="text-small text-center mt-3">
|
||||||
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
||||||
|
|||||||
@@ -18,15 +18,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
font-size: 13px;
|
overflow: scroll;
|
||||||
@media(min-width: 576px){
|
scrollbar-width: none;
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media(min-width: 576px){
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
thead th{
|
||||||
|
text-align: right;
|
||||||
|
&:first-child {
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar {
|
&:nth-child(3) {
|
||||||
|
display: none;
|
||||||
|
@media(min-width: 1100px){
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
text-align: right;
|
||||||
|
&:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
display: none;
|
display: none;
|
||||||
|
@media(min-width: 1100px){
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
@@ -41,44 +70,43 @@
|
|||||||
background-color: #1d1f31;
|
background-color: #1d1f31;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
color: #4a68b9;
|
color: #4a68b9;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-block {
|
.info-block {
|
||||||
float: left;
|
float: left;
|
||||||
width: 350px;
|
width: 350px;
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #2d3348;
|
background-color: #2d3348;
|
||||||
height: 1.1rem;
|
height: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-warning {
|
.bg-warning {
|
||||||
background-color: #b58800 !important;
|
background-color: #b58800 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-loader {
|
.skeleton-loader {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
&.shorter {
|
&.shorter {
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-padding {
|
.more-padding {
|
||||||
padding: 1.25rem 2rem 1.25rem 2rem;
|
padding: 1.25rem 2rem 1.25rem 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-card {
|
.graph-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
height: 385px;
|
height: 385px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ export class BisqMainDashboardComponent implements OnInit {
|
|||||||
const newTickers = [];
|
const newTickers = [];
|
||||||
for (const t in tickers) {
|
for (const t in tickers) {
|
||||||
|
|
||||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
if (this.stateService.env.BASE_MODULE !== 'bisq') {
|
||||||
const pair = t.split('_');
|
const pair = t.split('_');
|
||||||
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
|
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
|
||||||
continue;
|
continue;
|
||||||
@@ -114,7 +114,7 @@ export class BisqMainDashboardComponent implements OnInit {
|
|||||||
])
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([trades, markets]) => {
|
map(([trades, markets]) => {
|
||||||
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
|
if (this.stateService.env.BASE_MODULE !== 'bisq') {
|
||||||
trades = trades.filter((trade) => {
|
trades = trades.filter((trade) => {
|
||||||
const pair = trade.market.split('_');
|
const pair = trade.market.split('_');
|
||||||
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
|
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
|
||||||
|
|||||||
@@ -1,12 +1,38 @@
|
|||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
-ms-overflow-style: none;
|
scrollbar-width: none;
|
||||||
scrollbar-width: none;
|
font-size: 13px;
|
||||||
font-size: 13px;
|
&::-webkit-scrollbar {
|
||||||
@media(min-width: 576px){
|
display: none;
|
||||||
font-size: 16px;
|
}
|
||||||
|
@media(min-width: 576px){
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
thead th{
|
||||||
|
text-align: right;
|
||||||
|
&:first-child{
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar {
|
&:nth-child(2) {
|
||||||
|
display: none;
|
||||||
|
@media(min-width: 1100px){
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
text-align: right;
|
||||||
|
&:first-child{
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
display: none;
|
display: none;
|
||||||
|
@media(min-width: 1100px){
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td i18n="transaction.fee-per-vbyte|Transaction fee">Fee per vByte</td>
|
<td i18n="transaction.fee-per-vbyte|Transaction fee">Fee per vByte</td>
|
||||||
<td *ngIf="!isLoadingTx; else loadingTxFee">
|
<td *ngIf="!isLoadingTx; else loadingTxFee">
|
||||||
{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} <span class="symbol">sat/vB</span>
|
{{ tx.fee / (tx.weight / 4) | feeRounding }} <span class="symbol">sat/vB</span>
|
||||||
|
|
||||||
<app-tx-fee-rating [tx]="tx"></app-tx-fee-rating>
|
<app-tx-fee-rating [tx]="tx"></app-tx-fee-rating>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -85,7 +85,11 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tx.version) {
|
if (tx.version) {
|
||||||
this.router.navigate(['/tx/', this.txId], { state: { data: tx, bsqTx: true }});
|
if (this.stateService.env.BASE_MODULE === 'bisq') {
|
||||||
|
window.location.replace('https://mempool.space/tx/' + this.txId);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/tx/', this.txId], { state: { data: tx, bsqTx: true }});
|
||||||
|
}
|
||||||
return of(null);
|
return of(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="about-text">
|
<div class="about-text" *ngIf="stateService.env.BASE_MODULE === 'mempool'; else marginBox">
|
||||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></h5>
|
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></h5>
|
||||||
<p i18n>Building a mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, without any advertising, altcoins, or third-party trackers.</p>
|
<p i18n>Building a mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, without any advertising, altcoins, or third-party trackers.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<ng-template #marginBox>
|
||||||
|
<div class="no-about-margin"></div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<div class="social-icons">
|
<div class="social-icons">
|
||||||
<a target="_blank" href="https://github.com/mempool/mempool">
|
<a target="_blank" href="https://github.com/mempool/mempool">
|
||||||
@@ -47,6 +50,10 @@
|
|||||||
<img class="image" src="/resources/profile/foundry.svg" />
|
<img class="image" src="/resources/profile/foundry.svg" />
|
||||||
<span>Foundry</span>
|
<span>Foundry</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://unchained-capital.com/" target="_blank" title="Unchained">
|
||||||
|
<img class="image" src="/resources/profile/unchained.svg" />
|
||||||
|
<span>Unchained</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -176,4 +176,8 @@
|
|||||||
.footer-version {
|
.footer-version {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-about-margin {
|
||||||
|
height: 10px;
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ export class AboutComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private stateService: StateService,
|
public stateService: StateService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
@@ -41,7 +41,7 @@ export class AboutComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sponsor() {
|
sponsor() {
|
||||||
if (this.officialMempoolSpace) {
|
if (this.officialMempoolSpace && this.stateService.env.BASE_MODULE === 'mempool') {
|
||||||
this.router.navigateByUrl('/sponsor');
|
this.router.navigateByUrl('/sponsor');
|
||||||
} else {
|
} else {
|
||||||
this.showNavigateToSponsor = true;
|
this.showNavigateToSponsor = true;
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
||||||
|
<td i18n="address.unconfidential">Unconfidential</td>
|
||||||
|
<td><a [routerLink]="['/address/' | relativeUrl, addressInfo.unconfidential]">{{ addressInfo.unconfidential }}</a> <app-clipboard [text]="addressInfo.unconfidential"></app-clipboard></td>
|
||||||
|
</tr>
|
||||||
<ng-template [ngIf]="!address.electrum">
|
<ng-template [ngIf]="!address.electrum">
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="address.total-received">Total received</td>
|
<td i18n="address.total-received">Total received</td>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { AudioService } from 'src/app/services/audio.service';
|
|||||||
import { ApiService } from 'src/app/services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
import { of, merge, Subscription, Observable } from 'rxjs';
|
import { of, merge, Subscription, Observable } from 'rxjs';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
|
import { AddressInformation } from 'src/app/interfaces/node-api.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-address',
|
selector: 'app-address',
|
||||||
@@ -27,6 +28,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
error: any;
|
error: any;
|
||||||
mainSubscription: Subscription;
|
mainSubscription: Subscription;
|
||||||
addressLoadingStatus$: Observable<number>;
|
addressLoadingStatus$: Observable<number>;
|
||||||
|
addressInfo: null | AddressInformation = null;
|
||||||
|
|
||||||
totalConfirmedTxCount = 0;
|
totalConfirmedTxCount = 0;
|
||||||
loadedConfirmedTxCount = 0;
|
loadedConfirmedTxCount = 0;
|
||||||
@@ -67,8 +69,12 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.address = null;
|
this.address = null;
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
this.transactions = null;
|
this.transactions = null;
|
||||||
|
this.addressInfo = null;
|
||||||
document.body.scrollTo(0, 0);
|
document.body.scrollTo(0, 0);
|
||||||
this.addressString = params.get('id') || '';
|
this.addressString = params.get('id') || '';
|
||||||
|
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
|
||||||
|
this.addressString = this.addressString.toLowerCase();
|
||||||
|
}
|
||||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||||
|
|
||||||
return merge(
|
return merge(
|
||||||
@@ -92,10 +98,20 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((address) => !!address),
|
filter((address) => !!address),
|
||||||
|
tap((address: Address) => {
|
||||||
|
if (this.stateService.network === 'liquid' && /^([m-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address.address)) {
|
||||||
|
this.apiService.validateAddress$(address.address)
|
||||||
|
.subscribe((addressInfo) => {
|
||||||
|
this.addressInfo = addressInfo;
|
||||||
|
this.websocketService.startTrackAddress(addressInfo.unconfidential);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.websocketService.startTrackAddress(address.address);
|
||||||
|
}
|
||||||
|
}),
|
||||||
switchMap((address) => {
|
switchMap((address) => {
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.updateChainStats();
|
this.updateChainStats();
|
||||||
this.websocketService.startTrackAddress(address.address);
|
|
||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
return this.electrsApiService.getAddressTransactions$(address.address);
|
return this.electrsApiService.getAddressTransactions$(address.address);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-amount',
|
selector: 'app-amount',
|
||||||
@@ -8,11 +8,13 @@ import { Observable } from 'rxjs';
|
|||||||
styleUrls: ['./amount.component.scss'],
|
styleUrls: ['./amount.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class AmountComponent implements OnInit {
|
export class AmountComponent implements OnInit, OnDestroy {
|
||||||
conversions$: Observable<any>;
|
conversions$: Observable<any>;
|
||||||
viewFiat$: Observable<boolean>;
|
viewFiat$: Observable<boolean>;
|
||||||
network = '';
|
network = '';
|
||||||
|
|
||||||
|
stateSubscription: Subscription;
|
||||||
|
|
||||||
@Input() satoshis: number;
|
@Input() satoshis: number;
|
||||||
@Input() digitsInfo = '1.8-8';
|
@Input() digitsInfo = '1.8-8';
|
||||||
@Input() noFiat = false;
|
@Input() noFiat = false;
|
||||||
@@ -24,7 +26,13 @@ export class AmountComponent implements OnInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
|
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
|
||||||
this.conversions$ = this.stateService.conversions$.asObservable();
|
this.conversions$ = this.stateService.conversions$.asObservable();
|
||||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
this.stateSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.stateSubscription) {
|
||||||
|
this.stateSubscription.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,39 @@
|
|||||||
<div class="code">
|
<div class="code">
|
||||||
<ul ngbNav #navCodeTemplate="ngbNav" class="nav-tabs code-tab">
|
<ul ngbNav #navCodeTemplate="ngbNav" class="nav-tabs code-tab">
|
||||||
<li ngbNavItem *ngIf="code.codeSample.curl">
|
<li ngbNavItem *ngIf="code.codeTemplate.curl && method !== 'websocket'">
|
||||||
<a ngbNavLink>cURL</a>
|
<a ngbNavLink>cURL</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapCurl(code.codeSample.curl)"></app-clipboard></div>
|
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapCurlTemplate(code)"></app-clipboard></div>
|
||||||
<pre><code [innerText]="wrapCurl(code.codeSample.curl)"></code></pre>
|
<pre><code [innerText]="wrapCurlTemplate(code)"></code></pre>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
<li ngbNavItem>
|
<li ngbNavItem>
|
||||||
<a ngbNavLink>CommonJS</a>
|
<a ngbNavLink>CommonJS</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapCommonJS(code.codeSample.commonJS)"></app-clipboard></div>
|
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapCommonJS(code)"></app-clipboard></div>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="https://github.com/mempool/mempool.js" target="_blank">github repository</a>
|
<a [href]="npmGithubLink()" target="_blank">github repository</a>
|
||||||
</div>
|
</div>
|
||||||
<pre><code [innerText]="wrapCommonJS(code.codeSample.commonJS)"></code></pre>
|
<pre><code [innerText]="wrapCommonJS(code)"></code></pre>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
<li ngbNavItem>
|
<li ngbNavItem>
|
||||||
<a ngbNavLink>ES Module</a>
|
<a ngbNavLink>ES Module</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="subtitle"><ng-container i18n="API Docs install lib">Install Package</ng-container> <app-clipboard [text]="esModuleInstall"></app-clipboard></div>
|
<div class="subtitle"><ng-container i18n="API Docs install lib">Install Package</ng-container> <app-clipboard [text]="wrapImportTemplate()"></app-clipboard></div>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="https://github.com/mempool/mempool.js" target="_blank">github repository</a>
|
<a [href]="npmGithubLink()" target="_blank">github repository</a>
|
||||||
<a href="https://www.npmjs.org/package/@mempool/mempool.js" target="_blank">npm package</a>
|
<a [href]="npmModuleLink()" target="_blank">npm package</a>
|
||||||
</div>
|
</div>
|
||||||
<pre><code [innerText]="esModuleInstall"></code></pre>
|
<pre><code [innerText]="wrapImportTemplate()"></code></pre>
|
||||||
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapESmodule(code.codeSample.esModule)"></app-clipboard></div>
|
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapEsModule(code)"></app-clipboard></div>
|
||||||
<pre><code [innerText]="wrapESmodule(code.codeSample.esModule)"></code></pre>
|
<pre><code [innerText]="wrapEsModule(code)"></code></pre>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div [ngbNavOutlet]="navCodeTemplate"></div>
|
<div [ngbNavOutlet]="navCodeTemplate"></div>
|
||||||
<div *ngIf="code.responseSample" class="response">
|
<div *ngIf="code.codeTemplate && wrapResponse(code) !== ''" class="response">
|
||||||
<div class="subtitle"><ng-container i18n="API Docs API response">Response</ng-container> <app-clipboard [text]="code.responseSample"></app-clipboard></div>
|
<div class="subtitle"><ng-container i18n="API Docs API response">Response</ng-container> <app-clipboard [text]="wrapResponse(code)"></app-clipboard></div>
|
||||||
<pre><code [innerText]="code.responseSample"></code></pre>
|
<pre><code [innerText]="wrapResponse(code)"></code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,69 +1,189 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { Env, StateService } from 'src/app/services/state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-code-template',
|
selector: 'app-code-template',
|
||||||
templateUrl: './code-template.component.html',
|
templateUrl: './code-template.component.html',
|
||||||
styleUrls: ['./code-template.component.scss']
|
styleUrls: ['./code-template.component.scss']
|
||||||
})
|
})
|
||||||
export class CodeTemplateComponent {
|
export class CodeTemplateComponent implements OnInit {
|
||||||
@Input() network: string;
|
@Input() network: string;
|
||||||
@Input() code: {
|
@Input() code: any;
|
||||||
codeSample: {
|
@Input() hostname: string;
|
||||||
esModule: string;
|
@Input() method: 'get' | 'post' | 'websocket' = 'get';
|
||||||
commonJS: string;
|
env: Env;
|
||||||
curl: string;
|
|
||||||
},
|
|
||||||
responseSample: string;
|
|
||||||
};
|
|
||||||
hostname = document.location.hostname;
|
|
||||||
esModuleInstall = `# npm
|
|
||||||
npm install @mempool/mempool.js --save
|
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn add @mempool/mempool.js`;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private stateService: StateService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
normalizeCodeHostname(code: string) {
|
ngOnInit(): void {
|
||||||
let codeText: string;
|
this.env = this.stateService.env;
|
||||||
if (this.network === 'bisq' || this.network === 'liquid'){
|
}
|
||||||
codeText = code.replace('%{1}', this.network);
|
|
||||||
}else{
|
npmGithubLink(){
|
||||||
codeText = code.replace('%{1}', 'bitcoin');
|
let npmLink = `https://github.com/mempool/mempool.js`;
|
||||||
|
if (this.network === 'bisq') {
|
||||||
|
npmLink = `https://github.com/mempool/mempool.js/tree/main/npm-bisq-js`;
|
||||||
|
}
|
||||||
|
if (this.network === 'liquid') {
|
||||||
|
npmLink = `https://github.com/mempool/mempool.js/tree/main/npm-liquid-js`;
|
||||||
|
}
|
||||||
|
return npmLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
npmModuleLink() {
|
||||||
|
let npmLink = `https://www.npmjs.org/package/@mempool/mempool.js`;
|
||||||
|
if (this.network === 'bisq') {
|
||||||
|
npmLink = `https://www.npmjs.org/package/@mempool/bisq.js`;
|
||||||
|
}
|
||||||
|
if (this.network === 'liquid') {
|
||||||
|
npmLink = `https://www.npmjs.org/package/@mempool/liquid.js`;
|
||||||
|
}
|
||||||
|
return npmLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeHostsESModule(codeText: string) {
|
||||||
|
if (this.env.BASE_MODULE === 'mempool') {
|
||||||
|
if (['liquid', 'bisq'].includes(this.network)) {
|
||||||
|
codeText = codeText.replace('%{0}', this.network);
|
||||||
|
} else {
|
||||||
|
codeText = codeText.replace('%{0}', 'bitcoin');
|
||||||
|
}
|
||||||
|
if(['', 'main', 'liquid', 'bisq'].includes(this.network)) {
|
||||||
|
codeText = codeText.replace('mempoolJS();', `mempoolJS({
|
||||||
|
hostname: '${document.location.hostname}'
|
||||||
|
});`);
|
||||||
|
} else {
|
||||||
|
codeText = codeText.replace('mempoolJS();', `mempoolJS({
|
||||||
|
hostname: '${document.location.hostname}',
|
||||||
|
network: '${this.network}'
|
||||||
|
});`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.env.BASE_MODULE === 'bisq') {
|
||||||
|
codeText = codeText.replace('} = mempoolJS();', ` = bisqJS();`);
|
||||||
|
codeText = codeText.replace('{ %{0}: ', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.env.BASE_MODULE === 'liquid') {
|
||||||
|
codeText = codeText.replace('} = mempoolJS();', ` = liquidJS();`);
|
||||||
|
codeText = codeText.replace('{ %{0}: ', '');
|
||||||
}
|
}
|
||||||
return codeText;
|
return codeText;
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapESmodule(code: string) {
|
normalizeHostsCommonJS(codeText: string) {
|
||||||
let codeText = this.normalizeCodeHostname(code);
|
if (this.env.BASE_MODULE === 'mempool') {
|
||||||
|
if (['liquid', 'bisq'].includes(this.network)) {
|
||||||
if (this.network && this.network !== 'mainnet') {
|
codeText = codeText.replace('%{0}', this.network);
|
||||||
codeText = codeText.replace('mempoolJS();', `mempoolJS({
|
} else {
|
||||||
hostname: '${this.hostname}/${this.network}'
|
codeText = codeText.replace('%{0}', 'bitcoin');
|
||||||
});` );
|
}
|
||||||
|
if(['', 'main', 'liquid', 'bisq'].includes(this.network)) {
|
||||||
|
codeText = codeText.replace('mempoolJS();', `mempoolJS({
|
||||||
|
hostname: '${document.location.hostname}'
|
||||||
|
});`);
|
||||||
|
} else {
|
||||||
|
codeText = codeText.replace('mempoolJS();', `mempoolJS({
|
||||||
|
hostname: '${document.location.hostname}',
|
||||||
|
network: '${this.network}'
|
||||||
|
});`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return `import mempoolJS from "@mempool/mempool.js";
|
if (this.env.BASE_MODULE === 'bisq') {
|
||||||
|
codeText = codeText.replace('} = mempoolJS();', ` = bisqJS();`);
|
||||||
|
codeText = codeText.replace('{ %{0}: ', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.env.BASE_MODULE === 'liquid') {
|
||||||
|
codeText = codeText.replace('} = mempoolJS();', ` = liquidJS();`);
|
||||||
|
codeText = codeText.replace('{ %{0}: ', '');
|
||||||
|
}
|
||||||
|
return codeText;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapEsModule(code: any) {
|
||||||
|
let codeText: string;
|
||||||
|
if (code.codeTemplate) {
|
||||||
|
codeText = this.normalizeHostsESModule(code.codeTemplate.esModule);
|
||||||
|
|
||||||
|
if(this.network === '' || this.network === 'main') {
|
||||||
|
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleMainnet.esModule);
|
||||||
|
}
|
||||||
|
if (this.network === 'testnet') {
|
||||||
|
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
|
||||||
|
}
|
||||||
|
if (this.network === 'signet') {
|
||||||
|
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleSignet.esModule);
|
||||||
|
}
|
||||||
|
if (this.network === 'liquid') {
|
||||||
|
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleLiquid.esModule);
|
||||||
|
}
|
||||||
|
if (this.network === 'bisq') {
|
||||||
|
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleBisq.esModule);
|
||||||
|
}
|
||||||
|
|
||||||
|
let importText = `import mempoolJS from "@mempool/mempool.js";`;
|
||||||
|
if (this.env.BASE_MODULE === 'bisq') {
|
||||||
|
importText = `import bisqJS from "@mempool/bisq.js";`;
|
||||||
|
}
|
||||||
|
if (this.env.BASE_MODULE === 'liquid') {
|
||||||
|
importText = `import liquidJS from "@mempool/liquid.js";`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${importText}
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
${codeText}
|
${codeText}
|
||||||
};
|
};
|
||||||
init();`;
|
init();`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapCommonJS(code: string) {
|
wrapCommonJS(code: any) {
|
||||||
let codeText = this.normalizeCodeHostname(code);
|
let codeText: string;
|
||||||
|
if (code.codeTemplate) {
|
||||||
|
codeText = this.normalizeHostsCommonJS(code.codeTemplate.commonJS);
|
||||||
|
|
||||||
if (this.network && this.network !== 'mainnet') {
|
if(this.network === '' || this.network === 'main') {
|
||||||
codeText = codeText.replace('mempoolJS();', `mempoolJS({
|
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleMainnet.esModule);
|
||||||
hostname: '${this.hostname}/${this.network}'
|
}
|
||||||
});` );
|
if (this.network === 'testnet') {
|
||||||
}
|
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleTestnet.esModule);
|
||||||
return `<!DOCTYPE html>
|
}
|
||||||
|
if (this.network === 'signet') {
|
||||||
|
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleSignet.esModule);
|
||||||
|
}
|
||||||
|
if (this.network === 'liquid') {
|
||||||
|
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleLiquid.esModule);
|
||||||
|
}
|
||||||
|
if (this.network === 'bisq') {
|
||||||
|
codeText = this.replaceJSPlaceholder(codeText, code.codeSampleBisq.esModule);
|
||||||
|
}
|
||||||
|
|
||||||
|
let importText = `<script src="https://mempool.space/mempool.js"></script>`;
|
||||||
|
if (this.env.BASE_MODULE === 'bisq') {
|
||||||
|
importText = `<script src="https://bisq.markets/bisq.js"></script>`;
|
||||||
|
}
|
||||||
|
if (this.env.BASE_MODULE === 'liquid') {
|
||||||
|
importText = `<script src="https://liquid.network/liquid.js"></script>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultHtml = '<pre id="result"></pre>';
|
||||||
|
if (this.method === 'websocket') {
|
||||||
|
resultHtml = `<pre id="result-blocks"></pre>
|
||||||
|
<pre id="result-mempool-info"></pre>
|
||||||
|
<pre id="result-transactions"></pre>
|
||||||
|
<pre id="result-mempool-blocks"></pre>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script src="https://mempool.space/mempool.js"></script>
|
${importText}
|
||||||
<script>
|
<script>
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
${codeText}
|
${codeText}
|
||||||
@@ -71,14 +191,112 @@ init();`;
|
|||||||
init();
|
init();
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body>
|
||||||
|
${resultHtml}
|
||||||
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
|
||||||
wrapCurl(code: string) {
|
|
||||||
if (this.network && this.network !== 'mainnet') {
|
|
||||||
return code.replace('mempool.space/', `mempool.space/${this.network}/`);
|
|
||||||
}
|
}
|
||||||
return code;
|
}
|
||||||
|
|
||||||
|
wrapImportTemplate() {
|
||||||
|
|
||||||
|
let importTemplate = `# npm
|
||||||
|
npm install @mempool/mempool.js --save
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn add @mempool/mempool.js`;
|
||||||
|
|
||||||
|
if (this.env.BASE_MODULE === 'bisq') {
|
||||||
|
importTemplate = `# npm
|
||||||
|
npm install @mempool/bisq.js --save
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn add @mempool/bisq.js`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.env.BASE_MODULE === 'liquid') {
|
||||||
|
importTemplate = `# npm
|
||||||
|
npm install @mempool/liquid.js --save
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn add @mempool/liquid.js`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return importTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapCurlTemplate(code: any) {
|
||||||
|
if (code.codeTemplate) {
|
||||||
|
if (this.network === 'testnet') {
|
||||||
|
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleTestnet);
|
||||||
|
}
|
||||||
|
if (this.network === 'signet') {
|
||||||
|
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleSignet);
|
||||||
|
}
|
||||||
|
if (this.network === 'liquid') {
|
||||||
|
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleLiquid);
|
||||||
|
}
|
||||||
|
if (this.network === 'bisq') {
|
||||||
|
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleBisq);
|
||||||
|
}
|
||||||
|
if (this.network === '' || this.network === 'main') {
|
||||||
|
return this.replaceCurlPlaceholder(code.codeTemplate.curl, code.codeSampleMainnet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapResponse(code: any) {
|
||||||
|
if (this.method === 'websocket') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (this.network === 'testnet') {
|
||||||
|
return code.codeSampleTestnet.response;
|
||||||
|
}
|
||||||
|
if (this.network === 'signet') {
|
||||||
|
return code.codeSampleSignet.response;
|
||||||
|
}
|
||||||
|
if (this.network === 'liquid') {
|
||||||
|
return code.codeSampleLiquid.response;
|
||||||
|
}
|
||||||
|
if (this.network === 'bisq') {
|
||||||
|
return code.codeSampleBisq.response;
|
||||||
|
}
|
||||||
|
return code.codeSampleMainnet.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceJSPlaceholder(text: string, code: any) {
|
||||||
|
for (let index = 0; index < code.length; index++) {
|
||||||
|
const textReplace = code[index];
|
||||||
|
const indexNumber = index + 1;
|
||||||
|
text = text.replace('%{' + indexNumber + '}', textReplace);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceCurlPlaceholder(curlText: any, code: any) {
|
||||||
|
let text = curlText;
|
||||||
|
for (let index = 0; index < code.curl.length; index++) {
|
||||||
|
const textReplace = code.curl[index];
|
||||||
|
const indexNumber = index + 1;
|
||||||
|
text = text.replace('%{' + indexNumber + '}', textReplace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.env.BASE_MODULE === 'mempool') {
|
||||||
|
if (this.network === 'main' || this.network === '') {
|
||||||
|
if (this.method === 'post') {
|
||||||
|
return `curl POST -sSLd "${text}"`;
|
||||||
|
}
|
||||||
|
return `curl -sSL "${this.hostname}${text}"`;
|
||||||
|
}
|
||||||
|
if (this.method === 'post') {
|
||||||
|
text = text.replace('/api', `/${this.network}/api`);
|
||||||
|
return `curl POST -sSLd "${text}"`;
|
||||||
|
}
|
||||||
|
return `curl -sSL "${this.hostname}/${this.network}${text}"`;
|
||||||
|
}
|
||||||
|
if (this.env.BASE_MODULE !== 'mempool') {
|
||||||
|
return `curl -sSL "${this.hostname}${text}"`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||||
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
||||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||||
<img [src]="'./resources/bisq-markets.svg'" height="35" width="180" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
|
<img src="./resources/bisq/bisq-markets-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
|
||||||
<div class="connection-badge">
|
<div class="connection-badge">
|
||||||
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
|
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
|
||||||
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
|
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
|
||||||
@@ -10,15 +10,42 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<div ngbDropdown (window:resize)="onResize($event)" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED">
|
||||||
|
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||||
|
<img src="./resources/bisq-logo.png" style="width: 25px; height: 25px;" class="mr-1">
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||||
|
<a href="https://mempool.space" ngbDropdownItem class="mainnet"><img src="./resources/bitcoin-logo.png" style="width: 30px;" class="mr-1"> Mainnet</a>
|
||||||
|
<a href="https://mempool.space/signet" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><img src="./resources/signet-logo.png" style="width: 30px;" class="mr-1"> Signet</a>
|
||||||
|
<a href="https://mempool.space/testnet" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</a>
|
||||||
|
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
||||||
|
<button ngbDropdownItem class="mainnet active" routerLink="/"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</button>
|
||||||
|
<a href="https://liquid.network" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1"> Liquid</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="navbar-collapse" id="navbarCollapse">
|
<div class="navbar-collapse" id="navbarCollapse">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
||||||
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
|
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
||||||
|
<a class="nav-link" [routerLink]="['/transactions']" (click)="collapse()"><fa-icon [icon]="['fas', 'list']" [fixedWidth]="true" i18n-title="master-page.transactions" title="Transactions"></fa-icon></a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" routerLinkActive="active">
|
||||||
|
<a class="nav-link" [routerLink]="['/blocks']" (click)="collapse()"><fa-icon [icon]="['fas', 'cubes']" [fixedWidth]="true" i18n-title="master-page.blocks" title="Blocks"></fa-icon></a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" routerLinkActive="active">
|
||||||
|
<a class="nav-link" [routerLink]="['/stats']" (click)="collapse()"><fa-icon [icon]="['fas', 'file-alt']" [fixedWidth]="true" i18n-title="master-page.stats" title="Stats"></fa-icon></a>
|
||||||
|
</li>
|
||||||
<li class="nav-item mr-2" routerLinkActive="active">
|
<li class="nav-item mr-2" routerLinkActive="active">
|
||||||
<a class="nav-link" [routerLink]="['/api' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cogs']" [fixedWidth]="true" i18n-title="master-page.api" title="API"></fa-icon></a>
|
<a class="nav-link" [routerLink]="['/api' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cogs']" [fixedWidth]="true" i18n-title="master-page.api" title="API"></fa-icon></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" routerLinkActive="active">
|
||||||
|
<a class="nav-link" [routerLink]="['/about']" (click)="collapse()"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" i18n-title="master-page.about" title="About"></fa-icon></a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<app-search-form class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ fa-icon {
|
|||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
min-height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.nav-item {
|
li.nav-item {
|
||||||
|
margin: auto 5px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 992px) {
|
||||||
.navbar {
|
.navbar {
|
||||||
padding: 0rem 2rem;
|
padding: 0rem 2rem;
|
||||||
}
|
}
|
||||||
@@ -26,17 +28,54 @@ li.nav-item {
|
|||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
li.nav-item {
|
li.nav-item {
|
||||||
|
margin: auto 0px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
li.nav-item a {
|
.navbar-nav {
|
||||||
color: #ffffff;
|
background: #212121;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0px 0px 15px 0px #000;
|
||||||
|
flex-direction: row;
|
||||||
|
left: 0;
|
||||||
|
justify-content: center;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
font-size: 0.8em;
|
||||||
|
@media (min-width: 375px) {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-nav {
|
|
||||||
flex-direction: row;
|
.navbar-collapse {
|
||||||
justify-content: center;
|
flex-basis: auto;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.navbar-collapse {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.navbar-brand {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
@@ -81,5 +120,20 @@ nav {
|
|||||||
|
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items:center;
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.search-form-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navbar-dark .navbar-nav .nav-link {
|
||||||
|
color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { StateService } from '../../services/state.service';
|
import { Env, StateService } from '../../services/state.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -10,16 +10,23 @@ import { Observable } from 'rxjs';
|
|||||||
export class BisqMasterPageComponent implements OnInit {
|
export class BisqMasterPageComponent implements OnInit {
|
||||||
connectionState$: Observable<number>;
|
connectionState$: Observable<number>;
|
||||||
navCollapsed = false;
|
navCollapsed = false;
|
||||||
|
env: Env;
|
||||||
|
isMobile = window.innerWidth <= 767.98;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.env = this.stateService.env;
|
||||||
this.connectionState$ = this.stateService.connectionState$;
|
this.connectionState$ = this.stateService.connectionState$;
|
||||||
}
|
}
|
||||||
|
|
||||||
collapse(): void {
|
collapse(): void {
|
||||||
this.navCollapsed = !this.navCollapsed;
|
this.navCollapsed = !this.navCollapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onResize(event: any) {
|
||||||
|
this.isMobile = window.innerWidth <= 767.98;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,39 @@
|
|||||||
<div class="container-xl" (window:resize)="onResize($event)">
|
<div class="container-xl" (window:resize)="onResize($event)">
|
||||||
|
|
||||||
<div class="title-block" id="block">
|
<div class="title-block" id="block">
|
||||||
<h1><ng-template [ngIf]="blockHeight === 0" i18n="block.genesis">Genesis </ng-template><ng-template [ngIf]="blockHeight" i18n="block.block">Block <a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a></ng-template></h1>
|
<h1>
|
||||||
|
<ng-template [ngIf]="blockHeight === 0" i18n="block.genesis">Genesis
|
||||||
|
<div class="next-previous-blocks">
|
||||||
|
<a *ngIf="showNextBlocklink" [routerLink]="['/block/' | relativeUrl, nextBlockHeight]" (click)="navigateToNextBlock()" i18n-ngbTooltip="Next Block" ngbTooltip="Next Block" placement="bottom">
|
||||||
|
<fa-icon [icon]="['fas', 'angle-left']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a>
|
||||||
|
<span placement="bottom" class="disable">
|
||||||
|
<fa-icon [icon]="['fas', 'angle-right']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template [ngIf]="blockHeight" i18n="block.block"> Block
|
||||||
|
<div class="next-previous-blocks">
|
||||||
|
<a *ngIf="showNextBlocklink" [routerLink]="['/block/' | relativeUrl, nextBlockHeight]" (click)="navigateToNextBlock()" i18n-ngbTooltip="Next Block" ngbTooltip="Next Block" placement="bottom">
|
||||||
|
<fa-icon [icon]="['fas', 'angle-left']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<span *ngIf="!showNextBlocklink" placement="bottom" class="disable">
|
||||||
|
<fa-icon [icon]="['fas', 'angle-left']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</span>
|
||||||
|
<a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a>
|
||||||
|
<a *ngIf="showPreviousBlocklink && block" [routerLink]="['/block/' | relativeUrl, block.previousblockhash]" (click)="navigateToPreviousBlock()" i18n-ngbTooltip="Previous Block" ngbTooltip="Previous Block" placement="bottom">
|
||||||
|
<fa-icon [icon]="['fas', 'angle-right']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<span *ngIf="!showPreviousBlocklink" placement="bottom" class="disable">
|
||||||
|
<fa-icon [icon]="['fas', 'angle-right']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="grow"></div>
|
||||||
|
|
||||||
<button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">✕</button>
|
<button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ h1 {
|
|||||||
float: left;
|
float: left;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
a {
|
||||||
|
&:hover, &:focus{
|
||||||
|
text-decoration: none;;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-link {
|
.address-link {
|
||||||
@@ -110,4 +115,29 @@ h1 {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grow {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-previous-blocks {
|
||||||
|
font-size: 36px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1ad8f4;
|
||||||
|
&:hover, &:focus {
|
||||||
|
color: #09a3ba;
|
||||||
|
display: inline-block;
|
||||||
|
// transform: scale(1.2);
|
||||||
|
// transition: 150ms all ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.disable {
|
||||||
|
font-size: 36px;
|
||||||
|
color: #393e5c73;
|
||||||
}
|
}
|
||||||
@@ -18,14 +18,15 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
network = '';
|
network = '';
|
||||||
block: Block;
|
block: Block;
|
||||||
blockHeight: number;
|
blockHeight: number;
|
||||||
|
nextBlockHeight: number;
|
||||||
blockHash: string;
|
blockHash: string;
|
||||||
isLoadingBlock = true;
|
isLoadingBlock = true;
|
||||||
latestBlock: Block;
|
latestBlock: Block;
|
||||||
|
latestBlocks: Block[] = [];
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
isLoadingTransactions = true;
|
isLoadingTransactions = true;
|
||||||
error: any;
|
error: any;
|
||||||
blockSubsidy: number;
|
blockSubsidy: number;
|
||||||
subscription: Subscription;
|
|
||||||
fees: number;
|
fees: number;
|
||||||
paginationMaxSize: number;
|
paginationMaxSize: number;
|
||||||
coinbaseTx: Transaction;
|
coinbaseTx: Transaction;
|
||||||
@@ -33,6 +34,14 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
itemsPerPage: number;
|
itemsPerPage: number;
|
||||||
txsLoadingStatus$: Observable<number>;
|
txsLoadingStatus$: Observable<number>;
|
||||||
showDetails = false;
|
showDetails = false;
|
||||||
|
showPreviousBlocklink = true;
|
||||||
|
showNextBlocklink = true;
|
||||||
|
|
||||||
|
subscription: Subscription;
|
||||||
|
keyNavigationSubscription: Subscription;
|
||||||
|
blocksSubscription: Subscription;
|
||||||
|
networkChangedSubscription: Subscription;
|
||||||
|
queryParamsSubscription: Subscription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -56,8 +65,20 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
map((indicators) => indicators['blocktxs-' + this.blockHash] !== undefined ? indicators['blocktxs-' + this.blockHash] : 0)
|
map((indicators) => indicators['blocktxs-' + this.blockHash] !== undefined ? indicators['blocktxs-' + this.blockHash] : 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.subscription = this.route.paramMap
|
this.blocksSubscription = this.stateService.blocks$
|
||||||
.pipe(
|
.subscribe(([block]) => {
|
||||||
|
this.latestBlock = block;
|
||||||
|
this.latestBlocks.unshift(block);
|
||||||
|
this.latestBlocks = this.latestBlocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
||||||
|
this.setNextAndPreviousBlockLink();
|
||||||
|
|
||||||
|
if (block.id === this.blockHash) {
|
||||||
|
this.block = block;
|
||||||
|
this.fees = block.reward / 100000000 - this.blockSubsidy;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.subscription = this.route.paramMap.pipe(
|
||||||
switchMap((params: ParamMap) => {
|
switchMap((params: ParamMap) => {
|
||||||
const blockHash: string = params.get('id') || '';
|
const blockHash: string = params.get('id') || '';
|
||||||
this.block = undefined;
|
this.block = undefined;
|
||||||
@@ -85,7 +106,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
this.isLoadingBlock = true;
|
this.isLoadingBlock = true;
|
||||||
|
|
||||||
|
let blockInCache: Block;
|
||||||
if (isBlockHeight) {
|
if (isBlockHeight) {
|
||||||
|
blockInCache = this.latestBlocks.find((block) => block.height === parseInt(blockHash, 10));
|
||||||
|
if (blockInCache) {
|
||||||
|
return of(blockInCache);
|
||||||
|
}
|
||||||
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
|
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((hash) => {
|
switchMap((hash) => {
|
||||||
@@ -97,12 +123,21 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blockInCache = this.latestBlocks.find((block) => block.id === this.blockHash);
|
||||||
|
if (blockInCache) {
|
||||||
|
return of(blockInCache);
|
||||||
|
}
|
||||||
|
|
||||||
return this.electrsApiService.getBlock$(blockHash);
|
return this.electrsApiService.getBlock$(blockHash);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
tap((block: Block) => {
|
tap((block: Block) => {
|
||||||
this.block = block;
|
this.block = block;
|
||||||
this.blockHeight = block.height;
|
this.blockHeight = block.height;
|
||||||
|
this.nextBlockHeight = block.height + 1;
|
||||||
|
this.setNextAndPreviousBlockLink();
|
||||||
|
|
||||||
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
|
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
|
||||||
this.isLoadingBlock = false;
|
this.isLoadingBlock = false;
|
||||||
if (block.coinbaseTx) {
|
if (block.coinbaseTx) {
|
||||||
@@ -140,24 +175,38 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.isLoadingBlock = false;
|
this.isLoadingBlock = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.stateService.blocks$
|
this.networkChangedSubscription = this.stateService.networkChanged$
|
||||||
.subscribe(([block]) => this.latestBlock = block);
|
|
||||||
|
|
||||||
this.stateService.networkChanged$
|
|
||||||
.subscribe((network) => this.network = network);
|
.subscribe((network) => this.network = network);
|
||||||
|
|
||||||
this.route.queryParams.subscribe((params) => {
|
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
|
||||||
if (params.showDetails === 'true') {
|
if (params.showDetails === 'true') {
|
||||||
this.showDetails = true;
|
this.showDetails = true;
|
||||||
} else {
|
} else {
|
||||||
this.showDetails = false;
|
this.showDetails = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.keyNavigationSubscription = this.stateService.keyNavigation$.subscribe((event) => {
|
||||||
|
if (this.showPreviousBlocklink && event.key === 'ArrowRight' && this.nextBlockHeight - 2 >= 0) {
|
||||||
|
this.navigateToPreviousBlock();
|
||||||
|
}
|
||||||
|
if (event.key === 'ArrowLeft') {
|
||||||
|
if (this.showNextBlocklink) {
|
||||||
|
this.navigateToNextBlock();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([(this.network && this.stateService.env.BASE_MODULE === 'mempool' ? '/' + this.network : '') + '/mempool-block/', '0']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.stateService.markBlock$.next({});
|
this.stateService.markBlock$.next({});
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
|
this.keyNavigationSubscription.unsubscribe();
|
||||||
|
this.blocksSubscription.unsubscribe();
|
||||||
|
this.networkChangedSubscription.unsubscribe();
|
||||||
|
this.queryParamsSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
setBlockSubsidy() {
|
setBlockSubsidy() {
|
||||||
@@ -222,4 +271,34 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
onResize(event: any) {
|
onResize(event: any) {
|
||||||
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigateToPreviousBlock() {
|
||||||
|
if (!this.block) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const block = this.latestBlocks.find((b) => b.height === this.nextBlockHeight - 2);
|
||||||
|
this.router.navigate([(this.network && this.stateService.env.BASE_MODULE === 'mempool' ? '/' + this.network : '') + '/block/',
|
||||||
|
block ? block.id : this.block.previousblockhash], { state: { data: { block, blockHeight: this.nextBlockHeight - 2 } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToNextBlock() {
|
||||||
|
const block = this.latestBlocks.find((b) => b.height === this.nextBlockHeight);
|
||||||
|
this.router.navigate([(this.network && this.stateService.env.BASE_MODULE === 'mempool' ? '/' + this.network : '') + '/block/',
|
||||||
|
block ? block.id : this.nextBlockHeight], { state: { data: { block, blockHeight: this.nextBlockHeight } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
setNextAndPreviousBlockLink(){
|
||||||
|
if (this.latestBlock && this.blockHeight) {
|
||||||
|
if (this.blockHeight === 0){
|
||||||
|
this.showPreviousBlocklink = false;
|
||||||
|
} else {
|
||||||
|
this.showPreviousBlocklink = true;
|
||||||
|
}
|
||||||
|
if (this.latestBlock.height && this.latestBlock.height === this.blockHeight) {
|
||||||
|
this.showNextBlocklink = false;
|
||||||
|
} else {
|
||||||
|
this.showNextBlocklink = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<div class="blocks-container" *ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate">
|
<div class="blocks-container blockchain-blocks-container" *ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate">
|
||||||
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" >
|
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" >
|
||||||
<div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]">
|
<div class="text-center bitcoin-block mined-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]">
|
||||||
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }" class="blockLink"> </a>
|
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }" class="blockLink"> </a>
|
||||||
<div class="block-height">
|
<div class="block-height">
|
||||||
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
|
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="block-body">
|
<div class="block-body">
|
||||||
<div class="fees">
|
<div class="fees">
|
||||||
~{{ block.medianFee | number:'1.0-0' }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
~{{ block.medianFee | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="fee-span">
|
<div class="fee-span">
|
||||||
{{ block.feeRange[1] | number:'1.0-0' }} - {{ block.feeRange[block.feeRange.length - 1] | number:'1.0-0' }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
{{ block.feeRange[1] | number:feeRounding }} - {{ block.feeRange[block.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="block-size" [innerHTML]="block.size | bytes: 2">‎</div>
|
<div class="block-size" [innerHTML]="block.size | bytes: 2">‎</div>
|
||||||
<div class="transaction-count">
|
<div class="transaction-count">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
emptyBlockStyles = [];
|
emptyBlockStyles = [];
|
||||||
interval: any;
|
interval: any;
|
||||||
tabHidden = false;
|
tabHidden = false;
|
||||||
|
feeRounding = '1.0-0';
|
||||||
arrowVisible = false;
|
arrowVisible = false;
|
||||||
arrowLeftPx = 30;
|
arrowLeftPx = 30;
|
||||||
blocksFilled = false;
|
blocksFilled = false;
|
||||||
@@ -46,6 +46,9 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
if (this.stateService.network === 'liquid') {
|
||||||
|
this.feeRounding = '1.0-1';
|
||||||
|
}
|
||||||
this.emptyBlocks.forEach((b) => this.emptyBlockStyles.push(this.getStyleForEmptyBlock(b)));
|
this.emptyBlocks.forEach((b) => this.emptyBlockStyles.push(this.getStyleForEmptyBlock(b)));
|
||||||
this.loadingBlocks$ = this.stateService.isLoadingWebSocket$;
|
this.loadingBlocks$ = this.stateService.isLoadingWebSocket$;
|
||||||
this.networkSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
this.networkSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||||
@@ -62,7 +65,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.blocks.unshift(block);
|
this.blocks.unshift(block);
|
||||||
this.blocks = this.blocks.slice(0, 8);
|
this.blocks = this.blocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
||||||
|
|
||||||
if (this.blocksFilled && !this.tabHidden) {
|
if (this.blocksFilled && !this.tabHidden) {
|
||||||
block.stage = block.matchRate >= 66 ? 1 : 2;
|
block.stage = block.matchRate >= 66 ? 1 : 2;
|
||||||
@@ -98,28 +101,6 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
this.moveArrowToPosition(false);
|
this.moveArrowToPosition(false);
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.stateService.keyNavigation$.subscribe((event) => {
|
|
||||||
if (!this.markHeight) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'ArrowRight') {
|
|
||||||
const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight);
|
|
||||||
if (this.blocks[blockindex + 1]) {
|
|
||||||
this.router.navigate([(this.network ? '/' + this.network : '') + '/block/',
|
|
||||||
this.blocks[blockindex + 1].id], { state: { data: { block: this.blocks[blockindex + 1] } } });
|
|
||||||
}
|
|
||||||
} else if (event.key === 'ArrowLeft') {
|
|
||||||
const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight);
|
|
||||||
if (blockindex === 0) {
|
|
||||||
this.router.navigate([(this.network ? '/' + this.network : '') + '/mempool-block/', '0']);
|
|
||||||
} else {
|
|
||||||
this.router.navigate([(this.network ? '/' + this.network : '') + '/block/',
|
|
||||||
this.blocks[blockindex - 1].id], { state: { data: { block: this.blocks[blockindex - 1] }}});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@@ -165,7 +146,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStyleForBlock(block: Block) {
|
getStyleForBlock(block: Block) {
|
||||||
const greenBackgroundHeight = 100 - (block.weight / 4000000) * 100;
|
const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100;
|
||||||
let addLeft = 0;
|
let addLeft = 0;
|
||||||
|
|
||||||
if (block.stage === 1) {
|
if (block.stage === 1) {
|
||||||
@@ -200,7 +181,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
mountEmptyBlocks() {
|
mountEmptyBlocks() {
|
||||||
const emptyBlocks = [];
|
const emptyBlocks = [];
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < this.stateService.env.KEEP_BLOCKS_AMOUNT; i++) {
|
||||||
emptyBlocks.push({
|
emptyBlocks.push({
|
||||||
id: '',
|
id: '',
|
||||||
height: 0,
|
height: 0,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="text-center" class="blockchain-wrapper">
|
<div class="text-center" class="blockchain-wrapper">
|
||||||
<div class="position-container">
|
<div class="position-container {{ network }}">
|
||||||
<span>
|
<span>
|
||||||
<app-mempool-blocks></app-mempool-blocks>
|
<app-mempool-blocks></app-mempool-blocks>
|
||||||
<app-blockchain-blocks></app-blockchain-blocks>
|
<app-blockchain-blocks></app-blockchain-blocks>
|
||||||
|
|||||||
@@ -26,10 +26,17 @@
|
|||||||
top: 75px;
|
top: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.position-container.liquid {
|
||||||
|
left: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.position-container {
|
.position-container {
|
||||||
left: 95%;
|
left: 95%;
|
||||||
}
|
}
|
||||||
|
.position-container.liquid {
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
.position-container.loading {
|
.position-container.loading {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||||
|
import { StateService } from 'src/app/services/state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-blockchain',
|
selector: 'app-blockchain',
|
||||||
templateUrl: './blockchain.component.html',
|
templateUrl: './blockchain.component.html',
|
||||||
styleUrls: ['./blockchain.component.scss'],
|
styleUrls: ['./blockchain.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class BlockchainComponent {
|
export class BlockchainComponent implements OnInit {
|
||||||
|
network: string;
|
||||||
|
|
||||||
constructor() { }
|
constructor(
|
||||||
|
private stateService: StateService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.network = this.stateService.network;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export class FooterComponent implements OnInit {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
size: size,
|
size: size,
|
||||||
blocks: Math.ceil(vsize / 1000000)
|
blocks: Math.ceil(vsize / this.stateService.blockVSize)
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ export class LanguageSelectorComponent implements OnInit {
|
|||||||
try {
|
try {
|
||||||
document.cookie = `lang=${language}; expires=Thu, 18 Dec 2050 12:00:00 UTC; path=/`;
|
document.cookie = `lang=${language}; expires=Thu, 18 Dec 2050 12:00:00 UTC; path=/`;
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
this.document.location.href = `/${language}/${this.stateService.network}`;
|
|
||||||
|
if (this.stateService.env.BASE_MODULE === 'mempool') {
|
||||||
|
this.document.location.href = `/${language}/${this.stateService.network}`;
|
||||||
|
} else {
|
||||||
|
this.document.location.href = `/${language}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<td class="d-none d-lg-block">{{ block.tx_count | number }}</td>
|
<td class="d-none d-lg-block">{{ block.tx_count | number }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / 4000000)*100 + '%' }"></div>
|
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div>
|
||||||
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export class LatestBlocksComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private electrsApiService: ElectrsApiService,
|
private electrsApiService: ElectrsApiService,
|
||||||
private stateService: StateService,
|
public stateService: StateService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<header>
|
||||||
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||||
|
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
|
||||||
|
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||||
|
<img src="./resources/liquid/liquid-network-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
|
||||||
|
<div class="connection-badge">
|
||||||
|
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
|
||||||
|
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div ngbDropdown (window:resize)="onResize($event)" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED">
|
||||||
|
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||||
|
<img src="./resources/liquid-logo.png" style="width: 25px; height: 25px;" class="mr-1">
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||||
|
<a href="https://mempool.space" ngbDropdownItem class="mainnet"><img src="./resources/bitcoin-logo.png" style="width: 30px;" class="mr-1"> Mainnet</a>
|
||||||
|
<a href="https://mempool.space/signet" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><img src="./resources/signet-logo.png" style="width: 30px;" class="mr-1"> Signet</a>
|
||||||
|
<a href="https://mempool.space/testnet" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</a>
|
||||||
|
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
||||||
|
<a href="https://bisq.markets" ngbDropdownItem class="mainnet"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</a>
|
||||||
|
<button ngbDropdownItem class="liquid active" routerLink="/"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1"> Liquid</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-collapse" id="navbarCollapse">
|
||||||
|
<ul class="navbar-nav liquid">
|
||||||
|
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
||||||
|
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" routerLinkActive="active">
|
||||||
|
<a class="nav-link" [routerLink]="['/blocks' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cubes']" [fixedWidth]="true" i18n-title="master-page.blocks" title="Blocks"></fa-icon></a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" routerLinkActive="active">
|
||||||
|
<a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'chart-area']" [fixedWidth]="true" i18n-title="master-page.graphs" title="Graphs"></fa-icon></a>
|
||||||
|
</li>
|
||||||
|
<!--
|
||||||
|
<li class="nav-item d-none d-lg-block" routerLinkActive="active">
|
||||||
|
<a class="nav-link" [routerLink]="['/tv' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon></a>
|
||||||
|
</li>
|
||||||
|
-->
|
||||||
|
<li class="nav-item" routerLinkActive="active">
|
||||||
|
<a class="nav-link" [routerLink]="['/assets']" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a>
|
||||||
|
</li>
|
||||||
|
<li [hidden]="isMobile" class="nav-item mr-2" routerLinkActive="active">
|
||||||
|
<a class="nav-link" [routerLink]="['/api' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cogs']" [fixedWidth]="true" i18n-title="master-page.api" title="API"></fa-icon></a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" routerLinkActive="active">
|
||||||
|
<a class="nav-link" [routerLink]="['/about']" (click)="collapse()"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" i18n-title="master-page.about" title="About"></fa-icon></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<app-search-form class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
|
||||||
|
<br>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
li.nav-item.active {
|
||||||
|
background-color: #653b9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
fa-icon {
|
||||||
|
font-size: 1.66em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
z-index: 100;
|
||||||
|
min-height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.nav-item {
|
||||||
|
margin: auto 10px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.navbar {
|
||||||
|
padding: 0rem 2rem;
|
||||||
|
}
|
||||||
|
fa-icon {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
.dropdown-container {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
li.nav-item {
|
||||||
|
margin: auto 0px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav {
|
||||||
|
background: #212121;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0px 0px 15px 0px #000;
|
||||||
|
flex-direction: row;
|
||||||
|
left: 0;
|
||||||
|
justify-content: center;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
font-size: 0.8em;
|
||||||
|
@media (min-width: 375px) {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.navbar-collapse {
|
||||||
|
flex-basis: auto;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.navbar-collapse {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.navbar-brand {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
box-shadow: 0px 0px 15px 0px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 13px;
|
||||||
|
left: 0px;
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainnet.active {
|
||||||
|
background-color: #653b9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liquid.active {
|
||||||
|
background-color: #116761;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testnet.active {
|
||||||
|
background-color: #1d486f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signet.active {
|
||||||
|
background-color: #6f1d5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
border-top: 1px solid #121420;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
vertical-align: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.search-form-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navbar-dark .navbar-nav .nav-link {
|
||||||
|
color: #f1f1f1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Env, StateService } from '../../services/state.service';
|
||||||
|
import { Observable} from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-liquid-master-page',
|
||||||
|
templateUrl: './liquid-master-page.component.html',
|
||||||
|
styleUrls: ['./liquid-master-page.component.scss'],
|
||||||
|
})
|
||||||
|
export class LiquidMasterPageComponent implements OnInit {
|
||||||
|
env: Env;
|
||||||
|
connectionState$: Observable<number>;
|
||||||
|
navCollapsed = false;
|
||||||
|
isMobile = window.innerWidth <= 767.98;
|
||||||
|
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private stateService: StateService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.env = this.stateService.env;
|
||||||
|
this.connectionState$ = this.stateService.connectionState$;
|
||||||
|
}
|
||||||
|
|
||||||
|
collapse(): void {
|
||||||
|
this.navCollapsed = !this.navCollapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize(event: any) {
|
||||||
|
this.isMobile = window.innerWidth <= 767.98;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,17 +11,19 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div ngbDropdown display="dynamic" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED">
|
<div (window:resize)="onResize($event)" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED">
|
||||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||||
<img src="./resources/{{ network.val === '' ? 'bitcoin' : network.val }}-logo.png" style="width: 25px; height: 25px;" class="mr-1">
|
<img src="./resources/{{ network.val === '' ? 'bitcoin' : network.val }}-logo.png" style="width: 25px; height: 25px;" class="mr-1">
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu>
|
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||||
<button ngbDropdownItem class="mainnet" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 30px;" class="mr-1"> Mainnet</button>
|
<button ngbDropdownItem class="mainnet" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 30px;" class="mr-1"> Mainnet</button>
|
||||||
<button ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><img src="./resources/signet-logo.png" style="width: 30px;" class="mr-1"> Signet</button>
|
<button ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><img src="./resources/signet-logo.png" style="width: 30px;" class="mr-1"> Signet</button>
|
||||||
<button ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</button>
|
<button ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</button>
|
||||||
<h6 *ngIf="env.LIQUID_ENABLED || env.BISQ_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
<h6 *ngIf="env.LIQUID_ENABLED || env.BISQ_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
||||||
<button ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq" [class.active]="network.val === 'bisq'" routerLink="/bisq"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</button>
|
<a href="https://bisq.markets" ngbDropdownItem *ngIf="env.BISQ_ENABLED && env.OFFICIAL_MEMPOOL_SPACE" class="bisq"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</a>
|
||||||
<button ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'" routerLink="/liquid"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1"> Liquid</button>
|
<button ngbDropdownItem *ngIf="env.BISQ_ENABLED && !env.OFFICIAL_MEMPOOL_SPACE" class="bisq" [class.active]="network.val === 'bisq'" routerLink="/bisq"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</button>
|
||||||
|
<a href="https://liquid.network" ngbDropdownItem *ngIf="env.LIQUID_ENABLED && env.OFFICIAL_MEMPOOL_SPACE" class="liquid" [class.active]="network.val === 'liquid'"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1"> Liquid</a>
|
||||||
|
<button ngbDropdownItem *ngIf="env.LIQUID_ENABLED && !env.OFFICIAL_MEMPOOL_SPACE" class="liquid" [class.active]="network.val === 'liquid'" routerLink="/liquid"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1"> Liquid</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -28,4 +28,8 @@ export class MasterPageComponent implements OnInit {
|
|||||||
collapse(): void {
|
collapse(): void {
|
||||||
this.navCollapsed = !this.navCollapsed;
|
this.navCollapsed = !this.navCollapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onResize(event: any) {
|
||||||
|
this.isMobile = window.innerWidth <= 767.98;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<td i18n="mempool-block.size">Size</td>
|
<td i18n="mempool-block.size">Size</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
<div class="progress-bar progress-mempool {{ (network$ | async) }}" role="progressbar" [ngStyle]="{'width': (mempoolBlock.blockVSize / 1000000) * 100 + '%' }"></div>
|
<div class="progress-bar progress-mempool {{ (network$ | async) }}" role="progressbar" [ngStyle]="{'width': (mempoolBlock.blockVSize / stateService.blockVSize) * 100 + '%' }"></div>
|
||||||
<div class="progress-text" [innerHTML]="mempoolBlock.blockSize | bytes: 2"></div>
|
<div class="progress-text" [innerHTML]="mempoolBlock.blockSize | bytes: 2"></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private stateService: StateService,
|
public stateService: StateService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
) { }
|
) { }
|
||||||
@@ -66,7 +66,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getOrdinal(mempoolBlock: MempoolBlock): string {
|
getOrdinal(mempoolBlock: MempoolBlock): string {
|
||||||
const blocksInBlock = Math.ceil(mempoolBlock.blockVSize / 1000000);
|
const blocksInBlock = Math.ceil(mempoolBlock.blockVSize / this.stateService.blockVSize);
|
||||||
if (this.mempoolBlockIndex === 0) {
|
if (this.mempoolBlockIndex === 0) {
|
||||||
return $localize`:@@mempool-block.next.block:Next block`;
|
return $localize`:@@mempool-block.next.block:Next block`;
|
||||||
} else if (this.mempoolBlockIndex === this.stateService.env.KEEP_BLOCKS_AMOUNT - 1 && blocksInBlock > 1) {
|
} else if (this.mempoolBlockIndex === this.stateService.env.KEEP_BLOCKS_AMOUNT - 1 && blocksInBlock > 1) {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
<a [routerLink]="['/mempool-block/' | relativeUrl, i]" class="blockLink"> </a>
|
<a [routerLink]="['/mempool-block/' | relativeUrl, i]" class="blockLink"> </a>
|
||||||
<div class="block-body">
|
<div class="block-body">
|
||||||
<div class="fees">
|
<div class="fees">
|
||||||
~{{ projectedBlock.medianFee | number:'1.0-0' }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
~{{ projectedBlock.medianFee | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="fee-span">
|
<div class="fee-span">
|
||||||
{{ projectedBlock.feeRange[0] | number:'1.0-0' }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:'1.0-0' }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
{{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="block-size" [innerHTML]="projectedBlock.blockSize | bytes: 2">‎</div>
|
<div class="block-size" [innerHTML]="projectedBlock.blockSize | bytes: 2">‎</div>
|
||||||
<div class="transaction-count">
|
<div class="transaction-count">
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<div class="time-difference" *ngIf="projectedBlock.blockVSize <= 1000000; else mergedBlock">
|
<div class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock">
|
||||||
<ng-template [ngIf]="network === 'liquid'" [ngIfElse]="timeDiffMainnet">
|
<ng-template [ngIf]="network === 'liquid'" [ngIfElse]="timeDiffMainnet">
|
||||||
<app-time-until [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time-until>
|
<app-time-until [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time-until>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ng-template #mergedBlock>
|
<ng-template #mergedBlock>
|
||||||
<div class="time-difference">
|
<div class="time-difference">
|
||||||
<b>(<ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: projectedBlock.blockVSize / 1000000 | ceil }"></ng-container>)</b>
|
<b>(<ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: projectedBlock.blockVSize / stateService.blockVSize | ceil }"></ng-container>)</b>
|
||||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
blockPadding = 30;
|
blockPadding = 30;
|
||||||
arrowVisible = false;
|
arrowVisible = false;
|
||||||
tabHidden = false;
|
tabHidden = false;
|
||||||
|
feeRounding = '1.0-0';
|
||||||
|
|
||||||
rightPosition = 0;
|
rightPosition = 0;
|
||||||
transition = '2s';
|
transition = '2s';
|
||||||
@@ -47,11 +48,14 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private stateService: StateService,
|
public stateService: StateService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
if (this.stateService.network === 'liquid') {
|
||||||
|
this.feeRounding = '1.0-1';
|
||||||
|
}
|
||||||
this.mempoolEmptyBlocks.forEach((b) => {
|
this.mempoolEmptyBlocks.forEach((b) => {
|
||||||
this.mempoolEmptyBlockStyles.push(this.getStyleForMempoolEmptyBlock(b.index));
|
this.mempoolEmptyBlockStyles.push(this.getStyleForMempoolEmptyBlock(b.index));
|
||||||
});
|
});
|
||||||
@@ -153,7 +157,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
this.router.navigate([(this.network ? '/' + this.network : '') + '/mempool-block/', this.markIndex - 1]);
|
this.router.navigate([(this.network ? '/' + this.network : '') + '/mempool-block/', this.markIndex - 1]);
|
||||||
} else {
|
} else {
|
||||||
this.stateService.blocks$
|
this.stateService.blocks$
|
||||||
.pipe(take(8))
|
.pipe(take(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT))
|
||||||
.subscribe(([block]) => {
|
.subscribe(([block]) => {
|
||||||
if (this.stateService.latestBlockHeight === block.height) {
|
if (this.stateService.latestBlockHeight === block.height) {
|
||||||
this.router.navigate([(this.network ? '/' + this.network : '') + '/block/', block.id], { state: { data: { block } }});
|
this.router.navigate([(this.network ? '/' + this.network : '') + '/block/', block.id], { state: { data: { block } }});
|
||||||
@@ -211,7 +215,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStyleForMempoolBlock(mempoolBlock: MempoolBlock, index: number) {
|
getStyleForMempoolBlock(mempoolBlock: MempoolBlock, index: number) {
|
||||||
const emptyBackgroundSpacePercentage = Math.max(100 - mempoolBlock.blockVSize / 1000000 * 100, 0);
|
const emptyBackgroundSpacePercentage = Math.max(100 - mempoolBlock.blockVSize / this.stateService.blockVSize * 100, 0);
|
||||||
const usedBlockSpace = 100 - emptyBackgroundSpacePercentage;
|
const usedBlockSpace = 100 - emptyBackgroundSpacePercentage;
|
||||||
const backgroundGradients = [`repeating-linear-gradient(to right, #554b45, #554b45 ${emptyBackgroundSpacePercentage}%`];
|
const backgroundGradients = [`repeating-linear-gradient(to right, #554b45, #554b45 ${emptyBackgroundSpacePercentage}%`];
|
||||||
const gradientColors = [];
|
const gradientColors = [];
|
||||||
@@ -278,7 +282,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
const chunkPositionOffset = blockLocation * feeRangeChunkSize;
|
const chunkPositionOffset = blockLocation * feeRangeChunkSize;
|
||||||
const feePosition = feeRangeChunkSize * feeRangeIndex + chunkPositionOffset;
|
const feePosition = feeRangeChunkSize * feeRangeIndex + chunkPositionOffset;
|
||||||
|
|
||||||
const blockedFilledPercentage = (block.blockVSize > 1000000 ? 1000000 : block.blockVSize) / 1000000;
|
const blockedFilledPercentage = (block.blockVSize > this.stateService.blockVSize ? this.stateService.blockVSize : block.blockVSize) / this.stateService.blockVSize;
|
||||||
const arrowRightPosition = txInBlockIndex * (this.blockWidth + this.blockPadding)
|
const arrowRightPosition = txInBlockIndex * (this.blockWidth + this.blockPadding)
|
||||||
+ ((1 - feePosition) * blockedFilledPercentage * this.blockWidth);
|
+ ((1 - feePosition) * blockedFilledPercentage * this.blockWidth);
|
||||||
|
|
||||||
@@ -291,8 +295,8 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
mountEmptyBlocks() {
|
mountEmptyBlocks() {
|
||||||
const emptyBlocks = [];
|
const emptyBlocks = [];
|
||||||
const numberOfBlocks = 8;
|
const numberOfBlocks = this.stateService.env.MEMPOOL_BLOCKS_AMOUNT;
|
||||||
for (let i = 0; i <= numberOfBlocks; i++) {
|
for (let i = 0; i < numberOfBlocks; i++) {
|
||||||
emptyBlocks.push({
|
emptyBlocks.push({
|
||||||
blockSize: 0,
|
blockSize: 0,
|
||||||
blockVSize: 0,
|
blockVSize: 0,
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
|||||||
labelInterpolationFnc: (value: number): any => this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true),
|
labelInterpolationFnc: (value: number): any => this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true),
|
||||||
offset: this.showLegend ? 160 : 60,
|
offset: this.showLegend ? 160 : 60,
|
||||||
},
|
},
|
||||||
plugins: this.inverted ? [Chartist.plugins.ctTargetLine({ value: 1000000 })] : []
|
plugins: this.inverted ? [Chartist.plugins.ctTargetLine({ value: this.stateService.blockVSize })] : []
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.showLegend) {
|
if (this.showLegend) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export class SearchFormComponent implements OnInit {
|
|||||||
searchForm: FormGroup;
|
searchForm: FormGroup;
|
||||||
@Output() searchTriggered = new EventEmitter();
|
@Output() searchTriggered = new EventEmitter();
|
||||||
|
|
||||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[bB]?[a-z]{2,5}1[ac-hj-np-z02-9]{8,87})$/;
|
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/;
|
||||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||||
regexTransaction = /^[a-fA-F0-9]{64}$/;
|
regexTransaction = /^[a-fA-F0-9]{64}$/;
|
||||||
regexBlockheight = /^[0-9]+$/;
|
regexBlockheight = /^[0-9]+$/;
|
||||||
@@ -119,7 +119,7 @@ export class SearchFormComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
navigate(url: string, searchText: string) {
|
navigate(url: string, searchText: string) {
|
||||||
this.router.navigate([(this.network ? '/' + this.network : '') + url, searchText]);
|
this.router.navigate([(this.network && this.stateService.env.BASE_MODULE === 'mempool' ? '/' + this.network : '') + url, searchText]);
|
||||||
this.searchTriggered.emit();
|
this.searchTriggered.emit();
|
||||||
this.searchForm.setValue({
|
this.searchForm.setValue({
|
||||||
searchText: '',
|
searchText: '',
|
||||||
|
|||||||
139
frontend/src/app/components/transaction/liquid-ublinding.ts
Normal file
139
frontend/src/app/components/transaction/liquid-ublinding.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { Transaction } from '../../interfaces/electrs.interface';
|
||||||
|
|
||||||
|
// Parse the blinders data from a string encoded as a comma separated list, in the following format:
|
||||||
|
// <value_in_satoshis>,<asset_tag_hex>,<amount_blinder_hex>,<asset_blinder_hex>
|
||||||
|
// This can be repeated with a comma separator to specify blinders for multiple outputs.
|
||||||
|
export class LiquidUnblinding {
|
||||||
|
commitments: Map<any, any>;
|
||||||
|
|
||||||
|
parseBlinders(str: string) {
|
||||||
|
const parts = str.split(',');
|
||||||
|
const blinders = [];
|
||||||
|
while (parts.length) {
|
||||||
|
blinders.push({
|
||||||
|
value: this.verifyNum(parts.shift()),
|
||||||
|
asset: this.verifyHex32(parts.shift()),
|
||||||
|
value_blinder: this.verifyHex32(parts.shift()),
|
||||||
|
asset_blinder: this.verifyHex32(parts.shift()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return blinders;
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyNum(num: string) {
|
||||||
|
if (!+num) {
|
||||||
|
throw new Error('Invalid blinding data (invalid number)');
|
||||||
|
}
|
||||||
|
return +num;
|
||||||
|
}
|
||||||
|
verifyHex32(str: string) {
|
||||||
|
if (!str || !/^[0-9a-f]{64}$/i.test(str)) {
|
||||||
|
throw new Error('Invalid blinding data (invalid hex)');
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
async makeCommitmentMap(blinders: any) {
|
||||||
|
const libwally = await import('./libwally.js');
|
||||||
|
await libwally.load();
|
||||||
|
const commitments = new Map();
|
||||||
|
blinders.forEach(b => {
|
||||||
|
const { asset_commitment, value_commitment } =
|
||||||
|
libwally.generate_commitments(b.value, b.asset, b.value_blinder, b.asset_blinder);
|
||||||
|
|
||||||
|
commitments.set(`${asset_commitment}:${value_commitment}`, {
|
||||||
|
asset: b.asset,
|
||||||
|
value: b.value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return commitments;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the given output, returning an { value, asset } object
|
||||||
|
find(vout: any) {
|
||||||
|
return vout.assetcommitment && vout.valuecommitment &&
|
||||||
|
this.commitments.get(`${vout.assetcommitment}:${vout.valuecommitment}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup all transaction inputs/outputs and attach the unblinded data
|
||||||
|
tryUnblindTx(tx: Transaction) {
|
||||||
|
if (tx) {
|
||||||
|
if (tx._unblinded) { return tx; }
|
||||||
|
let matched = 0;
|
||||||
|
if (tx.vout !== undefined) {
|
||||||
|
tx.vout.forEach(vout => matched += +this.tryUnblindOut(vout));
|
||||||
|
tx.vin.filter(vin => vin.prevout).forEach(vin => matched += +this.tryUnblindOut(vin.prevout));
|
||||||
|
}
|
||||||
|
if (this.commitments !== undefined) {
|
||||||
|
tx._unblinded = { matched, total: this.commitments.size };
|
||||||
|
this.deduceBlinded(tx);
|
||||||
|
if (matched < this.commitments.size) {
|
||||||
|
throw new Error(`Invalid blinding data.`)
|
||||||
|
}
|
||||||
|
tx._deduced = false; // invalidate cache so deduction is attempted again
|
||||||
|
return tx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look the given output and attach the unblinded data
|
||||||
|
tryUnblindOut(vout: any) {
|
||||||
|
const unblinded = this.find(vout);
|
||||||
|
if (unblinded) { Object.assign(vout, unblinded); }
|
||||||
|
return !!unblinded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to deduce the blinded input/output based on the available information
|
||||||
|
deduceBlinded(tx: any) {
|
||||||
|
if (tx._deduced) { return; }
|
||||||
|
tx._deduced = true;
|
||||||
|
|
||||||
|
// Find ins/outs with unknown amounts (blinded ant not revealed via the `#blinded` hash fragment)
|
||||||
|
const unknownIns = tx.vin.filter(vin => vin.prevout && vin.prevout.value == null);
|
||||||
|
const unknownOuts = tx.vout.filter(vout => vout.value == null);
|
||||||
|
|
||||||
|
// If the transaction has a single unknown input/output, we can deduce its asset/amount
|
||||||
|
// based on the other known inputs/outputs.
|
||||||
|
if (unknownIns.length + unknownOuts.length === 1) {
|
||||||
|
|
||||||
|
// Keep a per-asset tally of all known input amounts, minus all known output amounts
|
||||||
|
const totals = new Map();
|
||||||
|
tx.vin.filter(vin => vin.prevout && vin.prevout.value != null)
|
||||||
|
.forEach(({ prevout }) =>
|
||||||
|
totals.set(prevout.asset, (totals.get(prevout.asset) || 0) + prevout.value));
|
||||||
|
tx.vout.filter(vout => vout.value != null)
|
||||||
|
.forEach(vout =>
|
||||||
|
totals.set(vout.asset, (totals.get(vout.asset) || 0) - vout.value));
|
||||||
|
|
||||||
|
// There should only be a single asset where the inputs and outputs amounts mismatch,
|
||||||
|
// which is the asset of the blinded input/output
|
||||||
|
const remainder = Array.from(totals.entries()).filter(([ asset, value ]) => value !== 0);
|
||||||
|
if (remainder.length !== 1) { throw new Error('unexpected remainder while deducing blinded tx'); }
|
||||||
|
const [ blindedAsset, blindedValue ] = remainder[0];
|
||||||
|
|
||||||
|
// A positive remainder (when known in > known out) is the asset/amount of the unknown blinded output,
|
||||||
|
// a negative one is the input.
|
||||||
|
if (blindedValue > 0) {
|
||||||
|
if (!unknownOuts.length) { throw new Error('expected unknown output'); }
|
||||||
|
unknownOuts[0].asset = blindedAsset;
|
||||||
|
unknownOuts[0].value = blindedValue;
|
||||||
|
} else {
|
||||||
|
if (!unknownIns.length) { throw new Error('expected unknown input'); }
|
||||||
|
unknownIns[0].prevout.asset = blindedAsset;
|
||||||
|
unknownIns[0].prevout.value = blindedValue * -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkUnblindedTx(tx: Transaction) {
|
||||||
|
const windowLocationHash = window.location.hash.substring('#blinded='.length);
|
||||||
|
if (windowLocationHash.length > 0) {
|
||||||
|
const blinders = this.parseBlinders(windowLocationHash);
|
||||||
|
if (blinders) {
|
||||||
|
this.commitments = await this.makeCommitmentMap(blinders);
|
||||||
|
return this.tryUnblindTx(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight / 4 | vbytes: 2"></td>
|
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight / 4 | vbytes: 2"></td>
|
||||||
<td>{{ cpfpInfo.bestDescendant.fee / (cpfpInfo.bestDescendant.weight / 4) | number : '1.1-1' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
<td>{{ cpfpInfo.bestDescendant.fee / (cpfpInfo.bestDescendant.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||||
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
|
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
|
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
|
||||||
<td>{{ cpfpTx.fee / (cpfpTx.weight / 4) | number : '1.1-1' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
<td>{{ cpfpTx.fee / (cpfpTx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||||
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) < roundToOneDecimal(tx)" class="arrow-red" [icon]="['fas', 'angle-double-down']" [fixedWidth]="true"></fa-icon></td>
|
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) < roundToOneDecimal(tx)" class="arrow-red" [icon]="['fas', 'angle-double-down']" [fixedWidth]="true"></fa-icon></td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -331,7 +331,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||||
<td>
|
<td>
|
||||||
{{ tx.feePerVsize | number : '1.1-1' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||||
<ng-template [ngIf]="tx.status.confirmed">
|
<ng-template [ngIf]="tx.status.confirmed">
|
||||||
|
|
||||||
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
|
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
|
||||||
@@ -342,7 +342,7 @@
|
|||||||
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="effective-fee-container">
|
<div class="effective-fee-container">
|
||||||
{{ tx.effectiveFeePerVsize | number : '1.1-1' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
{{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||||
<ng-template [ngIf]="tx.status.confirmed">
|
<ng-template [ngIf]="tx.status.confirmed">
|
||||||
<app-tx-fee-rating class="d-none d-lg-inline ml-2" *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
|
<app-tx-fee-rating class="d-none d-lg-inline ml-2" *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import {
|
|||||||
map
|
map
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
import { Transaction, Block } from '../../interfaces/electrs.interface';
|
import { Transaction, Block } from '../../interfaces/electrs.interface';
|
||||||
import { of, merge, Subscription, Observable, Subject, timer, combineLatest, } from 'rxjs';
|
import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
import { AudioService } from 'src/app/services/audio.service';
|
import { AudioService } from 'src/app/services/audio.service';
|
||||||
import { ApiService } from 'src/app/services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
import { CpfpInfo } from 'src/app/interfaces/node-api.interface';
|
import { CpfpInfo } from 'src/app/interfaces/node-api.interface';
|
||||||
|
import { LiquidUnblinding } from './liquid-ublinding';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-transaction',
|
selector: 'app-transaction',
|
||||||
@@ -40,9 +41,9 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
|||||||
cpfpInfo: CpfpInfo | null;
|
cpfpInfo: CpfpInfo | null;
|
||||||
showCpfpDetails = false;
|
showCpfpDetails = false;
|
||||||
fetchCpfp$ = new Subject<string>();
|
fetchCpfp$ = new Subject<string>();
|
||||||
commitments: Map<any, any>;
|
|
||||||
now = new Date().getTime();
|
now = new Date().getTime();
|
||||||
timeAvg$: Observable<number>;
|
timeAvg$: Observable<number>;
|
||||||
|
liquidUnblinding = new LiquidUnblinding();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -123,10 +124,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.subscription = this.route.paramMap
|
this.subscription = this.route.paramMap
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(async (params: ParamMap) => {
|
switchMap((params: ParamMap) => {
|
||||||
this.txId = params.get('id') || '';
|
this.txId = params.get('id') || '';
|
||||||
|
|
||||||
await this.checkUnblindedTx();
|
|
||||||
this.seoService.setTitle(
|
this.seoService.setTitle(
|
||||||
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
||||||
);
|
);
|
||||||
@@ -155,13 +154,25 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
|||||||
transactionObservable$,
|
transactionObservable$,
|
||||||
this.stateService.mempoolTransactions$
|
this.stateService.mempoolTransactions$
|
||||||
);
|
);
|
||||||
|
}),
|
||||||
|
switchMap((tx) => {
|
||||||
|
if (this.network === 'liquid') {
|
||||||
|
return from(this.liquidUnblinding.checkUnblindedTx(tx))
|
||||||
|
.pipe(
|
||||||
|
catchError((error) => {
|
||||||
|
this.errorUnblinded = error;
|
||||||
|
return of(tx);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return of(tx);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe(
|
.subscribe((tx: Transaction) => {
|
||||||
async (tx: Transaction) => {
|
|
||||||
if (!tx) {
|
if (!tx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tx = tx;
|
this.tx = tx;
|
||||||
if (tx.fee === undefined) {
|
if (tx.fee === undefined) {
|
||||||
this.tx.fee = 0;
|
this.tx.fee = 0;
|
||||||
@@ -199,7 +210,6 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
|||||||
this.fetchCpfp$.next(this.tx.txid);
|
this.fetchCpfp$.next(this.tx.txid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.checkUnblindedTx();
|
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
@@ -294,145 +304,4 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
|||||||
this.fetchCpfpSubscription.unsubscribe();
|
this.fetchCpfpSubscription.unsubscribe();
|
||||||
this.leaveTransaction();
|
this.leaveTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the blinders data from a string encoded as a comma separated list, in the following format:
|
|
||||||
// <value_in_satoshis>,<asset_tag_hex>,<amount_blinder_hex>,<asset_blinder_hex>
|
|
||||||
// This can be repeated with a comma separator to specify blinders for multiple outputs.
|
|
||||||
|
|
||||||
parseBlinders(str: string) {
|
|
||||||
const parts = str.split(',');
|
|
||||||
const blinders = [];
|
|
||||||
while (parts.length) {
|
|
||||||
blinders.push({
|
|
||||||
value: this.verifyNum(parts.shift()),
|
|
||||||
asset: this.verifyHex32(parts.shift()),
|
|
||||||
value_blinder: this.verifyHex32(parts.shift()),
|
|
||||||
asset_blinder: this.verifyHex32(parts.shift()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return blinders;
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyNum(num: string) {
|
|
||||||
if (!+num) {
|
|
||||||
throw new Error('Invalid blinding data (invalid number)');
|
|
||||||
}
|
|
||||||
return +num;
|
|
||||||
}
|
|
||||||
verifyHex32(str: string) {
|
|
||||||
if (!str || !/^[0-9a-f]{64}$/i.test(str)) {
|
|
||||||
throw new Error('Invalid blinding data (invalid hex)');
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
async makeCommitmentMap(blinders: any) {
|
|
||||||
const libwally = await import('./libwally.js');
|
|
||||||
await libwally.load();
|
|
||||||
const commitments = new Map();
|
|
||||||
blinders.forEach(b => {
|
|
||||||
const { asset_commitment, value_commitment } =
|
|
||||||
libwally.generate_commitments(b.value, b.asset, b.value_blinder, b.asset_blinder);
|
|
||||||
|
|
||||||
commitments.set(`${asset_commitment}:${value_commitment}`, {
|
|
||||||
asset: b.asset,
|
|
||||||
value: b.value,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return commitments;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for the given output, returning an { value, asset } object
|
|
||||||
find(vout: any) {
|
|
||||||
return vout.assetcommitment && vout.valuecommitment &&
|
|
||||||
this.commitments.get(`${vout.assetcommitment}:${vout.valuecommitment}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lookup all transaction inputs/outputs and attach the unblinded data
|
|
||||||
tryUnblindTx(tx: any) {
|
|
||||||
if (tx) {
|
|
||||||
if (tx._unblinded) { return tx._unblinded; }
|
|
||||||
let matched = 0;
|
|
||||||
if (tx.vout !== undefined) {
|
|
||||||
tx.vout.forEach(vout => matched += +this.tryUnblindOut(vout));
|
|
||||||
tx.vin.filter(vin => vin.prevout).forEach(vin => matched += +this.tryUnblindOut(vin.prevout));
|
|
||||||
}
|
|
||||||
if (this.commitments !== undefined) {
|
|
||||||
tx._unblinded = { matched, total: this.commitments.size };
|
|
||||||
this.deduceBlinded(tx);
|
|
||||||
if (matched < this.commitments.size) {
|
|
||||||
this.errorUnblinded = `Error: Invalid blinding data.`;
|
|
||||||
}
|
|
||||||
tx._deduced = false; // invalidate cache so deduction is attempted again
|
|
||||||
return tx._unblinded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look the given output and attach the unblinded data
|
|
||||||
tryUnblindOut(vout: any) {
|
|
||||||
const unblinded = this.find(vout);
|
|
||||||
if (unblinded) { Object.assign(vout, unblinded); }
|
|
||||||
return !!unblinded;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to deduce the blinded input/output based on the available information
|
|
||||||
deduceBlinded(tx: any) {
|
|
||||||
if (tx._deduced) { return; }
|
|
||||||
tx._deduced = true;
|
|
||||||
|
|
||||||
// Find ins/outs with unknown amounts (blinded ant not revealed via the `#blinded` hash fragment)
|
|
||||||
const unknownIns = tx.vin.filter(vin => vin.prevout && vin.prevout.value == null);
|
|
||||||
const unknownOuts = tx.vout.filter(vout => vout.value == null);
|
|
||||||
|
|
||||||
// If the transaction has a single unknown input/output, we can deduce its asset/amount
|
|
||||||
// based on the other known inputs/outputs.
|
|
||||||
if (unknownIns.length + unknownOuts.length === 1) {
|
|
||||||
|
|
||||||
// Keep a per-asset tally of all known input amounts, minus all known output amounts
|
|
||||||
const totals = new Map();
|
|
||||||
tx.vin.filter(vin => vin.prevout && vin.prevout.value != null)
|
|
||||||
.forEach(({ prevout }) =>
|
|
||||||
totals.set(prevout.asset, (totals.get(prevout.asset) || 0) + prevout.value));
|
|
||||||
tx.vout.filter(vout => vout.value != null)
|
|
||||||
.forEach(vout =>
|
|
||||||
totals.set(vout.asset, (totals.get(vout.asset) || 0) - vout.value));
|
|
||||||
|
|
||||||
// There should only be a single asset where the inputs and outputs amounts mismatch,
|
|
||||||
// which is the asset of the blinded input/output
|
|
||||||
const remainder = Array.from(totals.entries()).filter(([ asset, value ]) => value !== 0);
|
|
||||||
if (remainder.length !== 1) { throw new Error('unexpected remainder while deducing blinded tx'); }
|
|
||||||
const [ blindedAsset, blindedValue ] = remainder[0];
|
|
||||||
|
|
||||||
// A positive remainder (when known in > known out) is the asset/amount of the unknown blinded output,
|
|
||||||
// a negative one is the input.
|
|
||||||
if (blindedValue > 0) {
|
|
||||||
if (!unknownOuts.length) { throw new Error('expected unknown output'); }
|
|
||||||
unknownOuts[0].asset = blindedAsset;
|
|
||||||
unknownOuts[0].value = blindedValue;
|
|
||||||
} else {
|
|
||||||
if (!unknownIns.length) { throw new Error('expected unknown input'); }
|
|
||||||
unknownIns[0].prevout.asset = blindedAsset;
|
|
||||||
unknownIns[0].prevout.value = blindedValue * -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkUnblindedTx() {
|
|
||||||
try {
|
|
||||||
if (this.network === 'liquid') {
|
|
||||||
const windowLocationHash = window.location.hash.substring('#blinded='.length);
|
|
||||||
if (windowLocationHash.length > 0) {
|
|
||||||
|
|
||||||
const blinders = this.parseBlinders(windowLocationHash);
|
|
||||||
if (blinders) {
|
|
||||||
this.commitments = await this.makeCommitmentMap(blinders);
|
|
||||||
this.tryUnblindTx(this.tx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.errorUnblinded = error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<table class="table table-borderless smaller-text table-sm" id="table-tx-vin">
|
<table class="table table-borderless smaller-text table-sm" id="table-tx-vin">
|
||||||
<tbody>
|
<tbody>
|
||||||
<ng-template ngFor let-vin [ngForOf]="tx['@vinLimit'] ? tx.vin.slice(0, 10) : tx.vin" [ngForTrackBy]="trackByIndexFn">
|
<ng-template ngFor let-vin [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length>12)?tx.vin.slice(0, 10): tx.vin.slice(0, 12)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
|
||||||
<tr [ngClass]="assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded ? 'assetBox' : ''">
|
<tr [ngClass]="assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded ? 'assetBox' : ''">
|
||||||
<td class="arrow-td">
|
<td class="arrow-td">
|
||||||
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
|
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<tr *ngIf="tx.vin.length > 10 && tx['@vinLimit']">
|
<tr *ngIf="tx.vin.length > 12 && tx['@vinLimit']">
|
||||||
<td colspan="3" class="text-center">
|
<td colspan="3" class="text-center">
|
||||||
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@vinLimit'] = false;"><span i18n="transactions-list.load-all">Load all</span> ({{ tx.vin.length - 10 }})</button>
|
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@vinLimit'] = false;"><span i18n="transactions-list.load-all">Load all</span> ({{ tx.vin.length - 10 }})</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
<div class="col mobile-bottomcol">
|
<div class="col mobile-bottomcol">
|
||||||
<table class="table table-borderless smaller-text table-sm" id="table-tx-vout">
|
<table class="table table-borderless smaller-text table-sm" id="table-tx-vout">
|
||||||
<tbody>
|
<tbody>
|
||||||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] ? tx.vout.slice(0, 10) : tx.vout" [ngForTrackBy]="trackByIndexFn">
|
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] ?((tx.vout.length>12)?tx.vout.slice(0, 10): tx.vout.slice(0, 12)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
|
||||||
<tr [ngClass]="assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded ? 'assetBox' : ''">
|
<tr [ngClass]="assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded ? 'assetBox' : ''">
|
||||||
<td>
|
<td>
|
||||||
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
||||||
@@ -197,7 +197,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<tr *ngIf="tx.vout.length > 10 && tx['@voutLimit']">
|
<tr *ngIf="tx.vout.length > 12 && tx['@voutLimit']">
|
||||||
<td colspan="3" class="text-center">
|
<td colspan="3" class="text-center">
|
||||||
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="transactions-list.load-all">Load all</span> ({{ tx.vout.length - 10 }})</button>
|
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="transactions-list.load-all">Load all</span> ({{ tx.vout.length - 10 }})</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="float-left mt-2-5" *ngIf="!transactionPage && tx.fee">
|
<div class="float-left mt-2-5" *ngIf="!transactionPage && tx.fee">
|
||||||
{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="d-none d-sm-inline-block"> – {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></span>
|
{{ tx.fee / (tx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="d-none d-sm-inline-block"> – {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<span *ngIf="feeRating === 1" class="badge badge-success" i18n="tx-fee-rating.optimal|TX Fee Rating is Optimal">Optimal</span>
|
<span *ngIf="feeRating === 1" class="badge badge-success" i18n="tx-fee-rating.optimal|TX Fee Rating is Optimal">Optimal</span>
|
||||||
<span *ngIf="feeRating === 2" class="badge badge-warning" placement="bottom" i18n-ngbTooltip="tx-fee-rating.warning-tooltip" ngbTooltip="Only ~{{ medianFeeNeeded | number : '1.1-1' }} sat/vB was needed to get into this block" i18n="tx-fee-rating.overpaid.warning|TX Fee Rating is Warning">Overpaid {{ overpaidTimes }}x</span>
|
<span *ngIf="feeRating === 2" class="badge badge-warning" placement="bottom" i18n-ngbTooltip="tx-fee-rating.warning-tooltip" ngbTooltip="Only ~{{ medianFeeNeeded | feeRounding }} sat/vB was needed to get into this block" i18n="tx-fee-rating.overpaid.warning|TX Fee Rating is Warning">Overpaid {{ overpaidTimes }}x</span>
|
||||||
<span *ngIf="feeRating === 3" class="badge badge-danger" placement="bottom" i18n-ngbTooltip="tx-fee-rating.warning-tooltip" ngbTooltip="Only ~{{ medianFeeNeeded | number : '1.1-1' }} sat/vB was needed to get into this block" i18n="tx-fee-rating.overpaid.warning|TX Fee Rating is Warning">Overpaid {{ overpaidTimes }}x</span>
|
<span *ngIf="feeRating === 3" class="badge badge-danger" placement="bottom" i18n-ngbTooltip="tx-fee-rating.warning-tooltip" ngbTooltip="Only ~{{ medianFeeNeeded | feeRounding }} sat/vB was needed to get into this block" i18n="tx-fee-rating.overpaid.warning|TX Fee Rating is Warning">Overpaid {{ overpaidTimes }}x</span>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.medianFeeNeeded = block.medianFee;
|
this.medianFeeNeeded = block.medianFee;
|
||||||
|
|
||||||
// Block not filled
|
// Block not filled
|
||||||
if (block.weight < 4000000 * 0.95) {
|
if (block.weight < this.stateService.env.BLOCK_WEIGHT_UNITS * 0.95) {
|
||||||
this.medianFeeNeeded = 1;
|
this.medianFeeNeeded = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="container-xl dashboard-container">
|
<div class="container-xl dashboard-container">
|
||||||
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
|
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
|
||||||
<ng-template [ngIf]="collapseLevel === 'three'" [ngIfElse]="expanded">
|
<ng-template [ngIf]="collapseLevel === 'three'" [ngIfElse]="expanded">
|
||||||
<div class="col card-wrapper">
|
<div class="col card-wrapper" *ngIf="(network$ | async) !== 'liquid'">
|
||||||
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" *ngIf="(network$ | async) !== 'liquid'; else emptyBlock">
|
<div class="col" *ngIf="(network$ | async) !== 'liquid'">
|
||||||
<ng-container *ngTemplateOutlet="difficultyEpoch"></ng-container>
|
<ng-container *ngTemplateOutlet="difficultyEpoch"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@@ -27,10 +27,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #expanded>
|
<ng-template #expanded>
|
||||||
<div class="col card-wrapper">
|
<div class="col card-wrapper" *ngIf="(network$ | async) !== 'liquid'">
|
||||||
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -38,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" *ngIf="(network$ | async) !== 'liquid'; else emptyBlock">
|
<div class="col" *ngIf="(network$ | async) !== 'liquid'">
|
||||||
<ng-container *ngTemplateOutlet="difficultyEpoch"></ng-container>
|
<ng-container *ngTemplateOutlet="difficultyEpoch"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@@ -89,7 +88,7 @@
|
|||||||
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
|
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
|
||||||
<td class="table-cell-size">
|
<td class="table-cell-size">
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / 4000000)*100 + '%' }"> </div>
|
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"> </div>
|
||||||
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -114,9 +113,9 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let transaction of transactions$ | async; let i = index;">
|
<tr *ngFor="let transaction of transactions$ | async; let i = index;">
|
||||||
<td class="table-cell-txid"><a [routerLink]="['/tx' | relativeUrl, transaction.txid]">{{ transaction.txid | shortenString : 10 }}</a></td>
|
<td class="table-cell-txid"><a [routerLink]="['/tx' | relativeUrl, transaction.txid]">{{ transaction.txid | shortenString : 10 }}</a></td>
|
||||||
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount></td>
|
<td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquid'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td>
|
||||||
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
|
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
|
||||||
<td class="table-cell-fees">{{ transaction.fee / transaction.vsize | number : '1.1-1' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
<td class="table-cell-fees">{{ transaction.fee / transaction.vsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -166,10 +165,10 @@
|
|||||||
<ng-template #mempoolTable let-mempoolInfoData>
|
<ng-template #mempoolTable let-mempoolInfoData>
|
||||||
<div class="mempool-info-data">
|
<div class="mempool-info-data">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.00001 else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
|
<h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.00001 || (stateService.env.BASE_MODULE === 'liquid' && mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.000001) else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
|
||||||
<ng-template #purgingText><h5 class="card-title" i18n="dashboard.purging|Purgin below fee">Purging</h5></ng-template>
|
<ng-template #purgingText><h5 class="card-title" i18n="dashboard.purging|Purgin below fee">Purging</h5></ng-template>
|
||||||
<p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
|
<p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
|
||||||
<ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee > 0.00001">< </ng-template>{{ mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000 | number : '1.1-1' }} <span><ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container></span>
|
<ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee > 0.00001">< </ng-template>{{ mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000 | feeRounding }} <span><ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export class DashboardComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(LOCALE_ID) private locale: string,
|
@Inject(LOCALE_ID) private locale: string,
|
||||||
private stateService: StateService,
|
public stateService: StateService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
@@ -116,7 +116,7 @@ export class DashboardComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.difficultyEpoch$ = timer(0, 1000)
|
this.difficultyEpoch$ = timer(0, 1000)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => combineLatest([
|
switchMap(() => combineLatest([
|
||||||
@@ -128,72 +128,65 @@ export class DashboardComponent implements OnInit {
|
|||||||
const now = new Date().getTime() / 1000;
|
const now = new Date().getTime() / 1000;
|
||||||
const diff = now - DATime;
|
const diff = now - DATime;
|
||||||
const blocksInEpoch = block.height % 2016;
|
const blocksInEpoch = block.height % 2016;
|
||||||
const estimatedBlocks = Math.round(diff / 60 / 10);
|
const progress = (blocksInEpoch >= 0) ? (blocksInEpoch / 2016 * 100).toFixed(2) : `100`;
|
||||||
let difficultyChange = 0;
|
const remainingBlocks = 2016 - blocksInEpoch;
|
||||||
|
const newDifficultyHeight = block.height + remainingBlocks;
|
||||||
|
|
||||||
|
let change = 0;
|
||||||
if (blocksInEpoch > 0) {
|
if (blocksInEpoch > 0) {
|
||||||
difficultyChange = (600 / (diff / blocksInEpoch ) - 1) * 100;
|
change = (600 / (diff / blocksInEpoch ) - 1) * 100;
|
||||||
|
}
|
||||||
|
if (change > 300) {
|
||||||
|
change = 300;
|
||||||
|
}
|
||||||
|
if (change < -75) {
|
||||||
|
change = -75;
|
||||||
}
|
}
|
||||||
|
|
||||||
let base = 0;
|
const timeAvgDiff = change * 0.1;
|
||||||
|
|
||||||
if (blocksInEpoch >= estimatedBlocks) {
|
let timeAvgMins = 10;
|
||||||
base = estimatedBlocks / 2016 * 100;
|
if (timeAvgDiff > 0) {
|
||||||
|
timeAvgMins -= Math.abs(timeAvgDiff);
|
||||||
} else {
|
} else {
|
||||||
base = blocksInEpoch / 2016 * 100;
|
timeAvgMins += Math.abs(timeAvgDiff);
|
||||||
}
|
}
|
||||||
|
|
||||||
let colorAdjustments = '#dc3545';
|
const timeAvg = timeAvgMins.toFixed(0);
|
||||||
if (difficultyChange >= 0) {
|
const remainingTime = (remainingBlocks * timeAvgMins * 60 * 1000) + (now * 1000);
|
||||||
|
|
||||||
|
let colorAdjustments = '#ffffff66';
|
||||||
|
if (change > 0) {
|
||||||
colorAdjustments = '#3bcc49';
|
colorAdjustments = '#3bcc49';
|
||||||
}
|
}
|
||||||
|
if (change < 0) {
|
||||||
|
colorAdjustments = '#dc3545';
|
||||||
|
}
|
||||||
|
|
||||||
let colorPreviousAdjustments = '#dc3545';
|
let colorPreviousAdjustments = '#dc3545';
|
||||||
if(previousRetarget){
|
if (previousRetarget){
|
||||||
if (previousRetarget >= 0) {
|
if (previousRetarget >= 0) {
|
||||||
colorPreviousAdjustments = '#3bcc49';
|
colorPreviousAdjustments = '#3bcc49';
|
||||||
}
|
}
|
||||||
if (previousRetarget === 0) {
|
if (previousRetarget === 0) {
|
||||||
colorPreviousAdjustments = '#ffffff66';
|
colorPreviousAdjustments = '#ffffff66';
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
colorPreviousAdjustments = '#ffffff66';
|
colorPreviousAdjustments = '#ffffff66';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const timeAvgDiff = difficultyChange * 0.1;
|
|
||||||
|
|
||||||
let timeAvgMins = 10;
|
|
||||||
if(timeAvgDiff > timeAvgDiff){
|
|
||||||
if (timeAvgDiff > 0){
|
|
||||||
timeAvgMins -= Math.abs(timeAvgDiff);
|
|
||||||
} else {
|
|
||||||
timeAvgMins += Math.abs(timeAvgDiff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const remainingBlocks = 2016 - blocksInEpoch;
|
|
||||||
const nowMilliseconds = now * 1000;
|
|
||||||
const timeAvgMilliseconds = timeAvgMins * 60 * 1000;
|
|
||||||
const remainingBlocsMilliseconds = remainingBlocks * timeAvgMilliseconds;
|
|
||||||
|
|
||||||
if(difficultyChange > 300) {
|
|
||||||
difficultyChange = 300;
|
|
||||||
}
|
|
||||||
if(difficultyChange < -75){
|
|
||||||
difficultyChange = -75;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base: base + '%',
|
base: `${progress}%`,
|
||||||
change: difficultyChange,
|
change,
|
||||||
progress: base.toFixed(2),
|
progress,
|
||||||
remainingBlocks,
|
remainingBlocks,
|
||||||
timeAvg: timeAvgMins.toFixed(0),
|
timeAvg,
|
||||||
colorAdjustments,
|
colorAdjustments,
|
||||||
colorPreviousAdjustments,
|
colorPreviousAdjustments,
|
||||||
blocksInEpoch,
|
blocksInEpoch,
|
||||||
newDifficultyHeight: block.height + remainingBlocks,
|
newDifficultyHeight,
|
||||||
remainingTime: remainingBlocsMilliseconds + nowMilliseconds,
|
remainingTime,
|
||||||
previousRetarget: previousRetarget ? previousRetarget : 0
|
previousRetarget,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -207,7 +200,7 @@ export class DashboardComponent implements OnInit {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
size: size,
|
size: size,
|
||||||
blocks: Math.ceil(vsize / 1000000)
|
blocks: Math.ceil(vsize / this.stateService.blockVSize)
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user