diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 0825ce810..80c2b4e28 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -40,7 +40,9 @@ "PORT": 8332, "USERNAME": "mempool", "PASSWORD": "mempool", - "TIMEOUT": 60000 + "TIMEOUT": 60000, + "COOKIE": false, + "COOKIE_PATH": "/path/to/bitcoin/.cookie" }, "ELECTRUM": { "HOST": "127.0.0.1", @@ -60,7 +62,9 @@ "PORT": 8332, "USERNAME": "mempool", "PASSWORD": "mempool", - "TIMEOUT": 60000 + "TIMEOUT": 60000, + "COOKIE": false, + "COOKIE_PATH": "/path/to/bitcoin/.cookie" }, "DATABASE": { "ENABLED": true, diff --git a/backend/package-lock.json b/backend/package-lock.json index 33f19a26d..3e9e31988 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,7 +12,7 @@ "@babel/core": "^7.23.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "~1.5.0", + "axios": "~1.6.1", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", "express": "~4.18.2", @@ -2325,9 +2325,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", - "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -9415,9 +9415,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "axios": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", - "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", diff --git a/backend/package.json b/backend/package.json index bdce50902..0c1d3cc4a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -41,7 +41,7 @@ "@babel/core": "^7.23.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "~1.5.0", + "axios": "~1.6.1", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", "express": "~4.18.2", diff --git a/backend/src/__fixtures__/mempool-config.template.json b/backend/src/__fixtures__/mempool-config.template.json index 0e555014c..7d5a14de6 100644 --- a/backend/src/__fixtures__/mempool-config.template.json +++ b/backend/src/__fixtures__/mempool-config.template.json @@ -41,7 +41,9 @@ "PORT": 15, "USERNAME": "__CORE_RPC_USERNAME__", "PASSWORD": "__CORE_RPC_PASSWORD__", - "TIMEOUT": 1000 + "TIMEOUT": 1000, + "COOKIE": false, + "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__" }, "ELECTRUM": { "HOST": "__ELECTRUM_HOST__", @@ -61,7 +63,9 @@ "PORT": 17, "USERNAME": "__SECOND_CORE_RPC_USERNAME__", "PASSWORD": "__SECOND_CORE_RPC_PASSWORD__", - "TIMEOUT": 2000 + "TIMEOUT": 2000, + "COOKIE": false, + "COOKIE_PATH": "__SECOND_CORE_RPC_COOKIE_PATH__" }, "DATABASE": { "ENABLED": false, diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts index fbf1e3f0a..a565c64af 100644 --- a/backend/src/__tests__/config.test.ts +++ b/backend/src/__tests__/config.test.ts @@ -66,7 +66,9 @@ describe('Mempool Backend Config', () => { PORT: 8332, USERNAME: 'mempool', PASSWORD: 'mempool', - TIMEOUT: 60000 + TIMEOUT: 60000, + COOKIE: false, + COOKIE_PATH: '/bitcoin/.cookie' }); expect(config.SECOND_CORE_RPC).toStrictEqual({ @@ -74,7 +76,9 @@ describe('Mempool Backend Config', () => { PORT: 8332, USERNAME: 'mempool', PASSWORD: 'mempool', - TIMEOUT: 60000 + TIMEOUT: 60000, + COOKIE: false, + COOKIE_PATH: '/bitcoin/.cookie' }); expect(config.DATABASE).toStrictEqual({ diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index c44653a3d..9407a5441 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -32,4 +32,5 @@ export interface BitcoinRpcCredentials { user: string; pass: string; timeout: number; + cookie?: string; } diff --git a/backend/src/api/bitcoin/bitcoin-client.ts b/backend/src/api/bitcoin/bitcoin-client.ts index 429638984..e8b30a888 100644 --- a/backend/src/api/bitcoin/bitcoin-client.ts +++ b/backend/src/api/bitcoin/bitcoin-client.ts @@ -8,6 +8,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = { user: config.CORE_RPC.USERNAME, pass: config.CORE_RPC.PASSWORD, timeout: config.CORE_RPC.TIMEOUT, + cookie: config.CORE_RPC.COOKIE ? config.CORE_RPC.COOKIE_PATH : undefined, }; export default new bitcoin.Client(nodeRpcCredentials); diff --git a/backend/src/api/bitcoin/bitcoin-second-client.ts b/backend/src/api/bitcoin/bitcoin-second-client.ts index 7f81a96a0..6ae9cefb0 100644 --- a/backend/src/api/bitcoin/bitcoin-second-client.ts +++ b/backend/src/api/bitcoin/bitcoin-second-client.ts @@ -8,6 +8,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = { user: config.SECOND_CORE_RPC.USERNAME, pass: config.SECOND_CORE_RPC.PASSWORD, timeout: config.SECOND_CORE_RPC.TIMEOUT, + cookie: config.SECOND_CORE_RPC.COOKIE ? config.SECOND_CORE_RPC.COOKIE_PATH : undefined, }; export default new bitcoin.Client(nodeRpcCredentials); diff --git a/backend/src/api/disk-cache.ts b/backend/src/api/disk-cache.ts index 6f603489a..093f07f0d 100644 --- a/backend/src/api/disk-cache.ts +++ b/backend/src/api/disk-cache.ts @@ -252,7 +252,11 @@ class DiskCache { } if (rbfData?.rbf) { - rbfCache.load(rbfData.rbf); + rbfCache.load({ + txs: rbfData.rbf.txs.map(([txid, entry]) => ({ value: entry })), + trees: rbfData.rbf.trees, + expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })), + }); } } catch (e) { logger.warn('Failed to parse rbf cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e)); diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index b5592252c..6e1f37afb 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -53,6 +53,9 @@ class RbfCache { private expiring: Map = new Map(); private cacheQueue: CacheEvent[] = []; + private evictionCount = 0; + private staleCount = 0; + constructor() { setInterval(this.cleanup.bind(this), 1000 * 60 * 10); } @@ -245,6 +248,7 @@ class RbfCache { // flag a transaction as removed from the mempool public evict(txid: string, fast: boolean = false): void { + this.evictionCount++; if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) { const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours this.addExpiration(txid, expiryTime); @@ -272,18 +276,23 @@ class RbfCache { this.remove(txid); } } - logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire`); + logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire (${this.evictionCount} newly expired)`); + this.evictionCount = 0; } // remove a transaction & all previous versions from the cache private remove(txid): void { // don't remove a transaction if a newer version remains in the mempool if (!this.replacedBy.has(txid)) { + const root = this.treeMap.get(txid); const replaces = this.replaces.get(txid); this.replaces.delete(txid); this.treeMap.delete(txid); this.removeTx(txid); this.removeExpiration(txid); + if (root === txid) { + this.removeTree(txid); + } for (const tx of (replaces || [])) { // recursively remove prior versions from the cache this.replacedBy.delete(tx); @@ -359,18 +368,25 @@ class RbfCache { } public async load({ txs, trees, expiring }): Promise { - txs.forEach(txEntry => { - this.txs.set(txEntry.key, txEntry.value); - }); - for (const deflatedTree of trees) { - await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); - } - expiring.forEach(expiringEntry => { - if (this.txs.has(expiringEntry.key)) { - this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime()); + try { + txs.forEach(txEntry => { + this.txs.set(txEntry.value.txid, txEntry.value); + }); + this.staleCount = 0; + for (const deflatedTree of trees) { + await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); } - }); - this.cleanup(); + expiring.forEach(expiringEntry => { + if (this.txs.has(expiringEntry.key)) { + this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime()); + } + }); + logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`); + this.staleCount = 0; + this.cleanup(); + } catch (e) { + logger.err('failed to restore RBF cache: ' + (e instanceof Error ? e.message : e)); + } } exportTree(tree: RbfTree, deflated: any = null) { @@ -398,6 +414,13 @@ class RbfCache { const treeInfo = deflated[txid]; const replaces: RbfTree[] = []; + // if the root tx is unknown, remove this tree and return early + if (root === txid && !txs.has(txid)) { + this.staleCount++; + this.removeTree(deflated.key); + return; + } + // check if any transactions in this tree have already been confirmed mined = mined || treeInfo.mined; let exists = mined; @@ -413,7 +436,7 @@ class RbfCache { this.evict(txid, true); } } catch (e) { - // most transactions do not exist + // most transactions only exist in our cache } } diff --git a/backend/src/api/redis-cache.ts b/backend/src/api/redis-cache.ts index fcde8013a..00b280274 100644 --- a/backend/src/api/redis-cache.ts +++ b/backend/src/api/redis-cache.ts @@ -219,7 +219,7 @@ class RedisCache { await memPool.$setMempool(loadedMempool); await rbfCache.load({ txs: rbfTxs, - trees: rbfTrees.map(loadedTree => loadedTree.value), + trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }), expiring: rbfExpirations, }); } diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index c50941f39..3a444701f 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -577,7 +577,7 @@ class WebsocketHandler { response['utxoSpent'] = JSON.stringify(outspends); } - const rbfReplacedBy = rbfCache.getReplacedBy(client['track-tx']); + const rbfReplacedBy = rbfChanges.map[client['track-tx']] ? rbfCache.getReplacedBy(client['track-tx']) : false; if (rbfReplacedBy) { response['rbfTransaction'] = JSON.stringify({ txid: rbfReplacedBy, diff --git a/backend/src/config.ts b/backend/src/config.ts index 6ea7c48f8..37d5a2de9 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -78,6 +78,8 @@ interface IConfig { USERNAME: string; PASSWORD: string; TIMEOUT: number; + COOKIE: boolean; + COOKIE_PATH: string; }; SECOND_CORE_RPC: { HOST: string; @@ -85,6 +87,8 @@ interface IConfig { USERNAME: string; PASSWORD: string; TIMEOUT: number; + COOKIE: boolean; + COOKIE_PATH: string; }; DATABASE: { ENABLED: boolean; @@ -207,6 +211,8 @@ const defaults: IConfig = { 'USERNAME': 'mempool', 'PASSWORD': 'mempool', 'TIMEOUT': 60000, + 'COOKIE': false, + 'COOKIE_PATH': '/bitcoin/.cookie' }, 'SECOND_CORE_RPC': { 'HOST': '127.0.0.1', @@ -214,6 +220,8 @@ const defaults: IConfig = { 'USERNAME': 'mempool', 'PASSWORD': 'mempool', 'TIMEOUT': 60000, + 'COOKIE': false, + 'COOKIE_PATH': '/bitcoin/.cookie' }, 'DATABASE': { 'ENABLED': true, diff --git a/backend/src/rpc-api/jsonrpc.ts b/backend/src/rpc-api/jsonrpc.ts index 4f7a38baa..0bcbdc16c 100644 --- a/backend/src/rpc-api/jsonrpc.ts +++ b/backend/src/rpc-api/jsonrpc.ts @@ -1,5 +1,6 @@ var http = require('http') var https = require('https') +import { readFileSync } from 'fs'; var JsonRPC = function (opts) { // @ts-ignore @@ -55,7 +56,13 @@ JsonRPC.prototype.call = function (method, params) { } // use HTTP auth if user and password set - if (this.opts.user && this.opts.pass) { + if (this.opts.cookie) { + if (!this.cachedCookie) { + this.cachedCookie = readFileSync(this.opts.cookie).toString(); + } + // @ts-ignore + requestOptions.auth = this.cachedCookie; + } else if (this.opts.user && this.opts.pass) { // @ts-ignore requestOptions.auth = this.opts.user + ':' + this.opts.pass } @@ -93,7 +100,7 @@ JsonRPC.prototype.call = function (method, params) { reject(err) }) - request.on('response', function (response) { + request.on('response', (response) => { clearTimeout(reqTimeout) // We need to buffer the response chunks in a nonblocking way. @@ -104,7 +111,7 @@ JsonRPC.prototype.call = function (method, params) { // When all the responses are finished, we decode the JSON and // depending on whether it's got a result or an error, we call // emitSuccess or emitError on the promise. - response.on('end', function () { + response.on('end', () => { var err if (cbCalled) return @@ -113,6 +120,14 @@ JsonRPC.prototype.call = function (method, params) { try { var decoded = JSON.parse(buffer) } catch (e) { + // if we authenticated using a cookie and it failed, read the cookie file again + if ( + response.statusCode === 401 /* Unauthorized */ && + this.opts.cookie + ) { + this.cachedCookie = undefined; + } + if (response.statusCode !== 200) { err = new Error('Invalid params, response status code: ' + response.statusCode) err.code = -32602 diff --git a/docker/README.md b/docker/README.md index 13bda7ec6..8dc1f264a 100644 --- a/docker/README.md +++ b/docker/README.md @@ -164,7 +164,9 @@ Corresponding `docker-compose.yml` overrides: "PORT": 8332, "USERNAME": "mempool", "PASSWORD": "mempool", - "TIMEOUT": 60000 + "TIMEOUT": 60000, + "COOKIE": false, + "COOKIE_PATH": "" }, ``` @@ -177,6 +179,8 @@ Corresponding `docker-compose.yml` overrides: CORE_RPC_USERNAME: "" CORE_RPC_PASSWORD: "" CORE_RPC_TIMEOUT: 60000 + CORE_RPC_COOKIE: false + CORE_RPC_COOKIE_PATH: "" ... ``` @@ -231,7 +235,9 @@ Corresponding `docker-compose.yml` overrides: "PORT": 8332, "USERNAME": "mempool", "PASSWORD": "mempool", - "TIMEOUT": 60000 + "TIMEOUT": 60000, + "COOKIE": false, + "COOKIE_PATH": "" }, ``` @@ -244,6 +250,8 @@ Corresponding `docker-compose.yml` overrides: SECOND_CORE_RPC_USERNAME: "" SECOND_CORE_RPC_PASSWORD: "" SECOND_CORE_RPC_TIMEOUT: "" + SECOND_CORE_RPC_COOKIE: false + SECOND_CORE_RPC_COOKIE_PATH: "" ... ``` diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json index 0e445b358..a1359db97 100644 --- a/docker/backend/mempool-config.json +++ b/docker/backend/mempool-config.json @@ -41,7 +41,9 @@ "PORT": __CORE_RPC_PORT__, "USERNAME": "__CORE_RPC_USERNAME__", "PASSWORD": "__CORE_RPC_PASSWORD__", - "TIMEOUT": __CORE_RPC_TIMEOUT__ + "TIMEOUT": __CORE_RPC_TIMEOUT__, + "COOKIE": __CORE_RPC_COOKIE__, + "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__" }, "ELECTRUM": { "HOST": "__ELECTRUM_HOST__", @@ -61,7 +63,9 @@ "PORT": __SECOND_CORE_RPC_PORT__, "USERNAME": "__SECOND_CORE_RPC_USERNAME__", "PASSWORD": "__SECOND_CORE_RPC_PASSWORD__", - "TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__ + "TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__, + "COOKIE": __SECOND_CORE_RPC_COOKIE__, + "COOKIE_PATH": "__SECOND_CORE_RPC_COOKIE_PATH__" }, "DATABASE": { "ENABLED": __DATABASE_ENABLED__, diff --git a/docker/backend/start.sh b/docker/backend/start.sh index 3987dbc23..23c578efe 100755 --- a/docker/backend/start.sh +++ b/docker/backend/start.sh @@ -43,6 +43,8 @@ __CORE_RPC_PORT__=${CORE_RPC_PORT:=8332} __CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool} __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool} __CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000} +__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false} +__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""} # ELECTRUM __ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1} @@ -63,6 +65,8 @@ __SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332} __SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool} __SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool} __SECOND_CORE_RPC_TIMEOUT__=${SECOND_CORE_RPC_TIMEOUT:=60000} +__SECOND_CORE_RPC_COOKIE__=${SECOND_CORE_RPC_COOKIE:=false} +__SECOND_CORE_RPC_COOKIE_PATH__=${SECOND_CORE_RPC_COOKIE_PATH:=""} # DATABASE __DATABASE_ENABLED__=${DATABASE_ENABLED:=true} @@ -188,6 +192,8 @@ sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json sed -i "s!__CORE_RPC_USERNAME__!${__CORE_RPC_USERNAME__}!g" mempool-config.json sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json +sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json +sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json @@ -205,6 +211,8 @@ sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config sed -i "s!__SECOND_CORE_RPC_USERNAME__!${__SECOND_CORE_RPC_USERNAME__}!g" mempool-config.json sed -i "s!__SECOND_CORE_RPC_PASSWORD__!${__SECOND_CORE_RPC_PASSWORD__}!g" mempool-config.json sed -i "s!__SECOND_CORE_RPC_TIMEOUT__!${__SECOND_CORE_RPC_TIMEOUT__}!g" mempool-config.json +sed -i "s!__SECOND_CORE_RPC_COOKIE__!${__SECOND_CORE_RPC_COOKIE__}!g" mempool-config.json +sed -i "s!__SECOND_CORE_RPC_COOKIE_PATH__!${__SECOND_CORE_RPC_COOKIE_PATH__}!g" mempool-config.json sed -i "s!__DATABASE_ENABLED__!${__DATABASE_ENABLED__}!g" mempool-config.json sed -i "s!__DATABASE_HOST__!${__DATABASE_HOST__}!g" mempool-config.json diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 49410de87..e4cfb1fc4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -33,9 +33,9 @@ "clipboard": "^2.0.11", "domino": "^2.1.6", "echarts": "~5.4.3", - "echarts-gl": "^2.0.9", "lightweight-charts": "~3.8.0", - "ngx-echarts": "~16.0.0", + "mock-socket": "~9.3.1", + "ngx-echarts": "~16.2.0", "ngx-infinite-scroll": "^16.0.0", "qrcode": "1.5.1", "rxjs": "~7.8.1", @@ -59,10 +59,10 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.3.0", + "cypress": "^13.5.0", "cypress-fail-on-console-error": "~5.0.0", "cypress-wait-until": "^2.0.1", - "mock-socket": "~9.2.1", + "mock-socket": "~9.3.1", "start-server-and-test": "~2.0.0" } }, @@ -6429,11 +6429,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/claygl": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/claygl/-/claygl-1.3.0.tgz", - "integrity": "sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ==" - }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -7153,9 +7148,9 @@ "peer": true }, "node_modules/cypress": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.3.0.tgz", - "integrity": "sha512-mpI8qcTwLGiA4zEQvTC/U1xGUezVV4V8HQCOYjlEOrVmU1etVvxOjkCXHGwrlYdZU/EPmUiWfsO3yt1o+Q2bgw==", + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.0.tgz", + "integrity": "sha512-oh6U7h9w8wwHfzNDJQ6wVcAeXu31DlIYlNOBvfd6U4CcB8oe4akawQmH+QJVOMZlM42eBoCne015+svVqdwdRQ==", "hasInstallScript": true, "optional": true, "dependencies": { @@ -7825,18 +7820,6 @@ "zrender": "5.4.4" } }, - "node_modules/echarts-gl": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/echarts-gl/-/echarts-gl-2.0.9.tgz", - "integrity": "sha512-oKeMdkkkpJGWOzjgZUsF41DOh6cMsyrGGXimbjK2l6Xeq/dBQu4ShG2w2Dzrs/1bD27b2pLTGSaUzouY191gzA==", - "dependencies": { - "claygl": "^1.2.1", - "zrender": "^5.1.1" - }, - "peerDependencies": { - "echarts": "^5.1.2" - } - }, "node_modules/echarts/node_modules/tslib": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", @@ -9531,9 +9514,9 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "optional": true, "engines": { "node": "*" @@ -12209,9 +12192,9 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, "node_modules/mock-socket": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.2.1.tgz", - "integrity": "sha512-aw9F9T9G2zpGipLLhSNh6ZpgUyUl4frcVmRN08uE1NWPWg43Wx6+sGPDbQ7E5iFZZDJW5b5bypMeAEHqTbIFag==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", "optional": true, "engines": { "node": ">= 8" @@ -12460,9 +12443,9 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, "node_modules/ngx-echarts": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-16.0.0.tgz", - "integrity": "sha512-hdM7/CL29bY3sF3V5ihb7H1NeUsQlhijp8tVxT23+vkNTf9SJrUHPjs9oHOMkbTlr2Q8HB+eVpckYAL/tuK0CQ==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-16.2.0.tgz", + "integrity": "sha512-yhuDbp6qdkmR4kRVLS06Z0Iumod7xOj5n/Z++clRiKM24OQ4sM8WuOTicdfWy6eeYDNywdGSrri4Y5SUGRD8bg==", "dependencies": { "tslib": "^2.3.0" }, @@ -21550,11 +21533,6 @@ "safe-buffer": "^5.0.1" } }, - "claygl": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/claygl/-/claygl-1.3.0.tgz", - "integrity": "sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ==" - }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -22118,9 +22096,9 @@ "peer": true }, "cypress": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.3.0.tgz", - "integrity": "sha512-mpI8qcTwLGiA4zEQvTC/U1xGUezVV4V8HQCOYjlEOrVmU1etVvxOjkCXHGwrlYdZU/EPmUiWfsO3yt1o+Q2bgw==", + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.0.tgz", + "integrity": "sha512-oh6U7h9w8wwHfzNDJQ6wVcAeXu31DlIYlNOBvfd6U4CcB8oe4akawQmH+QJVOMZlM42eBoCne015+svVqdwdRQ==", "optional": true, "requires": { "@cypress/request": "^3.0.0", @@ -22659,15 +22637,6 @@ } } }, - "echarts-gl": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/echarts-gl/-/echarts-gl-2.0.9.tgz", - "integrity": "sha512-oKeMdkkkpJGWOzjgZUsF41DOh6cMsyrGGXimbjK2l6Xeq/dBQu4ShG2w2Dzrs/1bD27b2pLTGSaUzouY191gzA==", - "requires": { - "claygl": "^1.2.1", - "zrender": "^5.1.1" - } - }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -23971,9 +23940,9 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "optional": true }, "get-intrinsic": { @@ -25945,9 +25914,9 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, "mock-socket": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.2.1.tgz", - "integrity": "sha512-aw9F9T9G2zpGipLLhSNh6ZpgUyUl4frcVmRN08uE1NWPWg43Wx6+sGPDbQ7E5iFZZDJW5b5bypMeAEHqTbIFag==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", "optional": true }, "module-deps": { @@ -26141,9 +26110,9 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, "ngx-echarts": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-16.0.0.tgz", - "integrity": "sha512-hdM7/CL29bY3sF3V5ihb7H1NeUsQlhijp8tVxT23+vkNTf9SJrUHPjs9oHOMkbTlr2Q8HB+eVpckYAL/tuK0CQ==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-16.2.0.tgz", + "integrity": "sha512-yhuDbp6qdkmR4kRVLS06Z0Iumod7xOj5n/Z++clRiKM24OQ4sM8WuOTicdfWy6eeYDNywdGSrri4Y5SUGRD8bg==", "requires": { "tslib": "^2.3.0" } diff --git a/frontend/package.json b/frontend/package.json index 0c7874c30..e549eb122 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -86,7 +86,7 @@ "domino": "^2.1.6", "echarts": "~5.4.3", "lightweight-charts": "~3.8.0", - "ngx-echarts": "~16.0.0", + "ngx-echarts": "~16.2.0", "ngx-infinite-scroll": "^16.0.0", "qrcode": "1.5.1", "rxjs": "~7.8.1", @@ -110,10 +110,10 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.3.0", + "cypress": "^13.5.0", "cypress-fail-on-console-error": "~5.0.0", "cypress-wait-until": "^2.0.1", - "mock-socket": "~9.2.1", + "mock-socket": "~9.3.1", "start-server-and-test": "~2.0.0" }, "scarfSettings": { diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 7c2ac1274..ce91019ff 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -1,30 +1,10 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { AppPreloadingStrategy } from './app.preloading-strategy' -import { StartComponent } from './components/start/start.component'; -import { TransactionComponent } from './components/transaction/transaction.component'; -import { BlockComponent } from './components/block/block.component'; import { BlockViewComponent } from './components/block-view/block-view.component'; import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component'; import { ClockComponent } from './components/clock/clock.component'; -import { AddressComponent } from './components/address/address.component'; -import { MasterPageComponent } from './components/master-page/master-page.component'; -import { AboutComponent } from './components/about/about.component'; import { StatusViewComponent } from './components/status-view/status-view.component'; -import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component'; -import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component'; -import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component'; -import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component'; -import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; -import { BlocksList } from './components/blocks-list/blocks-list.component'; -import { RbfList } from './components/rbf-list/rbf-list.component'; -import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component'; -import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component'; -import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; -import { AssetsComponent } from './components/assets/assets.component'; -import { AssetComponent } from './components/asset/asset.component'; -import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component'; -import { CalculatorComponent } from './components/calculator/calculator.component'; const browserWindow = window || {}; // @ts-ignore @@ -37,95 +17,13 @@ let routes: Routes = [ { path: '', pathMatch: 'full', - loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule), + loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { path: '', - component: MasterPageComponent, - children: [ - { - path: 'mining/blocks', - redirectTo: 'blocks', - pathMatch: 'full' - }, - { - path: 'tx/push', - component: PushTransactionComponent, - }, - { - path: 'about', - component: AboutComponent, - }, - { - path: 'blocks', - component: BlocksList, - }, - { - path: 'rbf', - component: RbfList, - }, - { - path: 'terms-of-service', - component: TermsOfServiceComponent - }, - { - path: 'privacy-policy', - component: PrivacyPolicyComponent - }, - { - path: 'trademark-policy', - component: TrademarkPolicyComponent - }, - { - path: 'address/:id', - children: [], - component: AddressComponent, - data: { - ogImage: true, - networkSpecific: true, - } - }, - { - path: 'tx', - component: StartComponent, - data: { networkSpecific: true }, - children: [ - { - path: ':id', - component: TransactionComponent - }, - ], - }, - { - path: 'block', - component: StartComponent, - data: { networkSpecific: true }, - children: [ - { - path: ':id', - component: BlockComponent, - data: { - ogImage: true - } - }, - ], - }, - { - path: 'docs', - loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule), - data: { preload: true }, - }, - { - path: 'api', - loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) - }, - { - path: 'lightning', - loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule), - data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] }, - }, - ], + loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + data: { preload: true }, }, { path: 'status', @@ -134,7 +32,8 @@ let routes: Routes = [ }, { path: '', - loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule) + loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + data: { preload: true }, }, { path: '**', @@ -153,88 +52,13 @@ let routes: Routes = [ { path: '', pathMatch: 'full', - loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule) + loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + data: { preload: true }, }, { path: '', - component: MasterPageComponent, - children: [ - { - path: 'tx/push', - component: PushTransactionComponent, - }, - { - path: 'about', - component: AboutComponent, - }, - { - path: 'blocks', - component: BlocksList, - }, - { - path: 'rbf', - component: RbfList, - }, - { - path: 'terms-of-service', - component: TermsOfServiceComponent - }, - { - path: 'privacy-policy', - component: PrivacyPolicyComponent - }, - { - path: 'trademark-policy', - component: TrademarkPolicyComponent - }, - { - path: 'address/:id', - children: [], - component: AddressComponent, - data: { - ogImage: true, - networkSpecific: true, - } - }, - { - path: 'tx', - data: { networkSpecific: true }, - component: StartComponent, - children: [ - { - path: ':id', - component: TransactionComponent - }, - ], - }, - { - path: 'block', - data: { networkSpecific: true }, - component: StartComponent, - children: [ - { - path: ':id', - component: BlockComponent, - data: { - ogImage: true - } - }, - ], - }, - { - path: 'docs', - loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) - }, - { - path: 'api', - loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) - }, - { - path: 'lightning', - data: { networks: ['bitcoin'] }, - loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) - }, - ], + loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + data: { preload: true }, }, { path: 'status', @@ -243,7 +67,8 @@ let routes: Routes = [ }, { path: '', - loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule) + loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + data: { preload: true }, }, { path: '**', @@ -254,97 +79,13 @@ let routes: Routes = [ { path: '', pathMatch: 'full', - loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule) + loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + data: { preload: true }, }, { path: '', - component: MasterPageComponent, - children: [ - { - path: 'mining/blocks', - redirectTo: 'blocks', - pathMatch: 'full' - }, - { - path: 'tx/push', - component: PushTransactionComponent, - }, - { - path: 'about', - component: AboutComponent, - }, - { - path: 'blocks', - component: BlocksList, - }, - { - path: 'rbf', - component: RbfList, - }, - { - path: 'tools/calculator', - component: CalculatorComponent - }, - { - path: 'terms-of-service', - component: TermsOfServiceComponent - }, - { - path: 'privacy-policy', - component: PrivacyPolicyComponent - }, - { - path: 'trademark-policy', - component: TrademarkPolicyComponent - }, - { - path: 'address/:id', - children: [], - component: AddressComponent, - data: { - ogImage: true, - networkSpecific: true, - } - }, - { - path: 'tx', - data: { networkSpecific: true }, - component: StartComponent, - children: [ - { - path: ':id', - component: TransactionComponent - }, - ], - }, - { - path: 'block', - data: { networkSpecific: true }, - component: StartComponent, - children: [ - { - path: ':id', - component: BlockComponent, - data: { - ogImage: true - } - }, - ], - }, - { - path: 'docs', - loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) - }, - { - path: 'api', - loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) - }, - { - path: 'lightning', - data: { networks: ['bitcoin'] }, - loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) - }, - ], + loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + data: { preload: true }, }, { path: 'preview', @@ -390,7 +131,8 @@ let routes: Routes = [ }, { path: '', - loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule) + loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + data: { preload: true }, }, { path: '**', @@ -401,7 +143,6 @@ let routes: Routes = [ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') { routes = [{ path: '', - component: BisqMasterPageComponent, loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule) }]; } @@ -414,105 +155,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: '', pathMatch: 'full', - loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule) + loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), + data: { preload: true }, }, { path: '', - component: LiquidMasterPageComponent, - children: [ - { - path: 'tx/push', - component: PushTransactionComponent, - }, - { - path: 'about', - component: AboutComponent, - }, - { - path: 'blocks', - component: BlocksList, - }, - { - path: 'terms-of-service', - component: TermsOfServiceComponent - }, - { - path: 'privacy-policy', - component: PrivacyPolicyComponent - }, - { - path: 'trademark-policy', - component: TrademarkPolicyComponent - }, - { - path: 'address/:id', - children: [], - component: AddressComponent, - data: { - ogImage: true, - networkSpecific: true, - } - }, - { - path: 'tx', - data: { networkSpecific: true }, - component: StartComponent, - children: [ - { - path: ':id', - component: TransactionComponent - }, - ], - }, - { - path: 'block', - data: { networkSpecific: true }, - component: StartComponent, - children: [ - { - path: ':id', - component: BlockComponent, - data: { - ogImage: true - } - }, - ], - }, - { - path: 'assets', - data: { networks: ['liquid'] }, - component: AssetsNavComponent, - children: [ - { - path: 'all', - data: { networks: ['liquid'] }, - component: AssetsComponent, - }, - { - path: 'asset/:id', - data: { networkSpecific: true }, - component: AssetComponent - }, - { - path: 'group/:id', - data: { networkSpecific: true }, - component: AssetGroupComponent - }, - { - path: '**', - redirectTo: 'all' - } - ] - }, - { - path: 'docs', - loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) - }, - { - path: 'api', - loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) - }, - ], + loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), + data: { preload: true }, }, { path: 'status', @@ -521,7 +170,8 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: '', - loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule) + loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), + data: { preload: true }, }, { path: '**', @@ -532,110 +182,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: '', pathMatch: 'full', - loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule) + loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), + data: { preload: true }, }, { path: '', - component: LiquidMasterPageComponent, - children: [ - { - path: 'tx/push', - component: PushTransactionComponent, - }, - { - path: 'about', - component: AboutComponent, - }, - { - path: 'blocks', - component: BlocksList, - }, - { - path: 'terms-of-service', - component: TermsOfServiceComponent - }, - { - path: 'privacy-policy', - component: PrivacyPolicyComponent - }, - { - path: 'trademark-policy', - component: TrademarkPolicyComponent - }, - { - path: 'address/:id', - children: [], - component: AddressComponent, - data: { - ogImage: true, - networkSpecific: true, - } - }, - { - path: 'tx', - data: { networkSpecific: true }, - component: StartComponent, - children: [ - { - path: ':id', - component: TransactionComponent - }, - ], - }, - { - path: 'block', - data: { networkSpecific: true }, - component: StartComponent, - children: [ - { - path: ':id', - component: BlockComponent, - data: { - ogImage: true - } - }, - ], - }, - { - path: 'assets', - data: { networks: ['liquid'] }, - component: AssetsNavComponent, - children: [ - { - path: 'featured', - data: { networkSpecific: true }, - component: AssetsFeaturedComponent, - }, - { - path: 'all', - data: { networks: ['liquid'] }, - component: AssetsComponent, - }, - { - path: 'asset/:id', - data: { networkSpecific: true }, - component: AssetComponent - }, - { - path: 'group/:id', - data: { networkSpecific: true }, - component: AssetGroupComponent - }, - { - path: '**', - redirectTo: 'featured' - } - ] - }, - { - path: 'docs', - loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) - }, - { - path: 'api', - loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) - }, - ], + loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), + data: { preload: true }, }, { path: 'preview', @@ -657,7 +210,8 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: '', - loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule) + loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), + data: { preload: true }, }, { path: '**', diff --git a/frontend/src/app/bisq/bisq.module.ts b/frontend/src/app/bisq/bisq.module.ts index 93658d95a..f7f71156b 100644 --- a/frontend/src/app/bisq/bisq.module.ts +++ b/frontend/src/app/bisq/bisq.module.ts @@ -27,9 +27,11 @@ import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/auto import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe'; import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive'; import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component'; +import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component'; @NgModule({ declarations: [ + BisqMasterPageComponent, BisqTransactionsComponent, BisqTransactionComponent, BisqBlockComponent, diff --git a/frontend/src/app/bisq/bisq.routing.module.ts b/frontend/src/app/bisq/bisq.routing.module.ts index 11acdca2a..7c6d2ee1b 100644 --- a/frontend/src/app/bisq/bisq.routing.module.ts +++ b/frontend/src/app/bisq/bisq.routing.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { AboutComponent } from '../components/about/about.component'; +import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component'; import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component'; import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component'; import { BisqBlockComponent } from './bisq-block/bisq-block.component'; @@ -10,78 +10,83 @@ import { BisqStatsComponent } from './bisq-stats/bisq-stats.component'; import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component'; import { BisqMarketComponent } from './bisq-market/bisq-market.component'; import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component'; -import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component'; import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; const routes: Routes = [ - { - path: '', - component: BisqMainDashboardComponent, - }, - { - path: 'markets', - data: { networks: ['bisq'] }, - component: BisqDashboardComponent, - }, - { - path: 'transactions', - data: { networks: ['bisq'] }, - component: BisqTransactionsComponent - }, - { - path: 'market/:pair', - data: { networkSpecific: true }, - component: BisqMarketComponent, - }, - { - path: 'tx/push', - component: PushTransactionComponent, - }, - { - path: 'tx/:id', - data: { networkSpecific: true }, - component: BisqTransactionComponent - }, - { - path: 'blocks', - children: [], - component: BisqBlocksComponent - }, - { - path: 'block/:id', - data: { networkSpecific: true }, - component: BisqBlockComponent, - }, - { - path: 'address/:id', - data: { networkSpecific: true }, - component: BisqAddressComponent, - }, - { - path: 'stats', - data: { networks: ['bisq'] }, - component: BisqStatsComponent, - }, - { - path: 'about', - component: AboutComponent, - }, - { - path: 'docs', - loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule) - }, - { - path: 'api', - loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule) - }, - { - path: 'terms-of-service', - component: TermsOfServiceComponent - }, - { - path: '**', - redirectTo: '' - } + { + path: '', + component: BisqMasterPageComponent, + children: [ + { + path: '', + component: BisqMainDashboardComponent, + }, + { + path: 'markets', + data: { networks: ['bisq'] }, + component: BisqDashboardComponent, + }, + { + path: 'transactions', + data: { networks: ['bisq'] }, + component: BisqTransactionsComponent + }, + { + path: 'market/:pair', + data: { networkSpecific: true }, + component: BisqMarketComponent, + }, + { + path: 'tx/push', + component: PushTransactionComponent, + }, + { + path: 'tx/:id', + data: { networkSpecific: true }, + component: BisqTransactionComponent + }, + { + path: 'blocks', + children: [], + component: BisqBlocksComponent + }, + { + path: 'block/:id', + data: { networkSpecific: true }, + component: BisqBlockComponent, + }, + { + path: 'address/:id', + data: { networkSpecific: true }, + component: BisqAddressComponent, + }, + { + path: 'stats', + data: { networks: ['bisq'] }, + component: BisqStatsComponent, + }, + { + path: 'about', + loadChildren: () => import('../components/about/about.module').then(m => m.AboutModule), + }, + { + path: 'docs', + loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule) + }, + { + path: 'api', + loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule) + }, + { + path: 'terms-of-service', + loadChildren: () => import('../components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule), + }, + { + path: '**', + redirectTo: '' + } + ] + } ]; @NgModule({ diff --git a/frontend/src/app/bitcoin-graphs.module.ts b/frontend/src/app/bitcoin-graphs.module.ts new file mode 100644 index 000000000..710743245 --- /dev/null +++ b/frontend/src/app/bitcoin-graphs.module.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { MasterPageComponent } from './components/master-page/master-page.component'; + +const routes: Routes = [ + { + path: '', + component: MasterPageComponent, + loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule), + data: { preload: true }, + } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class BitcoinGraphsRoutingModule { } + +@NgModule({ + imports: [ + CommonModule, + BitcoinGraphsRoutingModule, + ], +}) +export class BitcoinGraphsModule { } + + + + + + diff --git a/frontend/src/app/components/about/about.module.ts b/frontend/src/app/components/about/about.module.ts new file mode 100644 index 000000000..1eb471f14 --- /dev/null +++ b/frontend/src/app/components/about/about.module.ts @@ -0,0 +1,40 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { AboutComponent } from './about.component'; +import { SharedModule } from '../../shared/shared.module'; + +const routes: Routes = [ + { + path: '', + component: AboutComponent, + } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class AboutRoutingModule { } + +@NgModule({ + imports: [ + CommonModule, + AboutRoutingModule, + SharedModule, + ], + declarations: [ + AboutComponent, + ] +}) +export class AboutModule { } + + + + + + diff --git a/frontend/src/app/components/block/block.module.ts b/frontend/src/app/components/block/block.module.ts new file mode 100644 index 000000000..d6991c68a --- /dev/null +++ b/frontend/src/app/components/block/block.module.ts @@ -0,0 +1,43 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { BlockComponent } from './block.component'; +import { SharedModule } from '../../shared/shared.module'; + +const routes: Routes = [ + { + path: ':id', + component: BlockComponent, + data: { + ogImage: true + } + } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class BlockRoutingModule { } + +@NgModule({ + imports: [ + CommonModule, + BlockRoutingModule, + SharedModule, + ], + declarations: [ + BlockComponent, + ] +}) +export class BlockModule { } + + + + + + diff --git a/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.scss b/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.scss new file mode 100644 index 000000000..e7150a720 --- /dev/null +++ b/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.scss @@ -0,0 +1,3 @@ +.fee-distribution-chart { + margin-top: 0.75rem; +} \ No newline at end of file diff --git a/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts b/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts index 010466952..178d87897 100644 --- a/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts +++ b/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts @@ -1,4 +1,4 @@ -import { OnChanges, OnDestroy } from '@angular/core'; +import { HostListener, OnChanges, OnDestroy } from '@angular/core'; import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { TransactionStripped } from '../../interfaces/websocket.interface'; import { StateService } from '../../services/state.service'; @@ -9,6 +9,7 @@ import { Subscription } from 'rxjs'; @Component({ selector: 'app-fee-distribution-graph', templateUrl: './fee-distribution-graph.component.html', + styleUrls: ['./fee-distribution-graph.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestroy { @@ -25,6 +26,7 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr simple: boolean = false; data: number[][]; labelInterval: number = 50; + smallScreen: boolean = window.innerWidth < 450; rateUnitSub: Subscription; weightMode: boolean = false; @@ -95,9 +97,9 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr this.mempoolVsizeFeesOptions = { grid: { height: '210', - right: '20', + right: this.smallScreen ? '10' : '20', top: '22', - left: '40', + left: this.smallScreen ? '10' : '40', }, xAxis: { type: 'category', @@ -131,16 +133,17 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr } }, axisLabel: { - show: true, + show: !this.smallScreen, formatter: (value: number): string => { const unitValue = this.weightMode ? value / 4 : value; const selectedPowerOfTen = selectPowerOfTen(unitValue); - const newVal = Math.round(unitValue / selectedPowerOfTen.divider); + const scaledValue = unitValue / selectedPowerOfTen.divider; + const newVal = scaledValue >= 100 ? Math.round(scaledValue) : scaledValue.toPrecision(3); return `${newVal}${selectedPowerOfTen.unit}`; }, }, axisTick: { - show: true, + show: !this.smallScreen, } }, series: [{ @@ -151,11 +154,13 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr position: 'top', color: '#ffffff', textShadowBlur: 0, + fontSize: this.smallScreen ? 10 : 12, formatter: (label: { data: number[] }): string => { const value = label.data[1]; const unitValue = this.weightMode ? value / 4 : value; const selectedPowerOfTen = selectPowerOfTen(unitValue); - const newVal = Math.round(unitValue / selectedPowerOfTen.divider); + const scaledValue = unitValue / selectedPowerOfTen.divider; + const newVal = scaledValue >= 100 ? Math.round(scaledValue) : scaledValue.toPrecision(3); return `${newVal}${selectedPowerOfTen.unit}`; } }, @@ -179,6 +184,16 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr }; } + @HostListener('window:resize', ['$event']) + onResize(): void { + const isSmallScreen = window.innerWidth < 450; + if (this.smallScreen !== isSmallScreen) { + this.smallScreen = isSmallScreen; + this.prepareChart(); + this.mountChart(); + } + } + ngOnDestroy(): void { this.rateUnitSub.unsubscribe(); } diff --git a/frontend/src/app/components/privacy-policy/privacy-policy.module.ts b/frontend/src/app/components/privacy-policy/privacy-policy.module.ts new file mode 100644 index 000000000..6d279d80a --- /dev/null +++ b/frontend/src/app/components/privacy-policy/privacy-policy.module.ts @@ -0,0 +1,40 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { PrivacyPolicyComponent } from './privacy-policy.component'; +import { SharedModule } from '../../shared/shared.module'; + +const routes: Routes = [ + { + path: '', + component: PrivacyPolicyComponent, + } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class PrivacyPolicyRoutingModule { } + +@NgModule({ + imports: [ + CommonModule, + PrivacyPolicyRoutingModule, + SharedModule, + ], + declarations: [ + PrivacyPolicyComponent, + ] +}) +export class PrivacyPolicyModule { } + + + + + + diff --git a/frontend/src/app/components/terms-of-service/terms-of-service.module.ts b/frontend/src/app/components/terms-of-service/terms-of-service.module.ts new file mode 100644 index 000000000..2ab139d8b --- /dev/null +++ b/frontend/src/app/components/terms-of-service/terms-of-service.module.ts @@ -0,0 +1,40 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { TermsOfServiceComponent } from './terms-of-service.component'; +import { SharedModule } from '../../shared/shared.module'; + +const routes: Routes = [ + { + path: '', + component: TermsOfServiceComponent, + } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class TermsModule { } + +@NgModule({ + imports: [ + CommonModule, + TermsModule, + SharedModule, + ], + declarations: [ + TermsOfServiceComponent, + ] +}) +export class TermsOfServiceModule { } + + + + + + diff --git a/frontend/src/app/components/trademark-policy/trademark-policy.module.ts b/frontend/src/app/components/trademark-policy/trademark-policy.module.ts new file mode 100644 index 000000000..24f70be52 --- /dev/null +++ b/frontend/src/app/components/trademark-policy/trademark-policy.module.ts @@ -0,0 +1,40 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { TrademarkPolicyComponent } from './trademark-policy.component'; +import { SharedModule } from '../../shared/shared.module'; + +const routes: Routes = [ + { + path: '', + component: TrademarkPolicyComponent, + } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class TrademarkRoutingModule { } + +@NgModule({ + imports: [ + CommonModule, + TrademarkRoutingModule, + SharedModule, + ], + declarations: [ + TrademarkPolicyComponent, + ] +}) +export class TrademarkModule { } + + + + + + diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 505c4686d..4743e5cd6 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -422,6 +422,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } this.rbfTransaction = rbfTransaction; this.replaced = true; + this.stateService.markBlock$.next({}); + if (rbfTransaction && !this.tx) { this.fetchCachedTx$.next(this.txId); } diff --git a/frontend/src/app/components/transaction/transaction.module.ts b/frontend/src/app/components/transaction/transaction.module.ts new file mode 100644 index 000000000..d933cc350 --- /dev/null +++ b/frontend/src/app/components/transaction/transaction.module.ts @@ -0,0 +1,45 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { TransactionComponent } from './transaction.component'; +import { SharedModule } from '../../shared/shared.module'; +import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module'; + +const routes: Routes = [ + { + path: ':id', + component: TransactionComponent, + data: { + ogImage: true + } + } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class TransactionRoutingModule { } + +@NgModule({ + imports: [ + CommonModule, + TransactionRoutingModule, + SharedModule, + TxBowtieModule, + ], + declarations: [ + TransactionComponent, + ] +}) +export class TransactionModule { } + + + + + + diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index c49ff0e3c..05d74a75d 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -6,7 +6,7 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter import { ElectrsApiService } from '../../services/electrs-api.service'; import { environment } from '../../../environments/environment'; import { AssetsService } from '../../services/assets.service'; -import { filter, map, tap, switchMap, shareReplay } from 'rxjs/operators'; +import { filter, map, tap, switchMap, shareReplay, catchError } from 'rxjs/operators'; import { BlockExtended } from '../../interfaces/node-api.interface'; import { ApiService } from '../../services/api.service'; import { PriceService } from '../../services/price.service'; @@ -75,7 +75,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { for (let i = 0; i < txIds.length; i += 50) { batches.push(txIds.slice(i, i + 50)); } - return forkJoin(batches.map(batch => this.apiService.getOutspendsBatched$(batch))); + return forkJoin(batches.map(batch => { return this.apiService.cachedRequest(this.apiService.getOutspendsBatched$, 250, batch); })); } else { return of([]); } @@ -90,6 +90,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { outspends.forEach((outspend, i) => { transactions[i]._outspends = outspend; }); + this.ref.markForCheck(); }), ), this.stateService.utxoSpent$ @@ -108,6 +109,10 @@ export class TransactionsListComponent implements OnInit, OnChanges { .pipe( filter(() => this.stateService.env.LIGHTNING), switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)), + catchError((error) => { + // handle 404 + return of([]); + }), tap((channels) => { if (!this.transactions) { return; diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts index 97e74957e..3bc352a35 100644 --- a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts +++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts @@ -123,7 +123,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { .pipe( switchMap((txid) => { if (!this.cached) { - return this.apiService.getOutspendsBatched$([txid]); + return this.apiService.cachedRequest(this.apiService.getOutspendsBatched$, 250, [txid]); } else { return of(null); } diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie.module.ts b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie.module.ts new file mode 100644 index 000000000..617425e7a --- /dev/null +++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../../shared/shared.module'; +import { TxBowtieGraphComponent } from '../tx-bowtie-graph/tx-bowtie-graph.component'; +import { TxBowtieGraphTooltipComponent } from '../tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component'; + + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + ], + declarations: [ + TxBowtieGraphComponent, + TxBowtieGraphTooltipComponent, + ], + exports: [ + TxBowtieGraphComponent, + TxBowtieGraphTooltipComponent, + ] +}) +export class TxBowtieModule { } + + + + + + diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts index 03800dcfc..346bcf7f1 100644 --- a/frontend/src/app/graphs/graphs.routing.module.ts +++ b/frontend/src/app/graphs/graphs.routing.module.ts @@ -8,8 +8,6 @@ import { BlockSizesWeightsGraphComponent } from '../components/block-sizes-weigh import { GraphsComponent } from '../components/graphs/graphs.component'; import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component'; import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component'; -import { LiquidMasterPageComponent } from '../components/liquid-master-page/liquid-master-page.component'; -import { MasterPageComponent } from '../components/master-page/master-page.component'; import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component'; import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component'; import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component'; @@ -18,22 +16,10 @@ import { StartComponent } from '../components/start/start.component'; import { StatisticsComponent } from '../components/statistics/statistics.component'; import { TelevisionComponent } from '../components/television/television.component'; import { DashboardComponent } from '../dashboard/dashboard.component'; -import { NodesNetworksChartComponent } from '../lightning/nodes-networks-chart/nodes-networks-chart.component'; -import { LightningStatisticsChartComponent } from '../lightning/statistics-chart/lightning-statistics-chart.component'; -import { NodesPerISPChartComponent } from '../lightning/nodes-per-isp-chart/nodes-per-isp-chart.component'; -import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-chart/nodes-per-country-chart.component'; -import { NodesMap } from '../lightning/nodes-map/nodes-map.component'; -import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels-map.component'; - -const browserWindow = window || {}; -// @ts-ignore -const browserWindowEnv = browserWindow.__env || {}; -const isLiquid = browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid'; const routes: Routes = [ { path: '', - component: isLiquid ? LiquidMasterPageComponent : MasterPageComponent, children: [ { path: 'mining/pool/:slug', @@ -108,34 +94,9 @@ const routes: Routes = [ component: BlockSizesWeightsGraphComponent, }, { - path: 'lightning/nodes-networks', - data: { networks: ['bitcoin'] }, - component: NodesNetworksChartComponent, - }, - { - path: 'lightning/capacity', - data: { networks: ['bitcoin'] }, - component: LightningStatisticsChartComponent, - }, - { - path: 'lightning/nodes-per-isp', - data: { networks: ['bitcoin'] }, - component: NodesPerISPChartComponent, - }, - { - path: 'lightning/nodes-per-country', - data: { networks: ['bitcoin'] }, - component: NodesPerCountryChartComponent, - }, - { - path: 'lightning/nodes-map', - data: { networks: ['bitcoin'] }, - component: NodesMap, - }, - { - path: 'lightning/nodes-channels-map', - data: { networks: ['bitcoin'] }, - component: NodesChannelsMap, + path: 'lightning', + data: { preload: true, networks: ['bitcoin'] }, + loadChildren: () => import ('./lightning-graphs.module').then(m => m.LightningGraphsModule), }, { path: '', diff --git a/frontend/src/app/graphs/lightning-graphs.module.ts b/frontend/src/app/graphs/lightning-graphs.module.ts new file mode 100644 index 000000000..ac123be33 --- /dev/null +++ b/frontend/src/app/graphs/lightning-graphs.module.ts @@ -0,0 +1,58 @@ +import { NgModule } from '@angular/core'; +import { SharedModule } from '../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { NodesNetworksChartComponent } from '../lightning/nodes-networks-chart/nodes-networks-chart.component'; +import { LightningStatisticsChartComponent } from '../lightning/statistics-chart/lightning-statistics-chart.component'; +import { NodesPerISPChartComponent } from '../lightning/nodes-per-isp-chart/nodes-per-isp-chart.component'; +import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-chart/nodes-per-country-chart.component'; +import { NodesMap } from '../lightning/nodes-map/nodes-map.component'; +import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels-map.component'; + +const routes: Routes = [ + { + path: 'nodes-networks', + data: { networks: ['bitcoin'] }, + component: NodesNetworksChartComponent, + }, + { + path: 'capacity', + data: { networks: ['bitcoin'] }, + component: LightningStatisticsChartComponent, + }, + { + path: 'nodes-per-isp', + data: { networks: ['bitcoin'] }, + component: NodesPerISPChartComponent, + }, + { + path: 'nodes-per-country', + data: { networks: ['bitcoin'] }, + component: NodesPerCountryChartComponent, + }, + { + path: 'nodes-map', + data: { networks: ['bitcoin'] }, + component: NodesMap, + }, + { + path: 'nodes-channels-map', + data: { networks: ['bitcoin'] }, + component: NodesChannelsMap, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class LightningGraphsRoutingModule { } + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + LightningGraphsRoutingModule, + ], +}) +export class LightningGraphsModule { } diff --git a/frontend/src/app/liquid/liquid-graphs.module.ts b/frontend/src/app/liquid/liquid-graphs.module.ts new file mode 100644 index 000000000..3da93fc9d --- /dev/null +++ b/frontend/src/app/liquid/liquid-graphs.module.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { LiquidMasterPageComponent } from '../components/liquid-master-page/liquid-master-page.component'; + +const routes: Routes = [ + { + path: '', + component: LiquidMasterPageComponent, + loadChildren: () => import('../graphs/graphs.module').then(m => m.GraphsModule), + data: { preload: true }, + } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class LiquidGraphsRoutingModule { } + +@NgModule({ + imports: [ + CommonModule, + LiquidGraphsRoutingModule, + ], +}) +export class LiquidGraphsModule { } + + + + + + diff --git a/frontend/src/app/liquid/liquid-master-page.module.ts b/frontend/src/app/liquid/liquid-master-page.module.ts new file mode 100644 index 000000000..10d87bc4b --- /dev/null +++ b/frontend/src/app/liquid/liquid-master-page.module.ts @@ -0,0 +1,125 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { SharedModule } from '../shared/shared.module'; +import { LiquidMasterPageComponent } from '../components/liquid-master-page/liquid-master-page.component'; + +import { StartComponent } from '../components/start/start.component'; +import { AddressComponent } from '../components/address/address.component'; +import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; +import { BlocksList } from '../components/blocks-list/blocks-list.component'; +import { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component'; +import { AssetsComponent } from '../components/assets/assets.component'; +import { AssetComponent } from '../components/asset/asset.component'; +import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component'; + +const routes: Routes = [ + { + path: '', + component: LiquidMasterPageComponent, + children: [ + { + path: 'tx/push', + component: PushTransactionComponent, + }, + { + path: 'about', + loadChildren: () => import('../components/about/about.module').then(m => m.AboutModule), + }, + { + path: 'blocks', + component: BlocksList, + }, + { + path: 'terms-of-service', + loadChildren: () => import('../components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule), + }, + { + path: 'privacy-policy', + loadChildren: () => import('../components/privacy-policy/privacy-policy.module').then(m => m.PrivacyPolicyModule), + }, + { + path: 'trademark-policy', + loadChildren: () => import('../components/trademark-policy/trademark-policy.module').then(m => m.TrademarkModule), + }, + { + path: 'address/:id', + children: [], + component: AddressComponent, + data: { + ogImage: true, + networkSpecific: true, + } + }, + { + path: 'tx', + component: StartComponent, + data: { preload: true, networkSpecific: true }, + loadChildren: () => import('../components/transaction/transaction.module').then(m => m.TransactionModule), + }, + { + path: 'block', + component: StartComponent, + data: { preload: true, networkSpecific: true }, + loadChildren: () => import('../components/block/block.module').then(m => m.BlockModule), + }, + { + path: 'assets', + data: { networks: ['liquid'] }, + component: AssetsNavComponent, + children: [ + { + path: 'all', + data: { networks: ['liquid'] }, + component: AssetsComponent, + }, + { + path: 'asset/:id', + data: { networkSpecific: true }, + component: AssetComponent + }, + { + path: 'group/:id', + data: { networkSpecific: true }, + component: AssetGroupComponent + }, + { + path: '**', + redirectTo: 'all' + } + ] + }, + { + path: 'docs', + loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule), + data: { preload: true }, + }, + { + path: 'api', + loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule) + }, + ], + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class LiquidRoutingModule { } + +@NgModule({ + imports: [ + CommonModule, + LiquidRoutingModule, + SharedModule, + ], + declarations: [ + LiquidMasterPageComponent, + ] +}) +export class LiquidMasterPageModule { } \ No newline at end of file diff --git a/frontend/src/app/master-page.module.ts b/frontend/src/app/master-page.module.ts new file mode 100644 index 000000000..bfc1aed53 --- /dev/null +++ b/frontend/src/app/master-page.module.ts @@ -0,0 +1,120 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { MasterPageComponent } from './components/master-page/master-page.component'; +import { SharedModule } from './shared/shared.module'; + +import { StartComponent } from './components/start/start.component'; +import { AddressComponent } from './components/address/address.component'; +import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; +import { BlocksList } from './components/blocks-list/blocks-list.component'; +import { RbfList } from './components/rbf-list/rbf-list.component'; + +const browserWindow = window || {}; +// @ts-ignore +const browserWindowEnv = browserWindow.__env || {}; + +const routes: Routes = [ + { + path: '', + component: MasterPageComponent, + children: [ + { + path: 'mining/blocks', + redirectTo: 'blocks', + pathMatch: 'full' + }, + { + path: 'tx/push', + component: PushTransactionComponent, + }, + { + path: 'about', + loadChildren: () => import('./components/about/about.module').then(m => m.AboutModule), + }, + { + path: 'blocks', + component: BlocksList, + }, + { + path: 'rbf', + component: RbfList, + }, + { + path: 'terms-of-service', + loadChildren: () => import('./components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule), + }, + { + path: 'privacy-policy', + loadChildren: () => import('./components/privacy-policy/privacy-policy.module').then(m => m.PrivacyPolicyModule), + }, + { + path: 'trademark-policy', + loadChildren: () => import('./components/trademark-policy/trademark-policy.module').then(m => m.TrademarkModule), + }, + { + path: 'address/:id', + children: [], + component: AddressComponent, + data: { + ogImage: true, + networkSpecific: true, + } + }, + { + path: 'tx', + component: StartComponent, + data: { preload: true, networkSpecific: true }, + loadChildren: () => import('./components/transaction/transaction.module').then(m => m.TransactionModule), + }, + { + path: 'block', + component: StartComponent, + data: { preload: true, networkSpecific: true }, + loadChildren: () => import('./components/block/block.module').then(m => m.BlockModule), + }, + { + path: 'docs', + loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule), + data: { preload: true }, + }, + { + path: 'api', + loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) + }, + { + path: 'lightning', + loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule), + data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] }, + }, + ], + } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class MasterPageRoutingModule { } + +@NgModule({ + imports: [ + CommonModule, + MasterPageRoutingModule, + SharedModule, + ], + declarations: [ + MasterPageComponent, + ] +}) +export class MasterPageModule { } + + + + + + diff --git a/frontend/src/app/previews.module.ts b/frontend/src/app/previews.module.ts index 2e8dbdc75..95124f232 100644 --- a/frontend/src/app/previews.module.ts +++ b/frontend/src/app/previews.module.ts @@ -9,6 +9,7 @@ import { BlockPreviewComponent } from './components/block/block-preview.componen import { AddressPreviewComponent } from './components/address/address-preview.component'; import { PoolPreviewComponent } from './components/pool/pool-preview.component'; import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; +import { TxBowtieModule } from './components/tx-bowtie-graph/tx-bowtie.module'; @NgModule({ declarations: [ TransactionPreviewComponent, @@ -23,6 +24,7 @@ import { MasterPagePreviewComponent } from './components/master-page-preview/mas RouterModule, PreviewsRoutingModule, GraphsModule, + TxBowtieModule, ], }) export class PreviewsModule { } diff --git a/frontend/src/app/previews.routing.module.ts b/frontend/src/app/previews.routing.module.ts index c2ad8db5f..6ac44a370 100644 --- a/frontend/src/app/previews.routing.module.ts +++ b/frontend/src/app/previews.routing.module.ts @@ -31,7 +31,8 @@ const routes: Routes = [ }, { path: 'lightning', - loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule) + loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule), + data: { preload: true }, }, ], } diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 744474f9d..046b27812 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit } from '../interfaces/node-api.interface'; -import { Observable, of } from 'rxjs'; +import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs'; import { StateService } from './state.service'; import { IBackendInfo, WebsocketResponse } from '../interfaces/websocket.interface'; import { Outspend, Transaction } from '../interfaces/electrs.interface'; @@ -20,6 +20,8 @@ export class ApiService { private apiBaseUrl: string; // base URL is protocol, hostname, and port private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet + private requestCache = new Map, expiry: number }>; + constructor( private httpClient: HttpClient, private stateService: StateService, @@ -44,6 +46,46 @@ export class ApiService { } } + private generateCacheKey(functionName: string, params: any[]): string { + return functionName + JSON.stringify(params); + } + + // delete expired cache entries + private cleanExpiredCache(): void { + this.requestCache.forEach((value, key) => { + if (value.expiry < Date.now()) { + this.requestCache.delete(key); + } + }); + } + + cachedRequest Observable>( + apiFunction: F, + expireAfter: number, // in ms + ...params: Parameters + ): Observable { + this.cleanExpiredCache(); + + const cacheKey = this.generateCacheKey(apiFunction.name, params); + if (!this.requestCache.has(cacheKey)) { + const subject = new BehaviorSubject(null); + this.requestCache.set(cacheKey, { subject, expiry: Date.now() + expireAfter }); + + apiFunction.bind(this)(...params).pipe( + tap(data => { + subject.next(data as T); + }), + catchError((error) => { + subject.error(error); + return of(null); + }), + shareReplay(1), + ).subscribe(); + } + + return this.requestCache.get(cacheKey).subject.asObservable().pipe(filter(val => val !== null), take(1)); + } + list2HStatistics$(): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/2h'); } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index dce65bfae..76dbc65f1 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -6,12 +6,8 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; -import { MasterPageComponent } from '../components/master-page/master-page.component'; import { MenuComponent } from '../components/menu/menu.component'; import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.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 { VbytesPipe } from './pipes/bytes-pipe/vbytes.pipe'; import { ShortenStringPipe } from './pipes/shorten-string-pipe/shorten-string.pipe'; import { CeilPipe } from './pipes/math-ceil/math-ceil.pipe'; @@ -45,9 +41,7 @@ import { AmountComponent } from '../components/amount/amount.component'; import { RouterModule } from '@angular/router'; import { CapAddressPipe } from './pipes/cap-address-pipe/cap-address-pipe'; import { StartComponent } from '../components/start/start.component'; -import { TransactionComponent } from '../components/transaction/transaction.component'; import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component'; -import { BlockComponent } from '../components/block/block.component'; import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component'; import { AddressComponent } from '../components/address/address.component'; @@ -62,13 +56,8 @@ import { FeesBoxComponent } from '../components/fees-box/fees-box.component'; import { DifficultyComponent } from '../components/difficulty/difficulty.component'; import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component'; import { DifficultyMiningComponent } from '../components/difficulty-mining/difficulty-mining.component'; -import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component'; import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component'; import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.component'; -import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component'; -import { TxBowtieGraphTooltipComponent } from '../components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component'; -import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component'; -import { TrademarkPolicyComponent } from '../components/trademark-policy/trademark-policy.component'; import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component'; import { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component'; @@ -141,15 +130,9 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir MempoolBlocksComponent, BlockchainBlocksComponent, AmountComponent, - AboutComponent, - MasterPageComponent, MenuComponent, PreviewTitleComponent, - BisqMasterPageComponent, - LiquidMasterPageComponent, StartComponent, - TransactionComponent, - BlockComponent, BlockOverviewGraphComponent, BlockOverviewTooltipComponent, TransactionsListComponent, @@ -166,11 +149,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir DifficultyTooltipComponent, RbfTimelineComponent, RbfTimelineTooltipComponent, - TxBowtieGraphComponent, - TxBowtieGraphTooltipComponent, - TermsOfServiceComponent, - PrivacyPolicyComponent, - TrademarkPolicyComponent, PushTransactionComponent, AssetsNavComponent, AssetsFeaturedComponent, @@ -233,7 +211,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir AmountShortenerPipe, ], exports: [ - MasterPageComponent, MenuComponent, RouterModule, ReactiveFormsModule, @@ -275,8 +252,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir BlockchainBlocksComponent, AmountComponent, StartComponent, - TransactionComponent, - BlockComponent, BlockOverviewGraphComponent, BlockOverviewTooltipComponent, TransactionsListComponent, @@ -293,11 +268,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir DifficultyTooltipComponent, RbfTimelineComponent, RbfTimelineTooltipComponent, - TxBowtieGraphComponent, - TxBowtieGraphTooltipComponent, - TermsOfServiceComponent, - PrivacyPolicyComponent, - TrademarkPolicyComponent, PushTransactionComponent, AssetsNavComponent, AssetsFeaturedComponent, @@ -320,6 +290,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir ConfirmationsComponent, ToggleComponent, GeolocationComponent, + TestnetAlertComponent, PreviewTitleComponent, GlobalFooterComponent, AcceleratePreviewComponent,