Merge branch 'master' into nymkappa/feature/cltv
This commit is contained in:
		
						commit
						b30483572d
					
				| @ -110,6 +110,11 @@ Run the Mempool backend: | ||||
| 
 | ||||
| ``` | ||||
| npm run start | ||||
| 
 | ||||
| ``` | ||||
| You can also set env var `MEMPOOL_CONFIG_FILE` to specify a custom config file location: | ||||
| ``` | ||||
| MEMPOOL_CONFIG_FILE=/path/to/mempool-config.json npm run start | ||||
| ``` | ||||
| 
 | ||||
| When it's running, you should see output like this: | ||||
|  | ||||
| @ -22,7 +22,10 @@ | ||||
|   "main": "index.ts", | ||||
|   "scripts": { | ||||
|     "tsc": "./node_modules/typescript/bin/tsc -p tsconfig.build.json", | ||||
|     "build": "npm run tsc", | ||||
|     "build": "npm run tsc && npm run create-resources", | ||||
|     "create-resources": "cp ./src/tasks/price-feeds/mtgox-weekly.json ./dist/tasks && node dist/api/fetch-version.js", | ||||
|     "package": "npm run build && rm -rf package && mv dist package && mv node_modules package && npm run package-rm-build-deps", | ||||
|     "package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint)", | ||||
|     "start": "node --max-old-space-size=2048 dist/index.js", | ||||
|     "start-production": "node --max-old-space-size=4096 dist/index.js", | ||||
|     "test": "./node_modules/.bin/jest --coverage", | ||||
|  | ||||
| @ -1,60 +1,37 @@ | ||||
| import * as fs from 'fs'; | ||||
| import * as os from 'os'; | ||||
| import logger from '../logger'; | ||||
| import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| import os from 'os'; | ||||
| import { IBackendInfo } from '../mempool.interfaces'; | ||||
| const { spawnSync } = require('child_process'); | ||||
| 
 | ||||
| class BackendInfo { | ||||
|   private gitCommitHash = ''; | ||||
|   private hostname = ''; | ||||
|   private version = ''; | ||||
|   private backendInfo: IBackendInfo; | ||||
| 
 | ||||
|   constructor() { | ||||
|     this.setLatestCommitHash(); | ||||
|     this.setVersion(); | ||||
|     this.hostname = os.hostname(); | ||||
|   } | ||||
| 
 | ||||
|   public getBackendInfo(): IBackendInfo { | ||||
|     return { | ||||
|       hostname: this.hostname, | ||||
|       gitCommit: this.gitCommitHash, | ||||
|       version: this.version, | ||||
|     // This file is created by ./fetch-version.ts during building
 | ||||
|     const versionFile = path.join(__dirname, 'version.json') | ||||
|     var versionInfo; | ||||
|     if (fs.existsSync(versionFile)) { | ||||
|       versionInfo = JSON.parse(fs.readFileSync(versionFile).toString()); | ||||
|     } else { | ||||
|       // Use dummy values if `versionFile` doesn't exist (e.g., during testing)
 | ||||
|       versionInfo = { | ||||
|         version: '?', | ||||
|         gitCommit: '?' | ||||
|       }; | ||||
|     } | ||||
|     this.backendInfo = { | ||||
|       hostname: os.hostname(), | ||||
|       version: versionInfo.version, | ||||
|       gitCommit: versionInfo.gitCommit | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   public getBackendInfo(): IBackendInfo { | ||||
|     return this.backendInfo; | ||||
|   } | ||||
| 
 | ||||
|   public getShortCommitHash() { | ||||
|     return this.gitCommitHash.slice(0, 7); | ||||
|   } | ||||
| 
 | ||||
|   private setLatestCommitHash(): void { | ||||
|     //TODO: share this logic with `generate-config.js`
 | ||||
|     if (process.env.DOCKER_COMMIT_HASH) { | ||||
|       this.gitCommitHash = process.env.DOCKER_COMMIT_HASH; | ||||
|     } else { | ||||
|       try { | ||||
|         const gitRevParse = spawnSync('git', ['rev-parse', '--short', 'HEAD']); | ||||
|         if (!gitRevParse.error) { | ||||
|           const output = gitRevParse.stdout.toString('utf-8').replace(/[\n\r\s]+$/, ''); | ||||
|           this.gitCommitHash = output ? output : '?'; | ||||
|         } else if (gitRevParse.error.code === 'ENOENT') { | ||||
|           console.log('git not found, cannot parse git hash'); | ||||
|           this.gitCommitHash = '?'; | ||||
|         } | ||||
|       } catch (e: any) { | ||||
|         console.log('Could not load git commit info: ' + e.message); | ||||
|         this.gitCommitHash = '?'; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private setVersion(): void { | ||||
|     try { | ||||
|       const packageJson = fs.readFileSync('package.json').toString(); | ||||
|       this.version = JSON.parse(packageJson).version; | ||||
|     } catch (e) { | ||||
|       throw new Error(e instanceof Error ? e.message : 'Error'); | ||||
|     } | ||||
|     return this.backendInfo.gitCommit.slice(0, 7); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -39,7 +39,8 @@ class ChannelsApi { | ||||
|         FROM channels | ||||
|         JOIN nodes AS nodes_1 on nodes_1.public_key = channels.node1_public_key | ||||
|         JOIN nodes AS nodes_2 on nodes_2.public_key = channels.node2_public_key | ||||
|         WHERE nodes_1.latitude IS NOT NULL AND nodes_1.longitude IS NOT NULL | ||||
|         WHERE channels.status = 1 | ||||
|           AND nodes_1.latitude IS NOT NULL AND nodes_1.longitude IS NOT NULL | ||||
|           AND nodes_2.latitude IS NOT NULL AND nodes_2.longitude IS NOT NULL | ||||
|       `;
 | ||||
| 
 | ||||
| @ -374,6 +375,7 @@ class ChannelsApi { | ||||
|       'transaction_vout': channel.transaction_vout, | ||||
|       'closing_transaction_id': channel.closing_transaction_id, | ||||
|       'closing_reason': channel.closing_reason, | ||||
|       'closing_date': channel.closing_date, | ||||
|       'updated_at': channel.updated_at, | ||||
|       'created': channel.created, | ||||
|       'status': channel.status, | ||||
|  | ||||
| @ -5,6 +5,49 @@ import { ILightningApi } from '../lightning/lightning-api.interface'; | ||||
| import { ITopNodesPerCapacity, ITopNodesPerChannels } from '../../mempool.interfaces'; | ||||
| 
 | ||||
| class NodesApi { | ||||
|   public async $getWorldNodes(): Promise<any> { | ||||
|     try { | ||||
|       let query = ` | ||||
|         SELECT nodes.public_key as publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, | ||||
|         CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, | ||||
|         CAST(COALESCE(nodes.channels, 0) as INT) as channels, | ||||
|         nodes.longitude, nodes.latitude, | ||||
|         geo_names_country.names as country, geo_names_iso.names as isoCode | ||||
|         FROM nodes | ||||
|         LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' | ||||
|         LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code' | ||||
|         WHERE status = 1 AND nodes.as_number IS NOT NULL | ||||
|         ORDER BY capacity | ||||
|       `;
 | ||||
| 
 | ||||
|       const [nodes]: any[] = await DB.query(query); | ||||
| 
 | ||||
|       for (let i = 0; i < nodes.length; ++i) { | ||||
|         nodes[i].country = JSON.parse(nodes[i].country); | ||||
|       } | ||||
| 
 | ||||
|       query = ` | ||||
|         SELECT MAX(nodes.capacity) as maxLiquidity, MAX(nodes.channels) as maxChannels | ||||
|         FROM nodes | ||||
|         WHERE status = 1 AND nodes.as_number IS NOT NULL | ||||
|       `;
 | ||||
| 
 | ||||
|       const [maximums]: any[] = await DB.query(query); | ||||
|        | ||||
|       return { | ||||
|         maxLiquidity: maximums[0].maxLiquidity, | ||||
|         maxChannels: maximums[0].maxChannels, | ||||
|         nodes: nodes.map(node => [ | ||||
|           node.longitude, node.latitude, | ||||
|           node.publicKey, node.alias, node.capacity, node.channels, | ||||
|           node.country, node.isoCode | ||||
|         ]) | ||||
|       }; | ||||
|     } catch (e) { | ||||
|       logger.err(`Can't get world nodes list. Reason: ${e instanceof Error ? e.message : e}`); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async $getNode(public_key: string): Promise<any> { | ||||
|     try { | ||||
|       // General info
 | ||||
| @ -133,10 +176,13 @@ class NodesApi { | ||||
|             CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, | ||||
|             CAST(COALESCE(nodes.channels, 0) as INT) as channels, | ||||
|             UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt, | ||||
|             geo_names_city.names as city, geo_names_country.names as country | ||||
|             geo_names_city.names as city, geo_names_country.names as country, | ||||
|             geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision | ||||
|           FROM nodes | ||||
|           LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' | ||||
|           LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city' | ||||
|           LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code' | ||||
|           LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division' | ||||
|           ORDER BY capacity DESC | ||||
|           LIMIT 100 | ||||
|         `;
 | ||||
| @ -175,10 +221,13 @@ class NodesApi { | ||||
|             CAST(COALESCE(nodes.channels, 0) as INT) as channels, | ||||
|             CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, | ||||
|             UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt, | ||||
|             geo_names_city.names as city, geo_names_country.names as country | ||||
|             geo_names_city.names as city, geo_names_country.names as country, | ||||
|             geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision | ||||
|           FROM nodes | ||||
|           LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' | ||||
|           LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city' | ||||
|           LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code' | ||||
|           LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division' | ||||
|           ORDER BY channels DESC | ||||
|           LIMIT 100 | ||||
|         `;
 | ||||
| @ -221,11 +270,14 @@ class NodesApi { | ||||
|             CAST(COALESCE(node_stats.channels, 0) as INT) as channels, | ||||
|             CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, | ||||
|             UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt, | ||||
|             geo_names_city.names as city, geo_names_country.names as country | ||||
|             geo_names_city.names as city, geo_names_country.names as country, | ||||
|             geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision | ||||
|           FROM node_stats | ||||
|           RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key | ||||
|           LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' | ||||
|           LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city' | ||||
|           LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code' | ||||
|           LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division' | ||||
|           WHERE added = FROM_UNIXTIME(${latestDate}) | ||||
|           ORDER BY first_seen | ||||
|           LIMIT 100 | ||||
| @ -382,12 +434,14 @@ class NodesApi { | ||||
|         SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels, | ||||
|           nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at, | ||||
|           geo_names_city.names as city, geo_names_country.names as country, | ||||
|           geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision | ||||
|           geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision, | ||||
|           nodes.longitude, nodes.latitude, nodes.as_number, geo_names_isp.names as isp | ||||
|         FROM nodes | ||||
|         LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' | ||||
|         LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city' | ||||
|         LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code' | ||||
|         LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division' | ||||
|         LEFT JOIN geo_names geo_names_isp on geo_names_isp.id = nodes.as_number AND geo_names_isp.type = 'as_organization' | ||||
|         WHERE geo_names_country.id = ? | ||||
|         ORDER BY capacity DESC | ||||
|       `;
 | ||||
| @ -397,6 +451,7 @@ class NodesApi { | ||||
|         rows[i].country = JSON.parse(rows[i].country); | ||||
|         rows[i].city = JSON.parse(rows[i].city); | ||||
|         rows[i].subdivision = JSON.parse(rows[i].subdivision); | ||||
|         rows[i].isp = JSON.parse(rows[i].isp); | ||||
|       } | ||||
|       return rows; | ||||
|     } catch (e) { | ||||
| @ -411,7 +466,8 @@ class NodesApi { | ||||
|         SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels, | ||||
|           nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at, | ||||
|           geo_names_city.names as city, geo_names_country.names as country, | ||||
|           geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision | ||||
|           geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision, | ||||
|           nodes.longitude, nodes.latitude | ||||
|         FROM nodes | ||||
|         LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' | ||||
|         LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city' | ||||
|  | ||||
| @ -9,6 +9,7 @@ class NodesRoutes { | ||||
| 
 | ||||
|   public initRoutes(app: Application) { | ||||
|     app | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/world', this.$getWorldNodes) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/country/:country', this.$getNodesPerCountry) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp-ranking', this.$getISPRanking) | ||||
| @ -115,7 +116,6 @@ class NodesRoutes { | ||||
|   private async $getISPRanking(req: Request, res: Response): Promise<void> { | ||||
|     try { | ||||
|       const nodesPerAs = await nodesApi.$getNodesISPRanking(); | ||||
| 
 | ||||
|       res.header('Pragma', 'public'); | ||||
|       res.header('Cache-control', 'public'); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); | ||||
| @ -125,6 +125,18 @@ class NodesRoutes { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $getWorldNodes(req: Request, res: Response) { | ||||
|     try { | ||||
|       const worldNodes = await nodesApi.$getWorldNodes(); | ||||
|       res.header('Pragma', 'public'); | ||||
|       res.header('Cache-control', 'public'); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); | ||||
|       res.json(worldNodes); | ||||
|     } catch (e) { | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $getNodesPerCountry(req: Request, res: Response) { | ||||
|     try { | ||||
|       const [country]: any[] = await DB.query( | ||||
|  | ||||
							
								
								
									
										37
									
								
								backend/src/api/fetch-version.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								backend/src/api/fetch-version.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| import fs from 'fs'; | ||||
| import path from "path"; | ||||
| const { spawnSync } = require('child_process'); | ||||
| 
 | ||||
| function getVersion(): string { | ||||
|   const packageJson = fs.readFileSync('package.json').toString(); | ||||
|   return JSON.parse(packageJson).version; | ||||
| } | ||||
| 
 | ||||
| function getGitCommit(): string { | ||||
|   if (process.env.MEMPOOL_COMMIT_HASH) { | ||||
|     return process.env.MEMPOOL_COMMIT_HASH; | ||||
|   } else { | ||||
|     const gitRevParse = spawnSync('git', ['rev-parse', '--short', 'HEAD']); | ||||
|     if (!gitRevParse.error) { | ||||
|       const output = gitRevParse.stdout.toString('utf-8').replace(/[\n\r\s]+$/, ''); | ||||
|       if (output) { | ||||
|         return output; | ||||
|       } else { | ||||
|         console.log('Could not fetch git commit: No repo available'); | ||||
|       } | ||||
|     } else if (gitRevParse.error.code === 'ENOENT') { | ||||
|       console.log('Could not fetch git commit: Command `git` is unavailable'); | ||||
|     } | ||||
|   } | ||||
|   return '?'; | ||||
| } | ||||
| 
 | ||||
| const versionInfo = { | ||||
|   version: getVersion(), | ||||
|   gitCommit: getGitCommit() | ||||
| } | ||||
| 
 | ||||
| fs.writeFileSync( | ||||
|   path.join(__dirname, 'version.json'), | ||||
|   JSON.stringify(versionInfo, null, 2) + "\n" | ||||
| ); | ||||
| @ -1,4 +1,6 @@ | ||||
| const configFile = require('../mempool-config.json'); | ||||
| const configFromFile = require( | ||||
|     process.env.MEMPOOL_CONFIG_FILE ? process.env.MEMPOOL_CONFIG_FILE : '../mempool-config.json' | ||||
| ); | ||||
| 
 | ||||
| interface IConfig { | ||||
|   MEMPOOL: { | ||||
| @ -249,7 +251,7 @@ class Config implements IConfig { | ||||
|   MAXMIND: IConfig['MAXMIND']; | ||||
| 
 | ||||
|   constructor() { | ||||
|     const configs = this.merge(configFile, defaults); | ||||
|     const configs = this.merge(configFromFile, defaults); | ||||
|     this.MEMPOOL = configs.MEMPOOL; | ||||
|     this.ESPLORA = configs.ESPLORA; | ||||
|     this.ELECTRUM = configs.ELECTRUM; | ||||
|  | ||||
| @ -12,9 +12,11 @@ import { ResultSetHeader } from 'mysql2'; | ||||
| import fundingTxFetcher from './sync-tasks/funding-tx-fetcher'; | ||||
| import NodesSocketsRepository from '../../repositories/NodesSocketsRepository'; | ||||
| import { Common } from '../../api/common'; | ||||
| import blocks from '../../api/blocks'; | ||||
| 
 | ||||
| class NetworkSyncService { | ||||
|   loggerTimer = 0; | ||||
|   closedChannelsScanBlock = 0; | ||||
| 
 | ||||
|   constructor() {} | ||||
| 
 | ||||
| @ -240,10 +242,22 @@ class NetworkSyncService { | ||||
|   } | ||||
| 
 | ||||
|   private async $scanForClosedChannels(): Promise<void> { | ||||
|     if (this.closedChannelsScanBlock === blocks.getCurrentBlockHeight()) { | ||||
|       logger.debug(`We've already scan closed channels for this block, skipping.`); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     let progress = 0; | ||||
| 
 | ||||
|     try { | ||||
|       logger.info(`Starting closed channels scan`); | ||||
|       let log = `Starting closed channels scan`; | ||||
|       if (this.closedChannelsScanBlock > 0) { | ||||
|         log += `. Last scan was at block ${this.closedChannelsScanBlock}`; | ||||
|       } else { | ||||
|         log += ` for the first time`; | ||||
|       } | ||||
|       logger.info(log); | ||||
| 
 | ||||
|       const channels = await channelsApi.$getChannelsByStatus([0, 1]); | ||||
|       for (const channel of channels) { | ||||
|         const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout); | ||||
| @ -263,7 +277,9 @@ class NetworkSyncService { | ||||
|           this.loggerTimer = new Date().getTime() / 1000; | ||||
|         } | ||||
|       } | ||||
|       logger.info(`Closed channels scan complete.`); | ||||
| 
 | ||||
|       this.closedChannelsScanBlock = blocks.getCurrentBlockHeight(); | ||||
|       logger.info(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`); | ||||
|     } catch (e) { | ||||
|       logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e)); | ||||
|     } | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import * as fs from 'fs'; | ||||
| import path from "path"; | ||||
| import { Common } from '../api/common'; | ||||
| import config from '../config'; | ||||
| import logger from '../logger'; | ||||
| @ -159,7 +160,7 @@ class PriceUpdater { | ||||
|     const existingPriceTimes = await PricesRepository.$getPricesTimes(); | ||||
| 
 | ||||
|     // Insert MtGox weekly prices
 | ||||
|     const pricesJson: any[] = JSON.parse(fs.readFileSync('./src/tasks/price-feeds/mtgox-weekly.json').toString()); | ||||
|     const pricesJson: any[] = JSON.parse(fs.readFileSync(path.join(__dirname, 'mtgox-weekly.json')).toString()); | ||||
|     const prices = this.getEmptyPricesObj(); | ||||
|     let insertedCount: number = 0; | ||||
|     for (const price of pricesJson) { | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| FROM node:16.16.0-buster-slim AS builder | ||||
| 
 | ||||
| ARG commitHash | ||||
| ENV DOCKER_COMMIT_HASH=${commitHash} | ||||
| ENV MEMPOOL_COMMIT_HASH=${commitHash} | ||||
| 
 | ||||
| WORKDIR /build | ||||
| COPY . . | ||||
| @ -9,18 +9,15 @@ COPY . . | ||||
| RUN apt-get update | ||||
| RUN apt-get install -y build-essential python3 pkg-config | ||||
| RUN npm install --omit=dev --omit=optional | ||||
| RUN npm run build | ||||
| RUN npm run package | ||||
| 
 | ||||
| FROM node:16.16.0-buster-slim | ||||
| 
 | ||||
| WORKDIR /backend | ||||
| 
 | ||||
| COPY --from=builder /build/ . | ||||
| 
 | ||||
| RUN chmod +x /backend/start.sh | ||||
| RUN chmod +x /backend/wait-for-it.sh | ||||
| 
 | ||||
| RUN chown -R 1000:1000 /backend && chmod -R 755 /backend | ||||
| RUN chown 1000:1000 ./ | ||||
| COPY --from=builder --chown=1000:1000 /build/package ./package/ | ||||
| COPY --from=builder --chown=1000:1000 /build/mempool-config.json /build/start.sh /build/wait-for-it.sh ./ | ||||
| 
 | ||||
| USER 1000 | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										2
									
								
								docker/backend/start.sh
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										2
									
								
								docker/backend/start.sh
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @ -205,4 +205,4 @@ sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json | ||||
| # CLN | ||||
| sed -i "s!__CLN_SOCKET__!${__CLN_SOCKET__}!g" mempool-config.json | ||||
| 
 | ||||
| node /backend/dist/index.js | ||||
| node /backend/package/index.js | ||||
|  | ||||
							
								
								
									
										0
									
								
								docker/backend/wait-for-it.sh
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								docker/backend/wait-for-it.sh
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @ -1,10 +1,7 @@ | ||||
| #!/bin/sh | ||||
| 
 | ||||
| #backend | ||||
| gitMaster="\.\.\/\.git\/refs\/heads\/master" | ||||
| git ls-remote https://github.com/mempool/mempool.git "$1^{}" | awk '{ print $1}' > ./backend/master | ||||
| cp ./docker/backend/* ./backend/ | ||||
| sed -i "s/${gitMaster}/master/g" ./backend/src/api/backend-info.ts | ||||
| 
 | ||||
| #frontend | ||||
| localhostIP="127.0.0.1" | ||||
|  | ||||
| @ -170,6 +170,10 @@ | ||||
|           }, | ||||
|           "configurations": { | ||||
|             "production": { | ||||
|               "assets": [ | ||||
|                 "src/favicon.ico", | ||||
|                 "src/robots.txt" | ||||
|               ], | ||||
|               "fileReplacements": [ | ||||
|                 { | ||||
|                   "replace": "src/environments/environment.ts", | ||||
|  | ||||
| @ -34,7 +34,7 @@ | ||||
|     "start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging", | ||||
|     "start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed", | ||||
|     "build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js", | ||||
|     "sync-assets": "node sync-assets.js && rsync -av ./dist/mempool/browser/en-US/resources ./dist/mempool/browser/resources", | ||||
|     "sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js", | ||||
|     "sync-assets-dev": "node sync-assets.js dev", | ||||
|     "generate-config": "node generate-config.js", | ||||
|     "build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js && npm run build-mempool-bisq-js", | ||||
|  | ||||
| @ -187,8 +187,8 @@ | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="selfhosted-integrations-sponsor"> | ||||
|     <h3 i18n="about.self-hosted-integrations">Self-Hosted Integrations</h3> | ||||
|   <div class="community-integrations-sponsor"> | ||||
|     <h3 i18n="about.community-integrations">Community Integrations</h3> | ||||
|     <div class="wrapper"> | ||||
|       <a href="https://github.com/getumbrel/umbrel" target="_blank" title="Umbrel"> | ||||
|         <img class="image" src="/resources/profile/umbrel.png" /> | ||||
| @ -218,18 +218,24 @@ | ||||
|         <img class="image" src="/resources/profile/start9.png" /> | ||||
|         <span>EmbassyOS</span> | ||||
|       </a> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="community-integrations-sponsor"> | ||||
|     <h3 i18n="about.wallet-integrations">Wallet Integrations</h3> | ||||
|     <div class="wrapper"> | ||||
|       <a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server"> | ||||
|         <img class="image" src="/resources/profile/btcpayserver.svg" /> | ||||
|         <span>BTCPay</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq"> | ||||
|         <img class="image" src="/resources/profile/bisq_network.png" /> | ||||
|         <span>Bisq</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet"> | ||||
|         <img class="image" src="/resources/profile/bluewallet.png" /> | ||||
|         <span>BlueWallet</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet"> | ||||
|         <img class="image" src="/resources/profile/muun.png" /> | ||||
|         <span>Muun</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet"> | ||||
|         <img class="image" src="/resources/profile/electrum.jpg" /> | ||||
|         <img class="image" src="/resources/profile/electrum.png" /> | ||||
|         <span>Electrum</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet"> | ||||
| @ -244,18 +250,14 @@ | ||||
|         <img class="image" src="/resources/profile/phoenix.jpg" /> | ||||
|         <span>Phoenix</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits"> | ||||
|         <img class="image" src="/resources/profile/lnbits.svg" /> | ||||
|         <span>LNBits</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/layer2tech/mercury-wallet" target="_blank" title="Mercury Wallet"> | ||||
|         <img class="image" src="/resources/profile/mercury.svg" /> | ||||
|         <span>Mercury</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet"> | ||||
|         <img class="image" src="/resources/profile/muun.png" /> | ||||
|         <span>Muun</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet"> | ||||
|         <img class="image" src="/resources/profile/bluewallet.png" /> | ||||
|         <span>BlueWallet</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/hsjoberg/blixt-wallet" target="_blank" title="Blixt Wallet"> | ||||
|         <img class="image" src="/resources/profile/blixt.png" /> | ||||
|         <span>Blixt</span> | ||||
|  | ||||
| @ -43,7 +43,6 @@ | ||||
|   .alliances, | ||||
|   .enterprise-sponsor, | ||||
|   .community-integrations-sponsor, | ||||
|   .selfhosted-integrations-sponsor, | ||||
|   .maintainers { | ||||
|     margin-top: 68px; | ||||
|     margin-bottom: 68px; | ||||
| @ -117,7 +116,6 @@ | ||||
|   .community-sponsor, | ||||
|   .project-translators, | ||||
|   .community-integrations-sponsor, | ||||
|   .selfhosted-integrations-sponsor, | ||||
|   .maintainers { | ||||
|     .wrapper { | ||||
|       display: inline-block; | ||||
| @ -193,6 +191,6 @@ | ||||
| } | ||||
| 
 | ||||
| .community-integrations-sponsor { | ||||
|   max-width: 830px; | ||||
|   max-width: 970px; | ||||
|   margin: auto; | ||||
| } | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| <div class="box preview-box" *ngIf="address && !error"> | ||||
|   <h2 class="preview-header" i18n="shared.address">Address</h2> | ||||
|   <div class="row"> | ||||
|     <div class="col-md"> | ||||
|       <div class="title-address"> | ||||
|         <h1 i18n="shared.address">Address</h1> | ||||
|       <div class="row d-flex justify-content-between"> | ||||
|         <div class="title-wrapper"> | ||||
|           <h1 class="title truncated"><span class="first">{{addressString.slice(0,-4)}}</span><span class="last-four">{{addressString.slice(-4)}}</span></h1> | ||||
|         </div> | ||||
|       </div> | ||||
|       <a [routerLink]="['/address/' | relativeUrl, addressString]" class="address-link" > | ||||
|         <span class="truncated-address">{{addressString.slice(0,-4)}}</span><span class="last-four">{{addressString.slice(-4)}}</span> | ||||
|       </a> | ||||
|       <table class="table table-borderless table-striped"> | ||||
|         <tbody> | ||||
|           <tr *ngIf="addressInfo && addressInfo.unconfidential"> | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| h1 { | ||||
|   font-size: 52px; | ||||
|   margin: 0; | ||||
| .title-wrapper { | ||||
|   padding: 0 15px; | ||||
| } | ||||
| 
 | ||||
| .qr-wrapper { | ||||
| @ -23,27 +22,9 @@ h1 { | ||||
| 
 | ||||
| .table { | ||||
|   font-size: 32px; | ||||
|   margin-top: 48px; | ||||
| 
 | ||||
|   ::ng-deep .symbol { | ||||
|     font-size: 24px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .address-link { | ||||
|   font-size: 24px; | ||||
|   margin-bottom: 0.5em; | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   align-items: baseline; | ||||
|   .truncated-address { | ||||
|     text-overflow: ellipsis; | ||||
|     overflow: hidden; | ||||
|     max-width: calc(640px - 4em); | ||||
|     display: inline-block; | ||||
|     white-space: nowrap; | ||||
|   } | ||||
|   .last-four { | ||||
|     display: inline-block; | ||||
|     white-space: nowrap; | ||||
|   } | ||||
| } | ||||
| } | ||||
| @ -4,7 +4,6 @@ import { map } from 'rxjs/operators'; | ||||
| import { moveDec } from 'src/app/bitcoin.utils'; | ||||
| import { AssetsService } from 'src/app/services/assets.service'; | ||||
| import { ElectrsApiService } from 'src/app/services/electrs-api.service'; | ||||
| import { formatNumber } from '@angular/common'; | ||||
| import { environment } from 'src/environments/environment'; | ||||
| 
 | ||||
| @Component({ | ||||
|  | ||||
| @ -1,19 +1,16 @@ | ||||
| <div class="box preview-box" *ngIf="!error"> | ||||
|   <h2 class="preview-header" i18n="shared.block-title">Block</h2> | ||||
|   <div class="row"> | ||||
|     <div class="col-sm"> | ||||
|       <h1 class="block-title"> | ||||
|         <ng-template [ngIf]="blockHeight === 0"><ng-container i18n="@@2303359202781425764">Genesis</ng-container> | ||||
|           <span class="next-previous-blocks"> | ||||
|             <a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a> | ||||
|           </span> | ||||
|         </ng-template> | ||||
|         <ng-template [ngIf]="blockHeight" i18n="shared.block-title">Block <ng-container *ngTemplateOutlet="blockTemplateContent"></ng-container></ng-template> | ||||
|         <ng-template #blockTemplateContent> | ||||
|           <span class="next-previous-blocks"> | ||||
|             <a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a> | ||||
|           </span> | ||||
|         </ng-template> | ||||
|       </h1> | ||||
|       <div class="row d-flex justify-content-between"> | ||||
|         <div class="title-wrapper"> | ||||
|           <h1 class="title"> | ||||
|             <ng-template [ngIf]="blockHeight === 0"><ng-container i18n="@@2303359202781425764">Genesis</ng-container></ng-template> | ||||
|             <ng-template [ngIf]="blockHeight" i18n="shared.block-title">{{ blockHeight }}</ng-template> | ||||
|           </h1> | ||||
|         </div> | ||||
|       </div> | ||||
|       <a class="subtitle truncated" [routerLink]="['/block/' | relativeUrl, blockHash]"><span class="first">{{blockHash.slice(0,-4)}}</span><span class="last-four">{{blockHash.slice(-4)}}</span></a> | ||||
|       <table class="table table-borderless table-striped"> | ||||
|         <tbody> | ||||
|           <!-- <tr> | ||||
|  | ||||
| @ -1,14 +1,10 @@ | ||||
| .block-title { | ||||
|   margin-bottom: 48px; | ||||
|   font-size: 52px; | ||||
| 
 | ||||
|   ::ng-deep .next-previous-blocks { | ||||
|     font-size: 52px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .table { | ||||
|   font-size: 32px; | ||||
|   margin-top: 6px; | ||||
| } | ||||
| 
 | ||||
| .title-wrapper { | ||||
|   padding-left: 15px; | ||||
| } | ||||
| 
 | ||||
| .chart-container { | ||||
|  | ||||
| @ -7,12 +7,12 @@ | ||||
|     </span> | ||||
| 
 | ||||
|     <div [ngSwitch]="network.val"> | ||||
|       <span *ngSwitchCase="'signet'" class="network signet"><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Signet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span> | ||||
|       <span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span> | ||||
|       <span *ngSwitchCase="'signet'" class="network signet"><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Signet</span> | ||||
|       <span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span> | ||||
|       <span *ngSwitchCase="'bisq'" class="network bisq"><app-svg-images name="bisq" width="35" height="35" viewBox="0 0 75 75" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Bisq</span> | ||||
|       <span *ngSwitchCase="'liquid'" class="network liquid"><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Liquid</span> | ||||
|       <span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</span> | ||||
|       <span *ngSwitchDefault class="network mainnet"><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span> | ||||
|       <span *ngSwitchDefault class="network mainnet"><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span> | ||||
|     </div> | ||||
|   </header> | ||||
|   <router-outlet></router-outlet> | ||||
|  | ||||
| @ -33,4 +33,66 @@ | ||||
|     justify-content: flex-start; | ||||
|     align-items: center; | ||||
|   } | ||||
| 
 | ||||
|   ::ng-deep .preview-header { | ||||
|     position: absolute; | ||||
|     top: -80px; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     width: 100%; | ||||
|     padding: 0 220px; | ||||
|     text-align: center; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     z-index: 101; | ||||
|     line-height: 80px; | ||||
|     text-transform: capitalize; | ||||
|   } | ||||
| 
 | ||||
|   ::ng-deep .title { | ||||
|     font-size: 52px; | ||||
|   } | ||||
|   ::ng-deep .subtitle { | ||||
|     font-size: 28px; | ||||
|   } | ||||
| 
 | ||||
|   ::ng-deep .title, ::ng-deep .subtitle { | ||||
|     max-width: 100%; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     white-space: nowrap; | ||||
|     margin: 0; | ||||
|     display: inline-block; | ||||
| 
 | ||||
|     &.truncated { | ||||
|       text-overflow: unset; | ||||
|       display: flex; | ||||
|       flex-direction: row; | ||||
|       align-items: baseline; | ||||
| 
 | ||||
|       .first { | ||||
|         flex-grow: 1; | ||||
|         flex-shrink: 1; | ||||
|         overflow: hidden; | ||||
|         text-overflow: ellipsis; | ||||
|         margin-right: -2px; | ||||
|       } | ||||
| 
 | ||||
|       .last-four { | ||||
|         flex-shrink: 0; | ||||
|         flex-grow: 0; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ::ng-deep .title-wrapper { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: flex-start; | ||||
|     justify-content: flex-start; | ||||
|     margin: 0; | ||||
|     width: 0; | ||||
|     flex-grow: 1; | ||||
|     flex-shrink: 1; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,10 +1,9 @@ | ||||
| <div class="box preview-box" *ngIf="tx && !error"> | ||||
| 
 | ||||
|   <div class="page-title"> | ||||
|     <h1 i18n="shared.transaction">Transaction</h1> | ||||
|     <a class="tx-link" [routerLink]="['/tx/' | relativeUrl, txId]"> | ||||
|       <span class="truncated">{{txId.slice(0,-4)}}</span><span class="last-four">{{txId.slice(-4)}}</span> | ||||
|     </a> | ||||
|   <h2 class="preview-header" i18n="shared.transaction">Transaction</h2> | ||||
|   <div class="row d-flex justify-content-between full-width-row"> | ||||
|     <div class="title-wrapper"> | ||||
|       <h1 class="title truncated"><span class="first">{{txId.slice(0,-4)}}</span><span class="last-four">{{txId.slice(-4)}}</span></h1> | ||||
|     </div> | ||||
|     <div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features"> | ||||
|       <app-tx-features [tx]="tx"></app-tx-features> | ||||
|       <span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1"> | ||||
| @ -15,7 +14,6 @@ | ||||
|       </span> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="top-data row"> | ||||
|     <span class="field col-sm-4 text-left"> | ||||
|       <ng-template [ngIf]="isLiquid && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template> | ||||
|  | ||||
| @ -26,56 +26,9 @@ | ||||
| 	margin-top: 0px; | ||||
| } | ||||
| 
 | ||||
| .page-title { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   justify-content: space-between; | ||||
|   align-items: baseline; | ||||
|   margin-bottom: 2px; | ||||
|   max-width: 100%; | ||||
| 
 | ||||
|   h1 { | ||||
|     font-size: 52px; | ||||
|     margin: 0; | ||||
|     line-height: 1; | ||||
|   } | ||||
| 
 | ||||
|   .features { | ||||
|     font-size: 24px; | ||||
|   } | ||||
| 
 | ||||
|   & > * { | ||||
|     flex-grow: 0; | ||||
|     flex-shrink: 0; | ||||
|   } | ||||
| 
 | ||||
|   .tx-link { | ||||
|     flex-grow: 1; | ||||
|     flex-shrink: 1; | ||||
|     margin: 0 1em; | ||||
|     overflow: hidden; | ||||
|     white-space: nowrap; | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     align-items: baseline; | ||||
| 
 | ||||
|     .truncated { | ||||
|       flex-grow: 1; | ||||
|       flex-shrink: 1; | ||||
|       overflow: hidden; | ||||
|       text-overflow: ellipsis; | ||||
|       margin-right: -2px; | ||||
|     } | ||||
| 
 | ||||
|     .last-four { | ||||
|       flex-shrink: 0; | ||||
|       flex-grow: 0; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .features { | ||||
|     align-self: center; | ||||
|   } | ||||
| .features { | ||||
|   font-size: 24px; | ||||
|   margin-left: 1em; | ||||
| } | ||||
| 
 | ||||
| .top-data { | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -161,6 +161,9 @@ export interface ITopNodesPerChannels { | ||||
|   updatedAt?: number, | ||||
|   city?: any, | ||||
|   country?: any, | ||||
|   subdivision?: any, | ||||
|   iso_code?: string, | ||||
|   geolocation?: any; | ||||
| } | ||||
| 
 | ||||
| export interface ITopNodesPerCapacity { | ||||
| @ -172,6 +175,9 @@ export interface ITopNodesPerCapacity { | ||||
|   updatedAt?: number, | ||||
|   city?: any, | ||||
|   country?: any, | ||||
|   subdivision?: any, | ||||
|   iso_code?: string, | ||||
|   geolocation?: any; | ||||
| } | ||||
| 
 | ||||
| export interface INodesRanking { | ||||
| @ -188,6 +194,9 @@ export interface IOldestNodes { | ||||
|   updatedAt?: number, | ||||
|   city?: any, | ||||
|   country?: any, | ||||
|   subdivision?: any, | ||||
|   iso_code?: string, | ||||
|   geolocation?: any; | ||||
| } | ||||
| 
 | ||||
| export interface IChannel { | ||||
|  | ||||
| @ -19,31 +19,31 @@ | ||||
|         <tr> | ||||
|           <td i18n="address.total-sent">Fee rate</td> | ||||
|           <td> | ||||
|             {{ channel.fee_rate }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span> | ||||
|             {{ channel.fee_rate ?? '-' }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span> | ||||
|           </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td i18n="address.total-sent">Base fee</td> | ||||
|           <td> | ||||
|             <app-sats [satoshis]="channel.base_fee_mtokens / 1000" digitsInfo="1.0-2"></app-sats> | ||||
|             <app-sats [valueOverride]="!channel.base_fee_mtokens ? '- ' : undefined" [satoshis]="channel.base_fee_mtokens / 1000" digitsInfo="1.0-2"></app-sats> | ||||
|           </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td i18n="address.total-sent">Min HTLC</td> | ||||
|           <td> | ||||
|             <app-sats [satoshis]="channel.min_htlc_mtokens / 1000"></app-sats> | ||||
|             <app-sats [valueOverride]="!channel.min_htlc_mtokens ? '- ' : undefined" [satoshis]="channel.min_htlc_mtokens / 1000"></app-sats> | ||||
|           </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td i18n="address.total-sent">Max HTLC</td> | ||||
|           <td> | ||||
|             <app-sats [satoshis]="channel.max_htlc_mtokens / 1000"></app-sats> | ||||
|             <app-sats [valueOverride]="!channel.max_htlc_mtokens ? '- ' : undefined" [satoshis]="channel.max_htlc_mtokens / 1000"></app-sats> | ||||
|           </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <td i18n="address.total-sent">Timelock delta</td> | ||||
|           <td> | ||||
|             <ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: channel.cltv_delta }"></ng-container> | ||||
|             <ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: channel.cltv_delta ?? '-' }"></ng-container> | ||||
|           </td> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| <div class="box preview-box" *ngIf="(channel$ | async) as channel"> | ||||
|   <h2 class="preview-header" i18n="lightning.channel">lightning channel</h2> | ||||
|   <div class="row d-flex justify-content-between full-width-row"> | ||||
|     <h1 class="title"> | ||||
|       <span i18n="lightning.channel">Channel</span>:  | ||||
|       <a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]"> {{ channel.short_id }}</a> | ||||
|     </h1> | ||||
|     <div class="title-wrapper"> | ||||
|       <h1 class="title">{{ channel.short_id }}</h1> | ||||
|     </div> | ||||
|     <div class="badges mb-2"> | ||||
|       <span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span> | ||||
|       <span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span> | ||||
| @ -12,20 +12,11 @@ | ||||
|       <app-closing-type [type]="channel.closing_reason" *ngIf="channel.status === 2"></app-closing-type> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="row d-flex justify-content-between full-width-row nodes"> | ||||
|     <span class="node left"> | ||||
|       {{ channel.node_left.alias || '?' }} | ||||
|     </span> | ||||
|     <fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true" title="channel between"></fa-icon> | ||||
|     <span class="node right"> | ||||
|       {{ channel.node_right.alias || '?' }} | ||||
|     </span> | ||||
|   </div> | ||||
|   <div class="row"> | ||||
|     <div class="col-md"> | ||||
|       <a class="subtitle" [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.id }}</a> | ||||
|       <table class="table table-borderless table-striped"> | ||||
|         <tbody> | ||||
|           <tr></tr> | ||||
|           <tr> | ||||
|             <td i18n="channel.created">Created</td> | ||||
|             <td>{{ channel.created | date:'yyyy-MM-dd HH:mm' }}</td> | ||||
| @ -61,6 +52,15 @@ | ||||
|       <app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" [placeholder]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="row d-flex justify-content-between full-width-row nodes"> | ||||
|     <span class="node left"> | ||||
|       {{ channel.node_left.alias || '?' }} | ||||
|     </span> | ||||
|     <fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true" title="channel between"></fa-icon> | ||||
|     <span class="node right"> | ||||
|       {{ channel.node_right.alias || '?' }} | ||||
|     </span> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <ng-template [ngIf]="error"> | ||||
|  | ||||
| @ -1,15 +1,17 @@ | ||||
| .title { | ||||
|   font-size: 52px; | ||||
|   margin: 0; | ||||
| } | ||||
| 
 | ||||
| .table { | ||||
|   font-size: 32px; | ||||
|   margin-top: 36px; | ||||
|   margin-top: 10px; | ||||
| } | ||||
| 
 | ||||
| .badges { | ||||
|   font-size: 28px; | ||||
|   flex-shrink: 0; | ||||
|   flex-grow: 0; | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   flex-wrap: nowrap; | ||||
|   align-items: baseline; | ||||
|   justify-content: flex-end; | ||||
| 
 | ||||
|   ::ng-deep .badge { | ||||
|     margin-left: 0.5em; | ||||
| @ -23,11 +25,12 @@ | ||||
| .full-width-row { | ||||
|   padding-left: 15px; | ||||
|   padding-right: 15px; | ||||
|   flex-wrap: nowrap; | ||||
| } | ||||
| 
 | ||||
|   &:nth-child(even) { | ||||
|     background: #181b2d; | ||||
|     margin: 15px 0; | ||||
|   } | ||||
| .row.nodes { | ||||
|   background: #181b2d; | ||||
|   margin: 15px 0 0; | ||||
| } | ||||
| 
 | ||||
| .nodes { | ||||
| @ -46,7 +49,7 @@ | ||||
|   min-width: 470px; | ||||
|   padding: 0; | ||||
|   background: #181b2d; | ||||
|   max-height: 470px; | ||||
|   max-height: 350px; | ||||
|   overflow: hidden; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -16,7 +16,8 @@ | ||||
| 
 | ||||
|   <div class="clearfix"></div> | ||||
| 
 | ||||
|   <app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'" [channel]="channelGeo"></app-nodes-channels-map> | ||||
|   <app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'" | ||||
|     [channel]="channelGeo"></app-nodes-channels-map> | ||||
| 
 | ||||
|   <div class="box"> | ||||
| 
 | ||||
| @ -25,13 +26,17 @@ | ||||
|           <table class="table table-borderless table-striped"> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td i18n="address.total-sent">Created</td> | ||||
|                 <td i18n="lightning.created">Created</td> | ||||
|                 <td><app-timestamp [dateString]="channel.created"></app-timestamp></td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td i18n="address.total-sent">Last update</td> | ||||
|               <tr *ngIf="channel.status !== 2"> | ||||
|                 <td i18n="lightning.last-update">Last update</td> | ||||
|                 <td><app-timestamp [dateString]="channel.updated_at"></app-timestamp></td> | ||||
|               </tr> | ||||
|               <tr *ngIf="channel.status === 2"> | ||||
|                 <td i18n="lightning.closing_date">Closing date</td> | ||||
|                 <td><app-timestamp [dateString]="channel.closing_date"></app-timestamp></td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
| @ -47,38 +52,57 @@ | ||||
|           </table> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|     </div> | ||||
| 
 | ||||
|     <br> | ||||
| 
 | ||||
|     <div class="row row-cols-1 row-cols-md-2"> | ||||
|       <div class="col"> | ||||
|         <app-channel-box [channel]="channel.node_left"></app-channel-box> | ||||
|       <div class="w-100 d-block d-md-none"></div> | ||||
|       <div class="col-md"> | ||||
|         <table class="table table-borderless table-striped"> | ||||
|           <tbody> | ||||
|             <tr> | ||||
|               <td i18n="address.total-received">Capacity</td> | ||||
|               <td> | ||||
|                 <app-sats [satoshis]="channel.capacity"></app-sats> | ||||
|                 <app-fiat [value]="channel.capacity" digitsInfo="1.0-0"></app-fiat> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </div> | ||||
|       <div class="col"> | ||||
|         <app-channel-box [channel]="channel.node_right"></app-channel-box> | ||||
|       </div> | ||||
|     </div> | ||||
|      | ||||
|     <br> | ||||
| 
 | ||||
|     <ng-container *ngIf="transactions$ | async as transactions"> | ||||
|       <ng-template [ngIf]="transactions[0]"> | ||||
|         <div class="d-flex"> | ||||
|           <h3>Opening transaction</h3> | ||||
|           <button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button> | ||||
|         </div> | ||||
|         <app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list> | ||||
|       </ng-template> | ||||
|       <ng-template [ngIf]="transactions[1]"> | ||||
|         <div class="closing-header d-flex"> | ||||
|           <h3 style="margin: 0;">Closing transaction</h3>  <app-closing-type [type]="channel.closing_reason"></app-closing-type> | ||||
|           <button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button> | ||||
|         </div> | ||||
|         <app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list> | ||||
|       </ng-template> | ||||
|     </ng-container> | ||||
|   </div> | ||||
| 
 | ||||
|   <br> | ||||
| 
 | ||||
|   <div class="row row-cols-1 row-cols-md-2"> | ||||
|     <div class="col"> | ||||
|       <app-channel-box [channel]="channel.node_left"></app-channel-box> | ||||
|     </div> | ||||
|     <div class="col"> | ||||
|       <app-channel-box [channel]="channel.node_right"></app-channel-box> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <br> | ||||
| 
 | ||||
|   <ng-container *ngIf="transactions$ | async as transactions"> | ||||
|     <ng-template [ngIf]="transactions[0]"> | ||||
|       <div class="d-flex"> | ||||
|         <h3>Opening transaction</h3> | ||||
|         <button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()" | ||||
|           i18n="transaction.details|Transaction Details">Details</button> | ||||
|       </div> | ||||
|       <app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"> | ||||
|       </app-transactions-list> | ||||
|     </ng-template> | ||||
|     <ng-template [ngIf]="transactions[1]"> | ||||
|       <div class="closing-header d-flex"> | ||||
|         <h3 style="margin: 0;">Closing transaction</h3>  <app-closing-type [type]="channel.closing_reason"> | ||||
|         </app-closing-type> | ||||
|         <button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()" | ||||
|           i18n="transaction.details|Transaction Details">Details</button> | ||||
|       </div> | ||||
|       <app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"> | ||||
|       </app-transactions-list> | ||||
|     </ng-template> | ||||
|   </ng-container> | ||||
| 
 | ||||
| </div> | ||||
| 
 | ||||
| @ -104,7 +128,7 @@ | ||||
|     <div class="badges mb-2"> | ||||
|       <span class="skeleton-loader" style="width: 50px; height: 22px; margin-top: 5px;"></span> | ||||
|     </div> | ||||
|    | ||||
| 
 | ||||
|     <div class="clearfix"></div> | ||||
| 
 | ||||
|     <div style="height: 413px;  padding: 15px;"> | ||||
| @ -148,4 +172,4 @@ | ||||
|     </div> | ||||
| 
 | ||||
|   </div> | ||||
| </ng-template> | ||||
| </ng-template> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | ||||
| import { ActivatedRoute, ParamMap } from '@angular/router'; | ||||
| import { Observable, of, zip } from 'rxjs'; | ||||
| import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; | ||||
| import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators'; | ||||
| import { IChannel } from 'src/app/interfaces/node-api.interface'; | ||||
| import { ElectrsApiService } from 'src/app/services/electrs-api.service'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| @ -31,9 +31,11 @@ export class ChannelComponent implements OnInit { | ||||
|       .pipe( | ||||
|         switchMap((params: ParamMap) => { | ||||
|           this.error = null; | ||||
|           this.seoService.setTitle(`Channel: ${params.get('short_id')}`); | ||||
|           return this.lightningApiService.getChannel$(params.get('short_id')) | ||||
|             .pipe( | ||||
|               tap((value) => { | ||||
|                 this.seoService.setTitle(`Channel: ${value.short_id}`); | ||||
|               }), | ||||
|               catchError((err) => { | ||||
|                 this.error = err; | ||||
|                 return of(null); | ||||
|  | ||||
| @ -35,7 +35,8 @@ | ||||
|     <th class="alias text-left" i18n="nodes.alias">Node Alias</th> | ||||
|     <th class="alias text-left d-none d-md-table-cell" i18n="channels.transaction"> </th> | ||||
|     <th class="alias text-left d-none d-md-table-cell" i18n="status">Status</th> | ||||
|     <th class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th> | ||||
|     <th *ngIf="status !== 'closed'" class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th> | ||||
|     <th *ngIf="status === 'closed'" class="channels text-left d-none d-md-table-cell" i18n="channels.closing_date">Closing date</th> | ||||
|     <th class="capacity text-right d-none d-md-table-cell" i18n="nodes.capacity">Capacity</th> | ||||
|     <th class="capacity text-right" i18n="channels.id">Channel ID</th> | ||||
|   </thead> | ||||
| @ -71,9 +72,12 @@ | ||||
|       </ng-template> | ||||
|     </ng-template> | ||||
|   </td> | ||||
|   <td class="capacity text-left d-none d-md-table-cell"> | ||||
|   <td *ngIf="status !== 'closed'" class="capacity text-left d-none d-md-table-cell"> | ||||
|     {{ channel.fee_rate }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span> | ||||
|   </td> | ||||
|   <td *ngIf="status === 'closed'" class="capacity text-left d-none d-md-table-cell"> | ||||
|     <app-timestamp [unixTime]="channel.closing_date"></app-timestamp> | ||||
|   </td> | ||||
|   <td class="capacity text-right d-none d-md-table-cell"> | ||||
|     <app-amount *ngIf="channel.capacity > 100000000; else smallchannel" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> | ||||
|     <ng-template #smallchannel> | ||||
|  | ||||
| @ -21,7 +21,7 @@ export class LightningDashboardComponent implements OnInit { | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.seoService.setTitle($localize`Lightning Dashboard`); | ||||
|     this.seoService.setTitle($localize`Lightning Network`); | ||||
| 
 | ||||
|     this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share()); | ||||
|     this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share()); | ||||
|  | ||||
| @ -1,15 +1,17 @@ | ||||
| <div class="box preview-box" *ngIf="(node$ | async) as node"> | ||||
|   <h2 class="preview-header" i18n="lightning.node">lightning node</h2> | ||||
|   <div class="row d-flex justify-content-between full-width-row"> | ||||
|     <h1 class="title"> | ||||
|       <span i18n="lightning.node">Node</span>: | ||||
|       <a [routerLink]="['/lightning/node' | relativeUrl, node.id]"> {{ node.alias }}</a> | ||||
|     </h1> | ||||
|     <h1 class="title"></h1> | ||||
|     <div class="title-wrapper"> | ||||
|       <h1 class="title">{{ node.alias }}</h1> | ||||
|     </div> | ||||
|     <div class="badges mb-2"> | ||||
|       <span class="badge rounded-pill badge-success" *ngFor="let socketType of socketTypes">{{ socketType }}</span> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="row"> | ||||
|     <div class="col-md"> | ||||
|       <a class="subtitle" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">{{ node.public_key }}</a> | ||||
|       <table class="table table-borderless table-striped"> | ||||
|         <tbody> | ||||
|           <tr> | ||||
|  | ||||
| @ -1,15 +1,17 @@ | ||||
| .title { | ||||
|   font-size: 52px; | ||||
|   margin-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .table { | ||||
|   margin-top: 48px; | ||||
|   margin-top: 6px; | ||||
|   font-size: 32px; | ||||
| } | ||||
| 
 | ||||
| .badges { | ||||
|   font-size: 28px; | ||||
|   flex-shrink: 0; | ||||
|   flex-grow: 0; | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   flex-wrap: nowrap; | ||||
|   align-items: baseline; | ||||
|   justify-content: flex-end; | ||||
| 
 | ||||
|   ::ng-deep .badge { | ||||
|     margin-left: 0.5em; | ||||
| @ -20,14 +22,14 @@ | ||||
|   flex-grow: 0; | ||||
|   flex-shrink: 0; | ||||
|   width: 470px; | ||||
|   height: 390px; | ||||
|   height: 408px; | ||||
|   min-width: 470px; | ||||
|   min-height: 390px; | ||||
|   max-height: 390px; | ||||
|   min-height: 408px; | ||||
|   max-height: 408px; | ||||
|   padding: 0; | ||||
|   background: #181b2d; | ||||
|   overflow: hidden; | ||||
|   margin-top: 18px; | ||||
|   margin-top: 6px; | ||||
| } | ||||
| 
 | ||||
| .row { | ||||
| @ -36,6 +38,7 @@ | ||||
| 
 | ||||
| .full-width-row { | ||||
|   padding-left: 15px; | ||||
|   flex-wrap: nowrap; | ||||
| } | ||||
| 
 | ||||
| ::ng-deep .symbol { | ||||
|  | ||||
| @ -120,7 +120,7 @@ | ||||
|   </div> | ||||
| 
 | ||||
|   <div *ngIf="!error"> | ||||
|     <div class="row" *ngIf="node.as_number"> | ||||
|     <div class="row" *ngIf="node.as_number && node.active_channel_count"> | ||||
|       <div class="col-sm"> | ||||
|         <app-nodes-channels-map [style]="'nodepage'" [publicKey]="node.public_key" [hasLocation]="!!node.as_number"></app-nodes-channels-map> | ||||
|       </div> | ||||
| @ -128,7 +128,7 @@ | ||||
|         <app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div *ngIf="!node.as_number"> | ||||
|     <div *ngIf="!node.as_number || !node.active_channel_count"> | ||||
|       <app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart> | ||||
|     </div> | ||||
| 
 | ||||
|  | ||||
| @ -1,16 +1,13 @@ | ||||
| <div class="full-container"> | ||||
| <div class="full-container" [class]="widget ? 'widget' : ''"> | ||||
| 
 | ||||
|   <div class="card-header"> | ||||
|   <div *ngIf="!widget" class="card-header"> | ||||
|     <div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px"> | ||||
|       <span i18n="lightning.nodes-heatmap">Lightning nodes world heat map</span> | ||||
|       <button class="btn p-0 pl-2" style="margin: 0 0 4px 0px"> | ||||
|         <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true" (click)="onSaveChart()"></fa-icon> | ||||
|       </button> | ||||
|       <span i18n="lightning.nodes-world-map">Lightning nodes world map</span> | ||||
|     </div> | ||||
|     <small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small> | ||||
|   </div> | ||||
| 
 | ||||
|   <div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" | ||||
|   <div *ngIf="observable$ | async" class="chart" [class]="widget ? 'widget' : ''" echarts [initOpts]="chartInitOptions" [options]="chartOptions" | ||||
|     (chartInit)="onChartInit($event)"> | ||||
|   </div> | ||||
| 
 | ||||
|  | ||||
| @ -16,6 +16,11 @@ | ||||
|     padding-bottom: 100px; | ||||
|   }; | ||||
| } | ||||
| .full-container.widget { | ||||
|   min-height: 240px; | ||||
|   height: 240px; | ||||
|   padding: 0px; | ||||
| } | ||||
| 
 | ||||
| .chart { | ||||
|   width: 100%; | ||||
| @ -38,3 +43,6 @@ | ||||
|     padding-bottom: 55px; | ||||
|   } | ||||
| } | ||||
| .chart.widget { | ||||
|   padding: 0px; | ||||
| } | ||||
|  | ||||
| @ -1,14 +1,15 @@ | ||||
| import { ChangeDetectionStrategy, Component, NgZone, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { mempoolFeeColors } from 'src/app/app.constants'; | ||||
| import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { ApiService } from 'src/app/services/api.service'; | ||||
| import { combineLatest, Observable, tap } from 'rxjs'; | ||||
| import { Observable, tap, zip } from 'rxjs'; | ||||
| import { AssetsService } from 'src/app/services/assets.service'; | ||||
| import { EChartsOption, registerMap } from 'echarts'; | ||||
| import { download } from 'src/app/shared/graphs.utils'; | ||||
| import { lerpColor } from 'src/app/shared/graphs.utils'; | ||||
| import { Router } from '@angular/router'; | ||||
| import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; | ||||
| import { StateService } from 'src/app/services/state.service'; | ||||
| import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; | ||||
| import { getFlagEmoji } from 'src/app/shared/common.utils'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-nodes-map', | ||||
| @ -16,7 +17,11 @@ import { StateService } from 'src/app/services/state.service'; | ||||
|   styleUrls: ['./nodes-map.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class NodesMap implements OnInit, OnDestroy { | ||||
| export class NodesMap implements OnInit { | ||||
|   @Input() widget: boolean = false; | ||||
|   @Input() nodes: any[] | undefined = undefined; | ||||
|   @Input() type: 'none' | 'isp' | 'country' = 'none'; | ||||
|    | ||||
|   observable$: Observable<any>; | ||||
| 
 | ||||
|   chartInstance = undefined; | ||||
| @ -26,44 +31,88 @@ export class NodesMap implements OnInit, OnDestroy { | ||||
|   }; | ||||
| 
 | ||||
|   constructor( | ||||
|     @Inject(LOCALE_ID) public locale: string, | ||||
|     private seoService: SeoService, | ||||
|     private apiService: ApiService, | ||||
|     private stateService: StateService, | ||||
|     private assetsService: AssetsService, | ||||
|     private router: Router, | ||||
|     private zone: NgZone, | ||||
|     private amountShortenerPipe: AmountShortenerPipe | ||||
|   ) { | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void {} | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.seoService.setTitle($localize`Lightning nodes world map`); | ||||
| 
 | ||||
|     this.observable$ = combineLatest([ | ||||
|     this.observable$ = zip( | ||||
|       this.assetsService.getWorldMapJson$, | ||||
|       this.apiService.getNodesPerCountry() | ||||
|     ]).pipe(tap((data) => { | ||||
|       this.nodes ? [this.nodes] : this.apiService.getWorldNodes$() | ||||
|     ).pipe(tap((data) => { | ||||
|       registerMap('world', data[0]); | ||||
| 
 | ||||
|       const countries = []; | ||||
|       let max = 0; | ||||
|       for (const country of data[1]) { | ||||
|         countries.push({ | ||||
|           name: country.name.en, | ||||
|           value: country.count, | ||||
|           iso: country.iso.toLowerCase(), | ||||
|         }); | ||||
|         max = Math.max(max, country.count); | ||||
|       let maxLiquidity = data[1].maxLiquidity; | ||||
|       let inputNodes: any[] = data[1].nodes; | ||||
|       let mapCenter: number[] = [0, 5]; | ||||
|       if (this.type === 'country') { | ||||
|         mapCenter = [0, 0]; | ||||
|       } else if (this.type === 'isp') { | ||||
|         mapCenter = [0, 10]; | ||||
|       } | ||||
| 
 | ||||
|       this.prepareChartOptions(countries, max); | ||||
|       let mapZoom = 1.3; | ||||
|       if (!inputNodes) { | ||||
|         inputNodes = []; | ||||
|         for (const node of data[1]) { | ||||
|           if (this.type === 'country') { | ||||
|             mapCenter[0] += node.longitude; | ||||
|             mapCenter[1] += node.latitude; | ||||
|           } | ||||
|           inputNodes.push([ | ||||
|             node.longitude, | ||||
|             node.latitude, | ||||
|             node.public_key, | ||||
|             node.alias, | ||||
|             node.capacity, | ||||
|             node.channels, | ||||
|             node.country, | ||||
|             node.iso_code, | ||||
|           ]); | ||||
|           maxLiquidity = Math.max(maxLiquidity ?? 0, node.capacity); | ||||
|         } | ||||
|         if (this.type === 'country') { | ||||
|           mapCenter[0] /= data[1].length; | ||||
|           mapCenter[1] /= data[1].length; | ||||
|           mapZoom = 6; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       const nodes: any[] = []; | ||||
|       for (const node of inputNodes) { | ||||
|         // We add a bit of noise so nodes at the same location are not all
 | ||||
|         // on top of each other
 | ||||
|         const random = Math.random() * 2 * Math.PI; | ||||
|         const random2 = Math.random() * 0.01; | ||||
|         nodes.push([ | ||||
|           node[0] + random2 * Math.cos(random), | ||||
|           node[1] + random2 * Math.sin(random), | ||||
|           node[4], // Liquidity
 | ||||
|           node[3], // Alias
 | ||||
|           node[2], // Public key
 | ||||
|           node[5], // Channels
 | ||||
|           node[6].en, // Country
 | ||||
|           node[7], // ISO Code
 | ||||
|         ]); | ||||
|       } | ||||
| 
 | ||||
|       maxLiquidity = Math.max(1, maxLiquidity); | ||||
|       this.prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom); | ||||
|     })); | ||||
|   } | ||||
| 
 | ||||
|   prepareChartOptions(countries, max) { | ||||
|   prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom) { | ||||
|     let title: object; | ||||
|     if (countries.length === 0) { | ||||
|     if (nodes.length === 0) { | ||||
|       title = { | ||||
|         textStyle: { | ||||
|           color: 'grey', | ||||
| @ -76,53 +125,82 @@ export class NodesMap implements OnInit, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
|     this.chartOptions = { | ||||
|       title: countries.length === 0 ? title : undefined, | ||||
|       tooltip: { | ||||
|         backgroundColor: 'rgba(17, 19, 31, 1)', | ||||
|         borderRadius: 4, | ||||
|         shadowColor: 'rgba(0, 0, 0, 0.5)', | ||||
|         textStyle: { | ||||
|           color: '#b1b1b1', | ||||
|       silent: false, | ||||
|       title: title ?? undefined, | ||||
|       tooltip: {}, | ||||
|       geo: { | ||||
|         animation: false, | ||||
|         silent: true, | ||||
|         center: mapCenter, | ||||
|         zoom: mapZoom, | ||||
|         tooltip: { | ||||
|           show: false | ||||
|         }, | ||||
|         borderColor: '#000', | ||||
|         formatter: function(country) { | ||||
|           if (country.data === undefined) {  | ||||
|             return `<b style="color: white">${country.name}<br>0 nodes</b><br>`; | ||||
|           } else { | ||||
|             return `<b style="color: white">${country.data.name}<br>${country.data.value} nodes</b><br>`; | ||||
|           } | ||||
|         map: 'world', | ||||
|         roam: true, | ||||
|         itemStyle: { | ||||
|           borderColor: 'black', | ||||
|           color: '#272b3f' | ||||
|         }, | ||||
|         scaleLimit: { | ||||
|           min: 1.3, | ||||
|           max: 100000, | ||||
|         }, | ||||
|         emphasis: { | ||||
|           disabled: true, | ||||
|         } | ||||
|       }, | ||||
|       visualMap: { | ||||
|         left: 'right', | ||||
|         show: true, | ||||
|         min: 1, | ||||
|         max: max, | ||||
|         text: ['High', 'Low'], | ||||
|         calculable: true,         | ||||
|         textStyle: { | ||||
|           color: 'white', | ||||
|         }, | ||||
|         inRange: { | ||||
|           color: mempoolFeeColors.map(color => `#${color}`), | ||||
|         }, | ||||
|       }, | ||||
|       series: { | ||||
|         type: 'map', | ||||
|         map: 'world', | ||||
|         emphasis: { | ||||
|           label: { | ||||
|             show: false, | ||||
|       series: [ | ||||
|         { | ||||
|           large: false, | ||||
|           type: 'scatter', | ||||
|           data: nodes, | ||||
|           coordinateSystem: 'geo', | ||||
|           geoIndex: 0, | ||||
|           progressive: 500, | ||||
|           symbolSize: function (params) { | ||||
|             return 10 * Math.pow(params[2] / maxLiquidity, 0.2) + 3; | ||||
|           }, | ||||
|           tooltip: { | ||||
|             position: function(point, params, dom, rect, size) { | ||||
|               return point; | ||||
|             }, | ||||
|             trigger: 'item', | ||||
|             show: true, | ||||
|             backgroundColor: 'rgba(17, 19, 31, 1)', | ||||
|             borderRadius: 0, | ||||
|             shadowColor: 'rgba(0, 0, 0, 0.5)', | ||||
|             textStyle: { | ||||
|               color: '#b1b1b1', | ||||
|               align: 'left', | ||||
|             }, | ||||
|             borderColor: '#000', | ||||
|             formatter: (value) => { | ||||
|               const data = value.data; | ||||
|               const alias = data[3].length > 0 ? data[3] : data[4].slice(0, 20); | ||||
|               const liquidity = data[2] >= 100000000 ? | ||||
|                 `${this.amountShortenerPipe.transform(data[2] / 100000000)} BTC` : | ||||
|                 `${this.amountShortenerPipe.transform(data[2], 2)} sats`; | ||||
| 
 | ||||
|               return ` | ||||
|                 <b style="color: white">${alias}</b><br> | ||||
|                 ${liquidity}<br> | ||||
|                 ${data[5]} channels<br> | ||||
|                 ${getFlagEmoji(data[7])} ${data[6]} | ||||
|               `;
 | ||||
|             } | ||||
|           }, | ||||
|           itemStyle: { | ||||
|             areaColor: '#FDD835', | ||||
|           } | ||||
|             color: function (params) { | ||||
|               return `${lerpColor('#1E88E5', '#D81B60', Math.pow(params.data[2] / maxLiquidity, 0.2))}`; | ||||
|             }, | ||||
|             opacity: 1, | ||||
|             borderColor: 'black', | ||||
|             borderWidth: 0, | ||||
|           }, | ||||
|           zlevel: 2, | ||||
|         }, | ||||
|         data: countries, | ||||
|         itemStyle: { | ||||
|           areaColor: '#5A6A6D' | ||||
|         }, | ||||
|       } | ||||
|       ] | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
| @ -134,30 +212,16 @@ export class NodesMap implements OnInit, OnDestroy { | ||||
|     this.chartInstance = ec; | ||||
| 
 | ||||
|     this.chartInstance.on('click', (e) => { | ||||
|       if (e.data && e.data.value > 0) { | ||||
|       if (e.data) { | ||||
|         this.zone.run(() => { | ||||
|           const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/nodes/country/${e.data.iso}`); | ||||
|           const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/node/${e.data[4]}`); | ||||
|           this.router.navigate([url]); | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onSaveChart() { | ||||
|     // @ts-ignore
 | ||||
|     const prevBottom = this.chartOptions.grid.bottom; | ||||
|     const now = new Date(); | ||||
|     // @ts-ignore
 | ||||
|     this.chartOptions.grid.bottom = 30; | ||||
|     this.chartOptions.backgroundColor = '#11131f'; | ||||
|     this.chartInstance.setOption(this.chartOptions); | ||||
|     download(this.chartInstance.getDataURL({ | ||||
|       pixelRatio: 2, | ||||
|       excludeComponents: ['dataZoom'], | ||||
|     }), `lightning-nodes-heatmap-clearnet-${Math.round(now.getTime() / 1000)}.svg`); | ||||
|     // @ts-ignore
 | ||||
|     this.chartOptions.grid.bottom = prevBottom; | ||||
|     this.chartOptions.backgroundColor = 'none'; | ||||
|     this.chartInstance.setOption(this.chartOptions); | ||||
|     this.chartInstance.on('georoam', (e) => { | ||||
|       this.chartInstance.resize(); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -46,7 +46,7 @@ | ||||
|           <td class="text-right capacity"> | ||||
|             <app-amount *ngIf="country.capacity > 100000000; else smallchannel" [satoshis]="country.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> | ||||
|             <ng-template #smallchannel> | ||||
|               {{ country.capacity | amountShortener: 1 }} | ||||
|               {{ country.capacity ?? 0 | amountShortener: 1 }} | ||||
|               <span class="sats" i18n="shared.sats">sats</span> | ||||
|             </ng-template> | ||||
|           </td> | ||||
|  | ||||
| @ -45,7 +45,7 @@ export class NodesPerCountryChartComponent implements OnInit { | ||||
|   ngOnInit(): void { | ||||
|     this.seoService.setTitle($localize`Lightning nodes per country`); | ||||
| 
 | ||||
|     this.nodesPerCountryObservable$ = this.apiService.getNodesPerCountry() | ||||
|     this.nodesPerCountryObservable$ = this.apiService.getNodesPerCountry$() | ||||
|       .pipe( | ||||
|         map(data => { | ||||
|           for (let i = 0; i < data.length; ++i) { | ||||
|  | ||||
| @ -1,21 +1,71 @@ | ||||
| <div class="container-xl full-height" style="min-height: 335px"> | ||||
|   <h1 class="float-left" i18n="lightning.nodes-in-country"> | ||||
|   <h1 i18n="lightning.nodes-in-country"> | ||||
|     <span>Lightning nodes in {{ country?.name }}</span> | ||||
|     <span style="font-size: 50px; vertical-align:sub;"> {{ country?.flag }}</span> | ||||
|   </h1> | ||||
| 
 | ||||
|   <div class="box"> | ||||
|     <div class="row" *ngIf="nodes$ | async as countryNodes"> | ||||
|       <div class="col-12 col-md-6"> | ||||
|         <table class="table table-borderless table-striped"> | ||||
|           <tbody> | ||||
|             <tr> | ||||
|               <td i18n="lightning.node-count">Nodes</td> | ||||
|               <td>{{ countryNodes.nodes.length }}</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <td i18n="lightning.liquidity">Liquidity</td> | ||||
|               <td> | ||||
|                 <app-amount *ngIf="countryNodes.sumLiquidity > 100000000; else smallnode" [satoshis]="countryNodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount> | ||||
|                 <ng-template #smallnode> | ||||
|                   {{ countryNodes.sumLiquidity | amountShortener: 1 }} | ||||
|                   <span class="sats" i18n="shared.sats">sats</span> | ||||
|                 </ng-template> | ||||
|                 <span class="d-none d-md-inline-block"> </span> | ||||
|                 <span class="d-block d-md-none"></span> | ||||
|                 <app-fiat [value]="countryNodes.sumLiquidity" digitsInfo="1.0-0"></app-fiat> | ||||
|               </td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <td i18n="lightning.channels">Channels</td> | ||||
|               <td>{{ countryNodes.sumChannels }}</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <td i18n="lightning.isp-count">ISP Count</td> | ||||
|               <td>{{ countryNodes.ispCount }}</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <td i18n="lightning.top-isp">Top ISP</td> | ||||
|               <td class="text-truncate"> | ||||
|                 <a class="d-block text-wrap" [routerLink]="['/lightning/nodes/isp' | relativeUrl, countryNodes.topIsp.id]"> | ||||
|                   {{ countryNodes.topIsp.name }} [ASN {{ countryNodes.topIsp.id }}] | ||||
|                 </a> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </div> | ||||
|       <div class="col-12 col-md-6 p-3 p-md-0 pr-md-3"> | ||||
|         <div style="background-color: #181b2d"> | ||||
|           <app-nodes-map [widget]="true" [nodes]="countryNodes.nodes" type="country"></app-nodes-map> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <div style="min-height: 295px"> | ||||
|     <table class="table table-borderless"> | ||||
|        | ||||
|       <thead> | ||||
|         <th class="alias text-left" i18n="lightning.alias">Alias</th> | ||||
|         <th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th> | ||||
|         <th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th> | ||||
|         <th class="capacity text-right" i18n="lightning.capacity">Capacity</th> | ||||
|         <th class="channels text-right" i18n="lightning.channels">Channels</th> | ||||
|         <th class="city text-right" i18n="lightning.city">City</th> | ||||
|         <th class="city text-right" i18n="lightning.location">Location</th> | ||||
|       </thead> | ||||
|       <tbody *ngIf="nodes$ | async as nodes"> | ||||
|         <tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey"> | ||||
|       <tbody *ngIf="nodes$ | async as countryNodes; else skeleton"> | ||||
|         <tr *ngFor="let node of countryNodes.nodes; let i= index; trackBy: trackByPublicKey"> | ||||
|           <td class="alias text-left text-truncate"> | ||||
|             <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a> | ||||
|           </td> | ||||
| @ -39,6 +89,32 @@ | ||||
|             <app-geolocation [data]="node.geolocation" [type]="'list-country'"></app-geolocation> | ||||
|           </td> | ||||
|       </tbody> | ||||
| 
 | ||||
|       <ng-template #skeleton> | ||||
|         <tbody> | ||||
|           <tr *ngFor="let item of skeletonLines"> | ||||
|             <td class="alias text-left text-truncate"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|             <td class="timestamp-first text-left"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|             <td class="timestamp-update text-left"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|             <td class="capacity text-right"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|             <td class="channels text-right"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|             <td class="city text-right text-truncate"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </ng-template> | ||||
| 
 | ||||
|     </table> | ||||
|   </div> | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { map, Observable } from 'rxjs'; | ||||
| import { map, Observable, share } from 'rxjs'; | ||||
| import { ApiService } from 'src/app/services/api.service'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { getFlagEmoji } from 'src/app/shared/common.utils'; | ||||
| @ -16,16 +16,24 @@ export class NodesPerCountry implements OnInit { | ||||
|   nodes$: Observable<any>; | ||||
|   country: {name: string, flag: string}; | ||||
| 
 | ||||
|   skeletonLines: number[] = []; | ||||
| 
 | ||||
|   constructor( | ||||
|     private apiService: ApiService, | ||||
|     private seoService: SeoService, | ||||
|     private route: ActivatedRoute, | ||||
|   ) { } | ||||
|   ) { | ||||
|     for (let i = 0; i < 20; ++i) { | ||||
|       this.skeletonLines.push(i); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country) | ||||
|       .pipe( | ||||
|         map(response => { | ||||
|           this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`); | ||||
| 
 | ||||
|           this.country = { | ||||
|             name: response.country.en, | ||||
|             flag: getFlagEmoji(this.route.snapshot.params.country) | ||||
| @ -39,14 +47,50 @@ export class NodesPerCountry implements OnInit { | ||||
|               iso: response.nodes[i].iso_code, | ||||
|             }; | ||||
|           } | ||||
| 
 | ||||
|           this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`); | ||||
|           return response.nodes; | ||||
|         }) | ||||
|            | ||||
|           const sumLiquidity = response.nodes.reduce((partialSum, a) => partialSum + a.capacity, 0); | ||||
|           const sumChannels = response.nodes.reduce((partialSum, a) => partialSum + a.channels, 0); | ||||
|           const isps = {}; | ||||
|           const topIsp = { | ||||
|             count: 0, | ||||
|             id: '', | ||||
|             name: '', | ||||
|           }; | ||||
|           for (const node of response.nodes) { | ||||
|             if (!node.isp) { | ||||
|               continue; | ||||
|             } | ||||
|             if (!isps[node.isp]) { | ||||
|               isps[node.isp] = { | ||||
|                 count: 0, | ||||
|                 asns: [], | ||||
|               }; | ||||
|             } | ||||
|             if (isps[node.isp].asns.indexOf(node.as_number) === -1) { | ||||
|               isps[node.isp].asns.push(node.as_number); | ||||
|             } | ||||
|             isps[node.isp].count++; | ||||
|              | ||||
|             if (isps[node.isp].count > topIsp.count) { | ||||
|               topIsp.count = isps[node.isp].count; | ||||
|               topIsp.id = isps[node.isp].asns.join(','); | ||||
|               topIsp.name = node.isp; | ||||
|             } | ||||
|           } | ||||
|            | ||||
|           return { | ||||
|             nodes: response.nodes, | ||||
|             sumLiquidity: sumLiquidity, | ||||
|             sumChannels: sumChannels, | ||||
|             topIsp: topIsp, | ||||
|             ispCount: Object.keys(isps).length | ||||
|           }; | ||||
|         }), | ||||
|         share() | ||||
|       ); | ||||
|   } | ||||
| 
 | ||||
|   trackByPublicKey(index: number, node: any) { | ||||
|   trackByPublicKey(index: number, node: any): string { | ||||
|     return node.public_key; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -10,14 +10,14 @@ | ||||
|         </p> | ||||
|       </div> | ||||
|       <div class="item"> | ||||
|         <h5 class="card-title d-inline-block">Unknown capacity</h5> | ||||
|         <h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown capacity</h5> | ||||
|         <p class="card-text" i18n-ngbTooltip="lightning.unknown-capacity-desc" | ||||
|         ngbTooltip="How much liquidity is running on nodes which ISP was not identifiable" placement="bottom"> | ||||
|           <app-amount [satoshis]="stats.unknownCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> | ||||
|         </p> | ||||
|       </div> | ||||
|       <div class="item"> | ||||
|         <h5 class="card-title d-inline-block">Tor capacity</h5> | ||||
|         <h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor capacity</h5> | ||||
|         <p class="card-text" i18n-ngbTooltip="lightning.tor-capacity-desc" | ||||
|         ngbTooltip="How much liquidity is running on nodes advertising only Tor addresses" placement="bottom"> | ||||
|           <app-amount [satoshis]="stats.torCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> | ||||
| @ -80,19 +80,19 @@ | ||||
| <ng-template #loadingReward> | ||||
|   <div class="pool-distribution"> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="lightning.tagged-isp">Tagged ISPs</h5> | ||||
|       <h5 class="card-title d-inline-block" i18n="lightning.clearnet-capacity">Clearnet capacity</h5> | ||||
|       <p class="card-text"> | ||||
|         <span class="skeleton-loader skeleton-loader-big"></span> | ||||
|       </p> | ||||
|     </div> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="lightning.tagged-capacity">Tagged capacity</h5> | ||||
|       <h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown capacity</h5> | ||||
|       <p class="card-text"> | ||||
|         <span class="skeleton-loader skeleton-loader-big"></span> | ||||
|       </p> | ||||
|     </div> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="lightning.tagged-nodes">Tagged nodes</h5> | ||||
|       <h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor capacity</h5> | ||||
|       <p class="card-text"> | ||||
|         <span class="skeleton-loader skeleton-loader-big"></span> | ||||
|       </p> | ||||
|  | ||||
| @ -47,7 +47,9 @@ export class NodesPerISPChartComponent implements OnInit { | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.seoService.setTitle($localize`Lightning nodes per ISP`); | ||||
|     if (!this.widget) { | ||||
|       this.seoService.setTitle($localize`Lightning nodes per ISP`); | ||||
|     } | ||||
| 
 | ||||
|     this.nodesPerAsObservable$ = combineLatest([ | ||||
|       this.sortBySubject.pipe(startWith(true)), | ||||
| @ -105,7 +107,7 @@ export class NodesPerISPChartComponent implements OnInit { | ||||
|   } | ||||
| 
 | ||||
|   generateChartSerieData(ispRanking): PieSeriesOption[] { | ||||
|     let shareThreshold = 0.5; | ||||
|     let shareThreshold = 0.4; | ||||
|     if (this.widget && isMobile() || isMobile()) { | ||||
|       shareThreshold = 1; | ||||
|     } else if (this.widget) { | ||||
| @ -132,9 +134,6 @@ export class NodesPerISPChartComponent implements OnInit { | ||||
|         return; | ||||
|       } | ||||
|       data.push({ | ||||
|         itemStyle: { | ||||
|           color: isp[0] === null ? '#7D4698' : undefined, | ||||
|         }, | ||||
|         value: this.sortBy === 'capacity' ? isp[7] : isp[6], | ||||
|         name: isp[1].replace('&', '') + (isMobile() || this.widget ? `` : ` (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)`), | ||||
|         label: { | ||||
| @ -204,7 +203,7 @@ export class NodesPerISPChartComponent implements OnInit { | ||||
|     } | ||||
| 
 | ||||
|     this.chartOptions = { | ||||
|       color: chartColors.slice(3), | ||||
|       color: chartColors.filter((color) => color != '#5E35B1'), // Remove color that looks like Tor
 | ||||
|       tooltip: { | ||||
|         trigger: 'item', | ||||
|         textStyle: { | ||||
|  | ||||
| @ -1,18 +1,68 @@ | ||||
| <div class="container-xl full-height" style="min-height: 335px"> | ||||
|   <h1 class="float-left" i18n="lightning.nodes-for-isp">Lightning nodes on ISP: {{ isp?.name }} [AS {{isp?.id}}]</h1> | ||||
|   <h1 i18n="lightning.nodes-for-isp">Lightning nodes on ISP: {{ isp?.name }}</h1> | ||||
| 
 | ||||
|   <div class="box"> | ||||
|     <div class="row" *ngIf="nodes$ | async as ispNodes"> | ||||
|       <div class="col-12 col-md-6"> | ||||
|         <table class="table table-borderless table-striped"> | ||||
|           <tbody> | ||||
|             <tr> | ||||
|               <td i18n="lightning.asn">ASN</td> | ||||
|               <td>{{ isp?.id }}</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <td i18n="lightning.node-count">Nodes</td> | ||||
|               <td>{{ ispNodes.nodes.length }}</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <td i18n="lightning.liquidity">Liquidity</td> | ||||
|               <td> | ||||
|                 <app-amount *ngIf="ispNodes.sumLiquidity > 100000000; else smallnode" [satoshis]="ispNodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount> | ||||
|                 <ng-template #smallnode> | ||||
|                   {{ ispNodes.sumLiquidity | amountShortener: 1 }} | ||||
|                   <span class="sats" i18n="shared.sats">sats</span> | ||||
|                 </ng-template> | ||||
|                 <span class="d-none d-md-inline-block"> </span> | ||||
|                 <span class="d-block d-md-none"></span> | ||||
|                 <app-fiat [value]="ispNodes.sumLiquidity" digitsInfo="1.0-0"></app-fiat> | ||||
|               </td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <td i18n="lightning.channels">Channels</td> | ||||
|               <td>{{ ispNodes.sumChannels }}</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <td i18n="lightning.top-country">Top country</td> | ||||
|               <td class="text-truncate"> | ||||
|                 <a class="d-block text-wrap" [routerLink]="['/lightning/nodes/country' | relativeUrl, ispNodes.topCountry.iso]"> | ||||
|                   <span class="">{{ ispNodes.topCountry.country }} {{ ispNodes.topCountry.flag }}</span> | ||||
|                 </a> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </div> | ||||
|       <div class="col-12 col-md-6 p-3 p-md-0 pr-md-3"> | ||||
|         <div style="background-color: #181b2d"> | ||||
|           <app-nodes-map [widget]="true" [nodes]="ispNodes.nodes" type="isp"></app-nodes-map> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <div style="min-height: 295px"> | ||||
|     <table class="table table-borderless"> | ||||
| 
 | ||||
|       <thead> | ||||
|         <th class="alias text-left" i18n="lightning.alias">Alias</th> | ||||
|         <th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th> | ||||
|         <th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th> | ||||
|         <th class="capacity text-right" i18n="lightning.capacity">Capacity</th> | ||||
|         <th class="channels text-right" i18n="lightning.channels">Channels</th> | ||||
|         <th class="city text-right" i18n="lightning.city">City</th> | ||||
|         <th class="city text-right" i18n="lightning.location">Location</th> | ||||
|       </thead> | ||||
|       <tbody *ngIf="nodes$ | async as nodes"> | ||||
|         <tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey"> | ||||
|       <tbody *ngIf="nodes$ | async as ispNodes; else skeleton"> | ||||
|         <tr *ngFor="let node of ispNodes.nodes; let i= index; trackBy: trackByPublicKey"> | ||||
|           <td class="alias text-left text-truncate"> | ||||
|             <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a> | ||||
|           </td> | ||||
| @ -36,6 +86,32 @@ | ||||
|             <app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation> | ||||
|           </td> | ||||
|       </tbody> | ||||
|      | ||||
|       <ng-template #skeleton> | ||||
|         <tbody> | ||||
|           <tr *ngFor="let item of skeletonLines"> | ||||
|             <td class="alias text-left text-truncate"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|             <td class="timestamp-first text-left"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|             <td class="timestamp-update text-left"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|             <td class="capacity text-right"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|             <td class="channels text-right"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|             <td class="city text-right text-truncate"> | ||||
|               <span class="skeleton-loader"></span> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </ng-template> | ||||
|    | ||||
|     </table> | ||||
|   </div> | ||||
| 
 | ||||
|  | ||||
| @ -59,4 +59,4 @@ | ||||
|   @media (max-width: 576px) { | ||||
|     display: none | ||||
|   } | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -1,8 +1,9 @@ | ||||
| import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { map, Observable } from 'rxjs'; | ||||
| import { map, Observable, share } from 'rxjs'; | ||||
| import { ApiService } from 'src/app/services/api.service'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { getFlagEmoji } from 'src/app/shared/common.utils'; | ||||
| import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; | ||||
| 
 | ||||
| @Component({ | ||||
| @ -15,11 +16,17 @@ export class NodesPerISP implements OnInit { | ||||
|   nodes$: Observable<any>; | ||||
|   isp: {name: string, id: number}; | ||||
| 
 | ||||
|   skeletonLines: number[] = []; | ||||
| 
 | ||||
|   constructor( | ||||
|     private apiService: ApiService, | ||||
|     private seoService: SeoService, | ||||
|     private route: ActivatedRoute, | ||||
|   ) { } | ||||
|   ) { | ||||
|     for (let i = 0; i < 20; ++i) { | ||||
|       this.skeletonLines.push(i); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp) | ||||
| @ -27,7 +34,7 @@ export class NodesPerISP implements OnInit { | ||||
|         map(response => { | ||||
|           this.isp = { | ||||
|             name: response.isp, | ||||
|             id: this.route.snapshot.params.isp | ||||
|             id: this.route.snapshot.params.isp.split(',').join(', ') | ||||
|           }; | ||||
|           this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`); | ||||
| 
 | ||||
| @ -40,12 +47,40 @@ export class NodesPerISP implements OnInit { | ||||
|             }; | ||||
|           } | ||||
| 
 | ||||
|           return response.nodes; | ||||
|         }) | ||||
|           const sumLiquidity = response.nodes.reduce((partialSum, a) => partialSum + a.capacity, 0); | ||||
|           const sumChannels = response.nodes.reduce((partialSum, a) => partialSum + a.channels, 0); | ||||
|           const countries = {}; | ||||
|           const topCountry = { | ||||
|             count: 0, | ||||
|             country: '', | ||||
|             iso: '', | ||||
|             flag: '', | ||||
|           }; | ||||
|           for (const node of response.nodes) { | ||||
|             if (!node.geolocation.iso) { | ||||
|               continue; | ||||
|             } | ||||
|             countries[node.geolocation.iso] = countries[node.geolocation.iso] ?? 0 + 1; | ||||
|             if (countries[node.geolocation.iso] > topCountry.count) { | ||||
|               topCountry.count = countries[node.geolocation.iso]; | ||||
|               topCountry.country = node.geolocation.country; | ||||
|               topCountry.iso = node.geolocation.iso; | ||||
|             } | ||||
|           } | ||||
|           topCountry.flag = getFlagEmoji(topCountry.iso); | ||||
|            | ||||
|           return { | ||||
|             nodes: response.nodes, | ||||
|             sumLiquidity: sumLiquidity, | ||||
|             sumChannels: sumChannels, | ||||
|             topCountry: topCountry, | ||||
|           }; | ||||
|         }), | ||||
|         share() | ||||
|       ); | ||||
|   } | ||||
| 
 | ||||
|   trackByPublicKey(index: number, node: any) { | ||||
|   trackByPublicKey(index: number, node: any): string { | ||||
|     return node.public_key; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -9,7 +9,7 @@ | ||||
|         <th class="rank"></th> | ||||
|         <th class="alias text-left" i18n="nodes.alias">Alias</th> | ||||
|         <th  class="timestamp-first text-right" i18n="lightning.first_seen">First seen</th> | ||||
|         <th *ngIf="!widget" class="capacity text-right" i18n="node.capacity">Capacity</th> | ||||
|         <th *ngIf="!widget" class="capacity text-right" i18n="node.liquidity">Liquidity</th> | ||||
|         <th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th> | ||||
|         <th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th> | ||||
|         <th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th> | ||||
| @ -35,7 +35,7 @@ | ||||
|             <app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp> | ||||
|           </td> | ||||
|           <td *ngIf="!widget" class="location text-right text-truncate"> | ||||
|             {{ node?.city?.en ?? '-' }} | ||||
|             <app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation> | ||||
|           </td> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | ||||
| import { map, Observable } from 'rxjs'; | ||||
| import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { IOldestNodes } from '../../../interfaces/node-api.interface'; | ||||
| import { LightningApiService } from '../../lightning-api.service'; | ||||
| 
 | ||||
| @ -15,19 +17,38 @@ export class OldestNodes implements OnInit { | ||||
|   oldestNodes$: Observable<IOldestNodes[]>; | ||||
|   skeletonRows: number[] = []; | ||||
| 
 | ||||
|   constructor(private apiService: LightningApiService) {} | ||||
|   constructor( | ||||
|     private apiService: LightningApiService, | ||||
|     private seoService: SeoService | ||||
|   ) {} | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     if (!this.widget) { | ||||
|       this.seoService.setTitle($localize`Oldest lightning nodes`); | ||||
|     } | ||||
| 
 | ||||
|     for (let i = 1; i <= (this.widget ? 10 : 100); ++i) { | ||||
|       this.skeletonRows.push(i); | ||||
|     } | ||||
| 
 | ||||
|     if (this.widget === false) { | ||||
|       this.oldestNodes$ = this.apiService.getOldestNodes$(); | ||||
|       this.oldestNodes$ = this.apiService.getOldestNodes$().pipe( | ||||
|         map((ranking) => { | ||||
|           for (const i in ranking) { | ||||
|             ranking[i].geolocation = <GeolocationData>{ | ||||
|               country: ranking[i].country?.en, | ||||
|               city: ranking[i].city?.en, | ||||
|               subdivision: ranking[i].subdivision?.en, | ||||
|               iso: ranking[i].iso_code, | ||||
|             }; | ||||
|           } | ||||
|           return ranking; | ||||
|         }) | ||||
|       ); | ||||
|     } else { | ||||
|       this.oldestNodes$ = this.apiService.getOldestNodes$().pipe( | ||||
|         map((nodes: IOldestNodes[]) => { | ||||
|           return nodes.slice(0, 10); | ||||
|           return nodes.slice(0, 7); | ||||
|         }) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
|       <thead> | ||||
|         <th class="rank"></th> | ||||
|         <th class="alias text-left" i18n="nodes.alias">Alias</th> | ||||
|         <th class="capacity text-right" i18n="node.capacity">Capacity</th> | ||||
|         <th class="capacity text-right" i18n="node.liquidity">Liquidity</th> | ||||
|         <th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th> | ||||
|         <th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th> | ||||
|         <th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th> | ||||
| @ -35,7 +35,7 @@ | ||||
|             <app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp> | ||||
|           </td> | ||||
|           <td *ngIf="!widget" class="location text-right text-truncate"> | ||||
|             {{ node?.city?.en ?? '-' }} | ||||
|             <app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation> | ||||
|           </td> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|  | ||||
| @ -1,7 +1,9 @@ | ||||
| import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | ||||
| import { map, Observable } from 'rxjs'; | ||||
| import { INodesRanking, ITopNodesPerCapacity } from 'src/app/interfaces/node-api.interface'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { isMobile } from 'src/app/shared/common.utils'; | ||||
| import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; | ||||
| import { LightningApiService } from '../../lightning-api.service'; | ||||
| 
 | ||||
| @Component({ | ||||
| @ -17,15 +19,34 @@ export class TopNodesPerCapacity implements OnInit { | ||||
|   topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>; | ||||
|   skeletonRows: number[] = []; | ||||
| 
 | ||||
|   constructor(private apiService: LightningApiService) {} | ||||
|   constructor( | ||||
|     private apiService: LightningApiService, | ||||
|     private seoService: SeoService | ||||
|   ) {} | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     if (!this.widget) { | ||||
|       this.seoService.setTitle($localize`Liquidity Ranking`); | ||||
|     } | ||||
| 
 | ||||
|     for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) { | ||||
|       this.skeletonRows.push(i); | ||||
|     } | ||||
| 
 | ||||
|     if (this.widget === false) { | ||||
|       this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$(); | ||||
|       this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$().pipe( | ||||
|         map((ranking) => { | ||||
|           for (const i in ranking) { | ||||
|             ranking[i].geolocation = <GeolocationData>{ | ||||
|               country: ranking[i].country?.en, | ||||
|               city: ranking[i].city?.en, | ||||
|               subdivision: ranking[i].subdivision?.en, | ||||
|               iso: ranking[i].iso_code, | ||||
|             }; | ||||
|           } | ||||
|           return ranking; | ||||
|         }) | ||||
|       ); | ||||
|     } else { | ||||
|       this.topNodesPerCapacity$ = this.nodes$.pipe( | ||||
|         map((ranking) => { | ||||
|  | ||||
| @ -9,7 +9,7 @@ | ||||
|         <th class="rank"></th> | ||||
|         <th class="alias text-left" i18n="nodes.alias">Alias</th> | ||||
|         <th class="channels text-right" i18n="node.channels">Channels</th> | ||||
|         <th *ngIf="!widget" class="capacity text-right" i18n="lightning.capacity">Capacity</th> | ||||
|         <th *ngIf="!widget" class="capacity text-right" i18n="lightning.liquidity">Liquidity</th> | ||||
|         <th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th> | ||||
|         <th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th> | ||||
|         <th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th> | ||||
| @ -35,9 +35,9 @@ | ||||
|             <app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp> | ||||
|           </td> | ||||
|           <td *ngIf="!widget" class="location text-right text-truncate"> | ||||
|             {{ node?.city?.en ?? '-' }} | ||||
|             <app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation> | ||||
|           </td> | ||||
|         </tr> | ||||
|       </tr> | ||||
|       </tbody> | ||||
|       <ng-template #skeleton> | ||||
|         <tbody> | ||||
|  | ||||
| @ -1,7 +1,9 @@ | ||||
| import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | ||||
| import { map, Observable } from 'rxjs'; | ||||
| import { INodesRanking, ITopNodesPerChannels } from 'src/app/interfaces/node-api.interface'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { isMobile } from 'src/app/shared/common.utils'; | ||||
| import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; | ||||
| import { LightningApiService } from '../../lightning-api.service'; | ||||
| 
 | ||||
| @Component({ | ||||
| @ -17,15 +19,34 @@ export class TopNodesPerChannels implements OnInit { | ||||
|   topNodesPerChannels$: Observable<ITopNodesPerChannels[]>; | ||||
|   skeletonRows: number[] = []; | ||||
| 
 | ||||
|   constructor(private apiService: LightningApiService) {} | ||||
|   constructor( | ||||
|     private apiService: LightningApiService, | ||||
|     private seoService: SeoService | ||||
|   ) {} | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     if (!this.widget) { | ||||
|       this.seoService.setTitle($localize`Connectivity Ranking`); | ||||
|     } | ||||
| 
 | ||||
|     for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) { | ||||
|       this.skeletonRows.push(i); | ||||
|     } | ||||
| 
 | ||||
|     if (this.widget === false) { | ||||
|       this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$(); | ||||
|       this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$().pipe( | ||||
|         map((ranking) => { | ||||
|           for (const i in ranking) { | ||||
|             ranking[i].geolocation = <GeolocationData>{ | ||||
|               country: ranking[i].country?.en, | ||||
|               city: ranking[i].city?.en, | ||||
|               subdivision: ranking[i].subdivision?.en, | ||||
|               iso: ranking[i].iso_code, | ||||
|             }; | ||||
|           } | ||||
|           return ranking; | ||||
|         }) | ||||
|       ); | ||||
|     } else { | ||||
|       this.topNodesPerChannels$ = this.nodes$.pipe( | ||||
|         map((ranking) => { | ||||
|  | ||||
| @ -267,10 +267,14 @@ export class ApiService { | ||||
|     return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp/' + isp); | ||||
|   } | ||||
| 
 | ||||
|   getNodesPerCountry(): Observable<any> { | ||||
|   getNodesPerCountry$(): Observable<any> { | ||||
|     return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/countries'); | ||||
|   } | ||||
| 
 | ||||
|   getWorldNodes$(): Observable<any> { | ||||
|     return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/world'); | ||||
|   } | ||||
| 
 | ||||
|   getChannelsGeo$(publicKey?: string, style?: 'graph' | 'nodepage' | 'widget' | 'channelpage'): Observable<any> { | ||||
|     return this.httpClient.get<any[]>( | ||||
|       this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels-geo' + | ||||
|  | ||||
| @ -1,5 +1,8 @@ | ||||
| ‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number : digitsInfo }} | ||||
| <span class="symbol"><ng-template [ngIf]="network === 'liquid'">L-</ng-template> | ||||
| <ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template> | ||||
| <ng-template [ngIf]="network === 'testnet'">t-</ng-template> | ||||
| <ng-template [ngIf]="network === 'signet'">s-</ng-template>sats</span> | ||||
| <span *ngIf="valueOverride !== undefined">{{ valueOverride }}</span> | ||||
| <span *ngIf="valueOverride === undefined">‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number : digitsInfo }} </span> | ||||
| <span class="symbol"> | ||||
|   <ng-template [ngIf]="network === 'liquid'">L-</ng-template> | ||||
|   <ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template> | ||||
|   <ng-template [ngIf]="network === 'testnet'">t-</ng-template> | ||||
|   <ng-template [ngIf]="network === 'signet'">s-</ng-template>sats | ||||
| </span> | ||||
| @ -11,6 +11,7 @@ export class SatsComponent implements OnInit { | ||||
|   @Input() satoshis: number; | ||||
|   @Input() digitsInfo = '1.0-0'; | ||||
|   @Input() addPlus = false; | ||||
|   @Input() valueOverride: string | undefined = undefined; | ||||
| 
 | ||||
|   network = ''; | ||||
|   stateSubscription: Subscription; | ||||
|  | ||||
| @ -1,4 +1,7 @@ | ||||
| ‎{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }} | ||||
| <div class="lg-inline"> | ||||
|   <i class="symbol">(<app-time-since [time]="seconds" [fastRender]="true"></app-time-since>)</i> | ||||
| </div> | ||||
| <span *ngIf="seconds === undefined">-</span> | ||||
| <span *ngIf="seconds !== undefined"> | ||||
|   ‎{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }} | ||||
|   <div class="lg-inline"> | ||||
|     <i class="symbol">(<app-time-since [time]="seconds" [fastRender]="true"></app-time-since>)</i> | ||||
|   </div> | ||||
| </span> | ||||
|  | ||||
| @ -11,15 +11,13 @@ export class TimestampComponent implements OnChanges { | ||||
|   @Input() dateString: string; | ||||
|   @Input() customFormat: string; | ||||
| 
 | ||||
|   seconds: number; | ||||
| 
 | ||||
|   constructor() { } | ||||
|   seconds: number | undefined = undefined; | ||||
| 
 | ||||
|   ngOnChanges(): void { | ||||
|     if (this.unixTime) { | ||||
|       this.seconds = this.unixTime; | ||||
|     } else if (this.dateString) { | ||||
|       this.seconds  = new Date(this.dateString).getTime() / 1000 | ||||
|       this.seconds = new Date(this.dateString).getTime() / 1000; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										1
									
								
								frontend/src/resources/profile/btcpayserver.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/resources/profile/btcpayserver.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 105.46 188.47"><defs><style>.cls-1{fill:#cedc21;}.cls-2{fill:#51b13e;}.cls-3{fill:#1e7a44;}.cls-4{fill:#fff;}</style></defs><title>BTCPayServer</title><path class="cls-1" d="M117.24,247.32a11.06,11.06,0,0,1-11-11.06V69.91a11.06,11.06,0,1,1,22.11,0V236.26A11.06,11.06,0,0,1,117.24,247.32Z" transform="translate(-106.19 -58.85)"/><path class="cls-2" d="M117.25,247.32a11.06,11.06,0,0,1-4.75-21l66.66-31.64L110.69,144.2a11.05,11.05,0,1,1,13.11-17.8l83.35,61.41a11,11,0,0,1-1.82,18.88L122,246.25A10.94,10.94,0,0,1,117.25,247.32Z" transform="translate(-106.19 -58.85)"/><path class="cls-1" d="M117.25,181.93a11.05,11.05,0,0,1-6.56-20l68.47-50.45L112.5,79.89a11.05,11.05,0,0,1,9.48-20l83.35,39.56a11.05,11.05,0,0,1,1.82,18.89L123.8,179.78A11,11,0,0,1,117.25,181.93Z" transform="translate(-106.19 -58.85)"/><polygon class="cls-3" points="22.11 70.86 22.11 117.61 53.82 94.25 22.11 70.86"/><rect class="cls-4" y="51.26" width="22.11" height="53.89"/><path class="cls-1" d="M128.3,69.91a11.06,11.06,0,1,0-22.11,0V209H128.3Z" transform="translate(-106.19 -58.85)"/></svg> | ||||
| After Width: | Height: | Size: 1.1 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 19 KiB | 
							
								
								
									
										
											BIN
										
									
								
								frontend/src/resources/profile/electrum.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/resources/profile/electrum.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 170 KiB | 
							
								
								
									
										1
									
								
								frontend/src/resources/profile/lnbits.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/resources/profile/lnbits.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><defs><path id="a" d="M33.2619 148.1667h154.2143v68.7917H33.2619z"/></defs><g fill="#1f2234" aria-label="LNbits" font-family="sans-serif" font-size=".3095" font-weight="400" letter-spacing=".0031" style="line-height:1.25;white-space:pre;shape-inside:url(#a)" transform="matrix(72.4607 0 0 72.4607 -2399.2814 -10741.3589)"><g transform="matrix(.00244 0 0 .00244 33.0708 148.1594)"><circle cx="101.2976" cy="116.4167" r="84.6667" fill="#673ab7" fill-rule="evenodd"/><path fill="#eee" d="M79.1105 71.9667v49.0613h13.3803v40.141l31.2208-53.5213h-17.8404l17.8404-35.681z"/></g><g fill="#eee" font-family="roboto"/></g></svg> | ||||
| After Width: | Height: | Size: 680 B | 
| @ -89,7 +89,7 @@ body { | ||||
| 
 | ||||
| .preview-box { | ||||
|   min-height: 520px; | ||||
|   padding: 1.5rem 3rem; | ||||
|   padding: 1rem 3rem 1.5rem; | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 767.98px) { | ||||
|  | ||||
| @ -4,7 +4,7 @@ var fs = require('fs'); | ||||
| const CONFIG_FILE_NAME = 'mempool-frontend-config.json'; | ||||
| let configContent = {}; | ||||
| 
 | ||||
| var PATH = 'dist/mempool/browser/en-US/resources/'; | ||||
| var PATH = 'dist/mempool/browser/resources/'; | ||||
| if (process.argv[2] && process.argv[2] === 'dev') { | ||||
|   PATH = 'src/resources/'; | ||||
| } | ||||
|  | ||||
| @ -18,7 +18,7 @@ | ||||
| 		expires 10m; | ||||
| 	} | ||||
| 	location /resources { | ||||
| 		try_files /$lang/$uri /$lang/$uri/ $uri $uri/ /en-US/$uri @index-redirect; | ||||
| 		try_files $uri @index-redirect; | ||||
| 		expires 1h; | ||||
| 	} | ||||
| 	location @index-redirect { | ||||
| @ -27,10 +27,6 @@ | ||||
| 
 | ||||
| 	# location block using regex are matched in order | ||||
| 
 | ||||
| 	# used to rewrite resources from /<lang>/ to /en-US/ | ||||
| 	location ~ ^/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh|hi)/resources/ { | ||||
| 		rewrite ^/[a-zA-Z-]*/resources/(.*) /en-US/resources/$1; | ||||
| 	} | ||||
| 	# used for cookie override | ||||
| 	location ~ ^/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh|hi)/ { | ||||
| 		try_files $uri $uri/ /$1/index.html =404; | ||||
|  | ||||
| @ -1009,7 +1009,6 @@ osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_ | ||||
| osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-build-all upgrade | ||||
| osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-kill-all stop | ||||
| osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-start-all start | ||||
| osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-restart-all restart | ||||
| 
 | ||||
| 
 | ||||
| case $OS in | ||||
|  | ||||
| @ -1,8 +0,0 @@ | ||||
| #!/usr/bin/env zsh | ||||
| HOSTNAME=$(hostname) | ||||
| 
 | ||||
| echo restarting mempool backends | wall | ||||
| echo "${HOSTNAME} restarted mempool backends" | /usr/local/bin/keybase chat send --nonblock --channel general mempool.ops | ||||
| ps uaxw|grep 'dist/index'|grep -v grep|grep -v services|awk '{print $2}'|xargs -n 1 kill | ||||
| 
 | ||||
| exit 0 | ||||
| @ -5,5 +5,5 @@ | ||||
| 37 13 * * * sleep 30 ; /mempool/mempool.space/backup >/dev/null 2>&1 & | ||||
| 
 | ||||
| # hourly liquid asset update | ||||
| 6 * * * * cd $HOME/liquid/frontend && npm run sync-assets && rsync -av $HOME/liquid/frontend/dist/mempool/browser/en-US/resources/assets* $HOME/public_html/liquid/en-US/resources/ >/dev/null 2>&1 | ||||
| 6 * * * * cd $HOME/liquid/frontend && npm run sync-assets && rsync -av $HOME/liquid/frontend/dist/mempool/browser/resources/assets* $HOME/public_html/liquid/resources/ >/dev/null 2>&1 | ||||
| 
 | ||||
|  | ||||
| @ -58,12 +58,6 @@ location = / { | ||||
| 	expires 5m; | ||||
| } | ||||
| 
 | ||||
| # used to rewrite resources from /<lang>/ to /en-US/ | ||||
| # cache /resources/** for 1 week since they don't change often | ||||
| location ~ ^/[a-z][a-z]/resources/(.*) { | ||||
| 	try_files $uri /en-US/resources/$1 =404; | ||||
| 	expires 1w; | ||||
| } | ||||
| # cache /<lang>/main.f40e91d908a068a2.js forever since they never change | ||||
| location ~ ^/([a-z][a-z])/(.+\..+\.(js|css)) { | ||||
| 	try_files $uri =404; | ||||
| @ -84,7 +78,7 @@ location ~ ^/([a-z][a-z])/ { | ||||
| 
 | ||||
| # cache /resources/** for 1 week since they don't change often | ||||
| location /resources { | ||||
| 	try_files $uri /en-US/$uri /en-US/index.html; | ||||
| 	try_files $uri /en-US/index.html; | ||||
| 	expires 1w; | ||||
| } | ||||
| # cache /main.f40e91d908a068a2.js forever since they never change | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user