diff --git a/backend/README.md b/backend/README.md index 823998fdc..3d7c23eaa 100644 --- a/backend/README.md +++ b/backend/README.md @@ -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: diff --git a/backend/package.json b/backend/package.json index 082449dac..3cd05e8ab 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/api/backend-info.ts b/backend/src/api/backend-info.ts index d98675671..57bb5fe13 100644 --- a/backend/src/api/backend-info.ts +++ b/backend/src/api/backend-info.ts @@ -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); } } diff --git a/backend/src/api/explorer/channels.api.ts b/backend/src/api/explorer/channels.api.ts index b396e4808..a52b0f28f 100644 --- a/backend/src/api/explorer/channels.api.ts +++ b/backend/src/api/explorer/channels.api.ts @@ -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, diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 128405ffd..cbd70a34f 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -5,6 +5,49 @@ import { ILightningApi } from '../lightning/lightning-api.interface'; import { ITopNodesPerCapacity, ITopNodesPerChannels } from '../../mempool.interfaces'; class NodesApi { + public async $getWorldNodes(): Promise { + 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 { 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' diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index 7a5ff880a..cf3f75208 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -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 { 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( diff --git a/backend/src/api/fetch-version.ts b/backend/src/api/fetch-version.ts new file mode 100644 index 000000000..cb0813c35 --- /dev/null +++ b/backend/src/api/fetch-version.ts @@ -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" +); diff --git a/backend/src/config.ts b/backend/src/config.ts index 8c91e104b..43ba16cb3 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -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; diff --git a/backend/src/tasks/lightning/network-sync.service.ts b/backend/src/tasks/lightning/network-sync.service.ts index b9b7df92c..8d8f1759f 100644 --- a/backend/src/tasks/lightning/network-sync.service.ts +++ b/backend/src/tasks/lightning/network-sync.service.ts @@ -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 { + 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)); } diff --git a/backend/src/tasks/price-updater.ts b/backend/src/tasks/price-updater.ts index 81066efb2..891e2bce3 100644 --- a/backend/src/tasks/price-updater.ts +++ b/backend/src/tasks/price-updater.ts @@ -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) { diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 8d13bc7f4..9be457bb2 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -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 diff --git a/docker/backend/start.sh b/docker/backend/start.sh old mode 100644 new mode 100755 index 8e6a34013..ade739f1f --- a/docker/backend/start.sh +++ b/docker/backend/start.sh @@ -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 diff --git a/docker/backend/wait-for-it.sh b/docker/backend/wait-for-it.sh old mode 100644 new mode 100755 diff --git a/docker/init.sh b/docker/init.sh index 49b53d646..4eb06f0c1 100755 --- a/docker/init.sh +++ b/docker/init.sh @@ -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" diff --git a/frontend/angular.json b/frontend/angular.json index 4eb697071..1ed29cad9 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -170,6 +170,10 @@ }, "configurations": { "production": { + "assets": [ + "src/favicon.ico", + "src/robots.txt" + ], "fileReplacements": [ { "replace": "src/environments/environment.ts", diff --git a/frontend/package.json b/frontend/package.json index d2f7f2f6c..b5055a5de 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 36d765a17..5cfb2c905 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -187,8 +187,8 @@ -
-

Self-Hosted Integrations

+
+

Community Integrations

-
- -
-

Wallet Integrations

-
+ + + BTCPay + Bisq + + + BlueWallet + + + + Muun + - + Electrum @@ -244,18 +250,14 @@ Phoenix + + + LNBits + Mercury - - - Muun - - - - BlueWallet - Blixt diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index 2cd755bde..2cc5d5102 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -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; } diff --git a/frontend/src/app/components/asset-circulation/asset-circulation.component.ts b/frontend/src/app/components/asset-circulation/asset-circulation.component.ts index 4d65417e6..d64607fe1 100644 --- a/frontend/src/app/components/asset-circulation/asset-circulation.component.ts +++ b/frontend/src/app/components/asset-circulation/asset-circulation.component.ts @@ -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({ diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index 1b02ceceb..7ea01584e 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -5977,6 +5977,2491 @@ export const restApiDocsData = [ } } }, + { + type: "category", + category: "lightning", + fragment: "lightning", + title: "Lightning", + showConditions: bitcoinNetworks + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-lightning-network-stats", + title: "GET Network Stats", + description: { + default: "

Returns network-wide stats such as total number of channels and nodes, total capacity, and average/median fee figures.

Pass one of the following for :interval: latest, 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y.

" + }, + urlString: "/v1/lightning/statistics/:interval", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/statistics/%{1}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`latest`], + response: `{ + "latest": { + "id": 163, + "added": "2022-08-30T00:00:00.000Z", + "channel_count": 81690, + "node_count": 15851, + "total_capacity": 460820222344, + "tor_nodes": 11455, + "clearnet_nodes": 2305, + "unannounced_nodes": 974, + "avg_capacity": 5641085, + "avg_fee_rate": 497, + "avg_base_fee_mtokens": 915, + "med_capacity": 1500000, + "med_fee_rate": 40, + "med_base_fee_mtokens": 100, + "clearnet_tor_nodes": 1117 + } +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`latest`], + response: `{ + "latest": { + "id": 13, + "added": "2022-08-30T00:00:00.000Z", + "channel_count": 5101, + "node_count": 1806, + "total_capacity": 43341092977, + "tor_nodes": 288, + "clearnet_nodes": 736, + "unannounced_nodes": 656, + "avg_capacity": 8496588, + "avg_fee_rate": 354, + "avg_base_fee_mtokens": 1183, + "med_capacity": 1148313, + "med_fee_rate": 1, + "med_base_fee_mtokens": 1000, + "clearnet_tor_nodes": 126 + } +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`latest`], + response: `{ + "latest": { + "id": 13, + "added": "2022-08-30T00:00:00.000Z", + "channel_count": 33, + "node_count": 24, + "total_capacity": 161821884, + "tor_nodes": 5, + "clearnet_nodes": 11, + "unannounced_nodes": 6, + "avg_capacity": 4903693, + "avg_fee_rate": 38, + "avg_base_fee_mtokens": 1061, + "med_capacity": 2000000, + "med_fee_rate": 1, + "med_base_fee_mtokens": 1000, + "clearnet_tor_nodes": 2 + } +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-lightning-nodes-channels", + title: "GET Nodes/Channels", + description: { + default: "

Returns Lightning nodes and channels that match a full-text, case-insensitive search :query across node aliases, node pubkeys, channel IDs, and short channel IDs.

" + }, + urlString: "/v1/lightning/search?searchText=:query", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/search?searchText=%{1}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`ACINQ`], + response: `{ + "nodes": [ + { + "public_key": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "alias": "ACINQ", + "capacity": 35920090247, + "channels": 2907 + }, + { + "public_key": "03d3902b46d6ab9558a76cbf91b27d093c0a3c54e59f33c7eb4bd643dbb3b1b5b0", + "alias": "Acinq", + "capacity": null, + "channels": null + } + ], + "channels": [] +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`lnd`], + response: `{ + "nodes": [ + { + "public_key": "02be8f360e57600486b93dd33ea0872a4e14a259924ba4084f27d693a77d151158", + "alias": "lndus1.dev.zaphq.io", + "capacity": 762968876, + "channels": 27 + }, + { + "public_key": "028c3640c57ffe47eb41db8225968833c5032f297aeba98672d6f7037090d59e3f", + "alias": "lndus0.next.zaphq.io", + "capacity": 641040063, + "channels": 26 + } + ], + "channels": [] +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`guggero`], + response: `{ + "nodes": [ + { + "public_key": "02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120", + "alias": "guggero", + "capacity": 66577093, + "channels": 12, + "status": 1 + } + ], + "channels": [] +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-lightning-nodes-country", + title: "GET Nodes in Country", + description: { + default: "

Returns a list of Lightning nodes running on clearnet in the requested :country, where :country is an ISO Alpha-2 country code.

" + }, + urlString: "/v1/lightning/nodes/country/:country", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/nodes/country/%{1}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`ch`], + response: `{ + "country": { + "de": "Schweiz", + "en": "Switzerland", + "es": "Suiza", + "fr": "Suisse", + "ja": "スイス連邦", + "pt-BR": "Suíça", + "ru": "Швейцария", + "zh-CN": "瑞士" + }, + "nodes": [ + { + "public_key": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "capacity": 54339697486, + "channels": 991, + "alias": "bfx-lnd0", + "first_seen": 1574813156, + "updated_at": 1661814056, + "city": { + "de": "Zürich", + "en": "Zurich", + "es": "Zúrich", + "fr": "Zurich", + "ja": "チューリッヒ", + "pt-BR": "Zurique", + "ru": "Цюрих", + "zh-CN": "苏黎世" + }, + "country": { + "de": "Schweiz", + "en": "Switzerland", + "es": "Suiza", + "fr": "Suisse", + "ja": "スイス連邦", + "pt-BR": "Suíça", + "ru": "Швейцария", + "zh-CN": "瑞士" + }, + "iso_code": "CH", + "subdivision": { + "de": "Zürich", + "en": "Zurich", + "fr": "Zurich" + } + }, + ... + ] +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`ch`], + response: `{ + "country": { + "de": "Schweiz", + "en": "Switzerland", + "es": "Suiza", + "fr": "Suisse", + "ja": "スイス連邦", + "pt-BR": "Suíça", + "ru": "Швейцария", + "zh-CN": "瑞士" + }, + "nodes": [ + { + "public_key": "0200a7f20e51049363cb7f2a0865fe072464d469dca0ac34c954bb3d4b552b6e95", + "capacity": 94802991, + "channels": 15, + "alias": "looptest", + "first_seen": 1601298108, + "updated_at": 1661857089, + "city": { + "de": "Thun", + "en": "Thun", + "es": "Thun", + "fr": "Thoune", + "ja": "トゥーン", + "pt-BR": "Tune", + "ru": "Тун", + "zh-CN": "图恩" + }, + "country": { + "de": "Schweiz", + "en": "Switzerland", + "es": "Suiza", + "fr": "Suisse", + "ja": "スイス連邦", + "pt-BR": "Suíça", + "ru": "Швейцария", + "zh-CN": "瑞士" + }, + "iso_code": "CH", + "subdivision": { + "de": "Bern", + "en": "Bern", + "fr": "Berne" + } + }, + ... + ] +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`us`], + response: `{ + "country": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + }, + "nodes": [ + { + "public_key": "03f70ac4525c0014bbf380c069ce82d70946d37a56c027a2ed18609a3e60c3b353", + "capacity": 2000000, + "channels": 1, + "alias": "", + "first_seen": 1637708194, + "updated_at": 0, + "city": { + "en": "Oak Park", + "ru": "Оук-Парк" + }, + "country": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + }, + "iso_code": "US", + "subdivision": { + "en": "Illinois", + "es": "Illinois", + "fr": "Illinois", + "ja": "イリノイ州", + "pt-BR": "Ilinóis", + "ru": "Иллинойс", + "zh-CN": "伊利诺伊州" + } + }, + { + "public_key": "0397b15fd867541c53a3a5e27c021f7acad582684d05d120af572266c92c8a0313", + "capacity": 19802, + "channels": 1, + "alias": "pseudozach", + "first_seen": 1637620444, + "updated_at": 1637721257, + "city": { + "de": "Atlanta", + "en": "Atlanta", + "es": "Atlanta", + "fr": "Atlanta", + "ja": "アトランタ", + "pt-BR": "Atlanta", + "ru": "Атланта", + "zh-CN": "亚特兰大" + }, + "country": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + }, + "iso_code": "US", + "subdivision": { + "en": "Georgia", + "es": "Georgia", + "fr": "Géorgie", + "ja": "ジョージア州", + "pt-BR": "Geórgia", + "ru": "Джорджия", + "zh-CN": "乔治亚" + } + }, + ... + ] +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-country-node-stats", + title: "GET Node Stats Per Country", + description: { + default: "

Returns aggregate capacity and number of clearnet nodes per country. Capacity figures are in satoshis.

" + }, + urlString: "/v1/lightning/nodes/countries", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/nodes/countries`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + { + "name": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + }, + "iso": "US", + "count": 2775, + "share": 34.53, + "capacity": "372732844657" + }, + { + "name": { + "de": "Frankreich", + "en": "France", + "es": "Francia", + "fr": "France", + "ja": "フランス共和国", + "pt-BR": "França", + "ru": "Франция", + "zh-CN": "法国" + }, + "iso": "FR", + "count": 972, + "share": 12.09, + "capacity": "7740713270" + }, + ... +]` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + { + "name": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + }, + "iso": "US", + "count": 304, + "share": 37.95, + "capacity": "23906225936" + }, + { + "name": { + "de": "Deutschland", + "en": "Germany", + "es": "Alemania", + "fr": "Allemagne", + "ja": "ドイツ連邦共和国", + "pt-BR": "Alemanha", + "ru": "Германия", + "zh-CN": "德国" + }, + "iso": "DE", + "count": 85, + "share": 10.61, + "capacity": "1878052329" + }, + ... +]` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + { + "name": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + }, + "iso": "US", + "count": 4, + "share": 36.36, + "capacity": "2059317" + }, + { + "name": { + "de": "Japan", + "en": "Japan", + "es": "Japón", + "fr": "Japon", + "ja": "日本", + "pt-BR": "Japão", + "ru": "Япония", + "zh-CN": "日本" + }, + "iso": "JP", + "count": 2, + "share": 18.18, + "capacity": "107710417" + }, + ... +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-isp-nodes", + title: "GET ISP Nodes", + description: { + default: "

Returns a list of nodes hosted by a specified :isp, where :isp is an ISP's ASN.

" + }, + urlString: "/v1/lightning/nodes/isp/:isp", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/nodes/isp/%{1}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`16509`], + response: `{ + "isp": "Amazon.com", + "nodes": [ + { + "public_key": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "capacity": 36010390247, + "channels": 2907, + "alias": "ACINQ", + "first_seen": 1522941222, + "updated_at": 1661274935, + "city": null, + "country": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + }, + "iso_code": "US", + "subdivision": null + }, + ... + ] +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`16509`], + response: `{ + "isp": "Amazon.com", + "nodes": [ + { + "public_key": "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", + "capacity": 2041664924, + "channels": 70, + "alias": "endurance", + "first_seen": 1566809576, + "updated_at": 1660926529, + "city": null, + "country": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + }, + "iso_code": "US", + "subdivision": null + }, + ... + ] +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`7684`], + response: `{ + "isp": "SAKURA Internet", + "nodes": [ + { + "public_key": "02dadf6c28f3284d591cd2a4189d1530c1ff82c07059ebea150a33ab76e7364b4a", + "capacity": 51155987, + "channels": 15, + "alias": "珠美ノード⚡@wakiyamap", + "first_seen": 1612221581, + "updated_at": 1662382573, + "city": null, + "country": { + "de": "Japan", + "en": "Japan", + "es": "Japón", + "fr": "Japon", + "ja": "日本", + "pt-BR": "Japão", + "ru": "Япония", + "zh-CN": "日本" + }, + "iso_code": "JP", + "subdivision": null + } + ] +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-isp-node-stats", + title: "GET Node Stats Per ISP", + description: { + default: "

Returns aggregate capacity, number of nodes, and number of channels per ISP. Capacity figures are in satoshis.

" + }, + urlString: "/v1/lightning/nodes/isp-ranking", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/nodes/isp-ranking`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "clearnetCapacity": 417154330493, + "torCapacity": 36605381932, + "unknownCapacity": 6678700534, + "ispRanking": [ + [ + "14061", //ASN + "DigitalOcean", //ISP name + 43681728521, //aggregate capacity, in sats + 5028, //total number of channels + 192 //number of nodes + ], + [ + "701", + "Verizon Internet Services", + 3047086363, + 507, + 55 + ], + [ + "396982,15169", + "Google Cloud", + 139554933568, + 2747, + 78 + ], + ... + ] +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "clearnetCapacity": 21714967205, + "torCapacity": 1183591190, + "unknownCapacity": 965032372, + "ispRanking": [ + [ + "14080", //ASN + "Telmex Colombia S.A.", //ISP Name + 220063321, //aggregate capacity, in sats + 98, //total number of channels + 1 //number of nodes + ], + [ + "16509,14618", + "Amazon.com", + 5590657952, + 445, + 41 + ], + ... + ] +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "clearnetCapacity": 126914725, + "torCapacity": 1000000, + "unknownCapacity": 31150000, + "ispRanking": [ + [ + "1136", + "KPN", + 99878, + 1, + 1 + ], + [ + "24940", + "Hetzner Online GmbH", + 34877093, + 6, + 1 + ], + ... + ] +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-top-100-nodes", + title: "GET Top 100 Nodes", + description: { + default: "

Returns two lists of the top 100 nodes: one ordered by liquidity (aggregate channel capacity) and the other ordered by connectivity (number of open channels).

" + }, + urlString: "/v1/lightning/nodes/rankings", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/nodes/rankings`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "topByCapacity": [ + { + "publicKey": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "alias": "bfx-lnd0", + "capacity": 54361697486 + }, + { + "publicKey": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "alias": "ACINQ", + "capacity": 36010516297 + }, + ... + ], + "topByChannels": [ + { + "publicKey": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "alias": "ACINQ", + "channels": 2908 + }, + { + "publicKey": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "alias": "WalletOfSatoshi.com", + "channels": 2771 + }, + ... + ] +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "topByCapacity": [ + { + "publicKey": "038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9", + "alias": "aranguren.org", + "capacity": 17155095532 + }, + { + "publicKey": "0225ff2ae6a3d9722b625072503c2f64f6eddb78d739379d2ee55a16b3b0ed0a17", + "alias": "STRANGEIRON", + "capacity": 7038263480 + }, + ... + ], + "topByChannels": [ + { + "publicKey": "038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9", + "alias": "aranguren.org", + "channels": 489 + }, + { + "publicKey": "030425d8babe3ab6dfc065e69dd8b10ce6738c86ea7d634324c913e21620fa5eaf", + "alias": "MTest441", + "channels": 258 + }, + ... + ] +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + "topByCapacity": [ + { + "publicKey": "02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120", + "alias": "guggero", + "capacity": 66577093 + }, + { + "publicKey": "0271cf3881e6eadad960f47125434342e57e65b98a78afa99f9b4191c02dd7ab3b", + "alias": "eclair@wakiyamap", + "capacity": 56554430 + }, + ... + ], + "topByChannels": [ + { + "publicKey": "02dadf6c28f3284d591cd2a4189d1530c1ff82c07059ebea150a33ab76e7364b4a", + "alias": "珠美ノード⚡@wakiyamap", + "channels": 15 + }, + { + "publicKey": "02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120", + "alias": "guggero", + "channels": 12 + }, + ... + ] +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-top-100-nodes-liquidity", + title: "GET Top 100 Nodes by Liquidity", + description: { + default: "

Returns a list of the top 100 nodes by liquidity (aggregate channel capacity).

" + }, + urlString: "/v1/lightning/nodes/rankings/liquidity", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/nodes/rankings/liquidity`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + { + "publicKey": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "alias": "bfx-lnd0", + "capacity": 54361697486, + "channels": 993, + "firstSeen": 1574813156, + "updatedAt": 1661814056, + "city": { + "de": "Zürich", + "en": "Zurich", + "es": "Zúrich", + "fr": "Zurich", + "ja": "チューリッヒ", + "pt-BR": "Zurique", + "ru": "Цюрих", + "zh-CN": "苏黎世" + }, + "country": { + "de": "Schweiz", + "en": "Switzerland", + "es": "Suiza", + "fr": "Suisse", + "ja": "スイス連邦", + "pt-BR": "Suíça", + "ru": "Швейцария", + "zh-CN": "瑞士" + } + }, + { + "publicKey": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "alias": "ACINQ", + "capacity": 36010516297, + "channels": 2908, + "firstSeen": 1522941222, + "updatedAt": 1661274935, + "city": null, + "country": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + } + }, + ... +]` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + { + "publicKey": "038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9", + "alias": "aranguren.org", + "capacity": 17155095532, + "channels": 489, + "firstSeen": 1521457251, + "updatedAt": 1662035238, + "city": { + "de": "Melbourne", + "en": "Melbourne", + "es": "Melbourne", + "fr": "Melbourne", + "ja": "メルボルン", + "pt-BR": "Melbourne", + "ru": "Мельбурн", + "zh-CN": "墨尔本" + }, + "country": { + "de": "Australien", + "en": "Australia", + "es": "Australia", + "fr": "Australie", + "ja": "オーストラリア", + "pt-BR": "Austrália", + "ru": "Австралия", + "zh-CN": "澳大利亚" + } + }, + { + "publicKey": "0225ff2ae6a3d9722b625072503c2f64f6eddb78d739379d2ee55a16b3b0ed0a17", + "alias": "STRANGEIRON", + "capacity": 7038263480, + "channels": 95, + "firstSeen": 1651725065, + "updatedAt": 1661958465, + "city": { + "de": "Melbourne", + "en": "Melbourne", + "es": "Melbourne", + "fr": "Melbourne", + "ja": "メルボルン", + "pt-BR": "Melbourne", + "ru": "Мельбурн", + "zh-CN": "墨尔本" + }, + "country": { + "de": "Australien", + "en": "Australia", + "es": "Australia", + "fr": "Australie", + "ja": "オーストラリア", + "pt-BR": "Austrália", + "ru": "Австралия", + "zh-CN": "澳大利亚" + } + }, + ... +]` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + { + "publicKey": "02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120", + "alias": "guggero", + "capacity": 66577093, + "channels": 12, + "firstSeen": 1608832520, + "updatedAt": 1662440260, + "city": null, + "country": { + "de": "Deutschland", + "en": "Germany", + "es": "Alemania", + "fr": "Allemagne", + "ja": "ドイツ連邦共和国", + "pt-BR": "Alemanha", + "ru": "Германия", + "zh-CN": "德国" + } + }, + { + "publicKey": "0271cf3881e6eadad960f47125434342e57e65b98a78afa99f9b4191c02dd7ab3b", + "alias": "eclair@wakiyamap", + "capacity": 56554430, + "channels": 4, + "firstSeen": 1628031165, + "updatedAt": 1648064593, + "city": { + "de": "Ōsaka", + "en": "Osaka", + "es": "Osaka", + "fr": "Osaka", + "ja": "大阪市", + "pt-BR": "Osaka", + "ru": "Осака" + }, + "country": { + "de": "Japan", + "en": "Japan", + "es": "Japón", + "fr": "Japon", + "ja": "日本", + "pt-BR": "Japão", + "ru": "Япония", + "zh-CN": "日本" + } + }, + ... +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-top-100-nodes-connectivity", + title: "GET Top 100 Nodes by Connectivity", + description: { + default: "

Returns a list of the top 100 nodes by connectivity (number of open channels).

" + }, + urlString: "/v1/lightning/nodes/rankings/connectivity", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/nodes/rankings/connectivity`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + { + "publicKey": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "alias": "ACINQ", + "channels": 2908, + "capacity": 36010516297, + "firstSeen": 1522941222, + "updatedAt": 1661274935, + "city": null, + "country": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + } + }, + { + "publicKey": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "alias": "WalletOfSatoshi.com", + "channels": 2772, + "capacity": 15464503162, + "firstSeen": 1601429940, + "updatedAt": 1661812116, + "city": { + "de": "Vancouver", + "en": "Vancouver", + "es": "Vancouver", + "fr": "Vancouver", + "ja": "バンクーバー市", + "pt-BR": "Vancôver", + "ru": "Ванкувер" + }, + "country": { + "de": "Kanada", + "en": "Canada", + "es": "Canadá", + "fr": "Canada", + "ja": "カナダ", + "pt-BR": "Canadá", + "ru": "Канада", + "zh-CN": "加拿大" + } + }, + ... +]` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + { + "publicKey": "038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9", + "alias": "aranguren.org", + "channels": 489, + "capacity": 17155095532, + "firstSeen": 1521457251, + "updatedAt": 1662035238, + "city": { + "de": "Melbourne", + "en": "Melbourne", + "es": "Melbourne", + "fr": "Melbourne", + "ja": "メルボルン", + "pt-BR": "Melbourne", + "ru": "Мельбурн", + "zh-CN": "墨尔本" + }, + "country": { + "de": "Australien", + "en": "Australia", + "es": "Australia", + "fr": "Australie", + "ja": "オーストラリア", + "pt-BR": "Austrália", + "ru": "Австралия", + "zh-CN": "澳大利亚" + } + }, + { + "publicKey": "030425d8babe3ab6dfc065e69dd8b10ce6738c86ea7d634324c913e21620fa5eaf", + "alias": "MTest441", + "channels": 258, + "capacity": 4113430492, + "firstSeen": 1640955758, + "updatedAt": 1662035216, + "city": null, + "country": null + }, + { + "publicKey": "0270685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b", + "alias": "0270685ca81a8e4d4d01", + "channels": 164, + "capacity": 638119030, + "firstSeen": 1535613050, + "updatedAt": 1662034882, + "city": { + "de": "Clifton", + "en": "Clifton", + "ja": "クリフトン", + "pt-BR": "Clifton", + "ru": "Клифтон", + "zh-CN": "克利夫頓" + }, + "country": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + } + }, + ... +]` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + { + "publicKey": "02dadf6c28f3284d591cd2a4189d1530c1ff82c07059ebea150a33ab76e7364b4a", + "alias": "珠美ノード⚡@wakiyamap", + "channels": 15, + "capacity": 51155987, + "firstSeen": 1612221581, + "updatedAt": 1662382573, + "city": null, + "country": { + "de": "Japan", + "en": "Japan", + "es": "Japón", + "fr": "Japon", + "ja": "日本", + "pt-BR": "Japão", + "ru": "Япония", + "zh-CN": "日本" + } + }, + { + "publicKey": "02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120", + "alias": "guggero", + "channels": 12, + "capacity": 66577093, + "firstSeen": 1608832520, + "updatedAt": 1662440260, + "city": null, + "country": { + "de": "Deutschland", + "en": "Germany", + "es": "Alemania", + "fr": "Allemagne", + "ja": "ドイツ連邦共和国", + "pt-BR": "Alemanha", + "ru": "Германия", + "zh-CN": "德国" + } + }, + ... +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-top-100-oldest-nodes", + title: "GET Top 100 Oldest Nodes", + description: { + default: "

Returns a list of the top 100 oldest nodes.

" + }, + urlString: "/v1/lightning/nodes/rankings/age", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/nodes/rankings/age`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + { + "publicKey": "02d4531a2f2e6e5a9033d37d548cff4834a3898e74c3abe1985b493c42ebbd707d", + "alias": "coinfinity.co", + "channels": 13, + "capacity": 35945717, + "firstSeen": 1518001533, + "updatedAt": 1661713804, + "city": { + "de": "Brüssel", + "en": "Brussels", + "es": "Bruselas", + "fr": "Bruxelles", + "ja": "ブリュッセル", + "pt-BR": "Bruxelas", + "ru": "Брюссель", + "zh-CN": "布鲁塞尔" + }, + "country": { + "de": "Belgien", + "en": "Belgium", + "es": "Bélgica", + "fr": "Belgique", + "ja": "ベルギー王国", + "pt-BR": "Bélgica", + "ru": "Бельгия", + "zh-CN": "比利时" + } + }, + { + "publicKey": "024bd94f0425590434538fd21d4e58982f7e9cfd8f339205a73deb9c0e0341f5bd", + "alias": "CL.rompert.com🔵 ", + "channels": 2, + "capacity": 600000, + "firstSeen": 1520596684, + "updatedAt": 1603261631, + "city": { + "de": "Clifton", + "en": "Clifton", + "ja": "クリフトン", + "pt-BR": "Clifton", + "ru": "Клифтон", + "zh-CN": "克利夫頓" + }, + "country": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + } + }, + ... +]` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + { + "publicKey": "038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9", + "alias": "aranguren.org", + "channels": 489, + "capacity": 17155095532, + "firstSeen": 1521457251, + "updatedAt": 1662035238, + "city": { + "de": "Melbourne", + "en": "Melbourne", + "es": "Melbourne", + "fr": "Melbourne", + "ja": "メルボルン", + "pt-BR": "Melbourne", + "ru": "Мельбурн", + "zh-CN": "墨尔本" + }, + "country": { + "de": "Australien", + "en": "Australia", + "es": "Australia", + "fr": "Australie", + "ja": "オーストラリア", + "pt-BR": "Austrália", + "ru": "Австралия", + "zh-CN": "澳大利亚" + } + }, + { + "publicKey": "0277622bf4c497475960bf91bd3c673a4cb4e9b589cebfde9700c197b3989cc1b8", + "alias": "CoinGate", + "channels": 11, + "capacity": 91768515, + "firstSeen": 1525964963, + "updatedAt": 1661991683, + "city": { + "de": "Frankfurt am Main", + "en": "Frankfurt am Main", + "es": "Francfort", + "fr": "Francfort-sur-le-Main", + "ja": "フランクフルト・アム・マイン", + "pt-BR": "Frankfurt am Main", + "ru": "Франкфурт", + "zh-CN": "法兰克福" + }, + "country": { + "de": "Deutschland", + "en": "Germany", + "es": "Alemania", + "fr": "Allemagne", + "ja": "ドイツ連邦共和国", + "pt-BR": "Alemanha", + "ru": "Германия", + "zh-CN": "德国" + } + }, + ... +]` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + { + "publicKey": "02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120", + "alias": "guggero", + "channels": 12, + "capacity": 66577093, + "firstSeen": 1608832520, + "updatedAt": 1662440260, + "city": null, + "country": { + "de": "Deutschland", + "en": "Germany", + "es": "Alemania", + "fr": "Allemagne", + "ja": "ドイツ連邦共和国", + "pt-BR": "Alemanha", + "ru": "Германия", + "zh-CN": "德国" + } + }, + { + "publicKey": "03870a4c4c54a9b2e705023d706843ffbac5b0e95e2b80d4b02dc7a9efb5380322", + "alias": "03870a4c4c54a9b2e705", + "channels": 2, + "capacity": 30000000, + "firstSeen": 1608832520, + "updatedAt": 0, + "city": null, + "country": null + }, + ... +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-node-stats", + title: "GET Node Stats", + description: { + default: "

Returns details about a node with the given :pubKey.

" + }, + urlString: "/v1/lightning/nodes/:pubKey", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/nodes/%{1}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`033ac2f9f7ff643c235cc247c521663924aff73b26b38118a6c6821460afcde1b3`], + response: `{ + "public_key": "033ac2f9f7ff643c235cc247c521663924aff73b26b38118a6c6821460afcde1b3", + "alias": "Red.de.Rayos", + "first_seen": 1521504055, + "updated_at": 1661869523, + "color": "#68f442", + "sockets": "84.44.203.181:9735", + "as_number": 8422, + "city_id": 2886242, + "country_id": 2921044, + "subdivision_id": 2861876, + "longitude": 6.9489, + "latitude": 50.9298, + "iso_code": "DE", + "as_organization": "NetCologne GmbH", + "city": { + "de": "Köln", + "en": "Cologne", + "es": "Colonia", + "fr": "Cologne", + "ja": "ケルン", + "pt-BR": "Colônia", + "ru": "Кёльн", + "zh-CN": "科隆" + }, + "country": { + "de": "Deutschland", + "en": "Germany", + "es": "Alemania", + "fr": "Allemagne", + "ja": "ドイツ連邦共和国", + "pt-BR": "Alemanha", + "ru": "Германия", + "zh-CN": "德国" + }, + "subdivision": { + "de": "Nordrhein-Westfalen", + "en": "North Rhine-Westphalia", + "es": "Renania del Norte-Westfalia", + "fr": "Rhénanie du Nord-Westphalie", + "ru": "Северный Рейн-Вестфалия" + }, + "active_channel_count": 55, + "capacity": "31505027", + "opened_channel_count": 55, + "closed_channel_count": 111 +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`03f060953bef5b777dc77e44afa3859d022fc1a77c55138deb232ad7255e869c00`], + response: `{ + "public_key": "03f060953bef5b777dc77e44afa3859d022fc1a77c55138deb232ad7255e869c00", + "alias": "Boltz", + "first_seen": 1551006126, + "updated_at": 1662033208, + "color": "#ff9800", + "sockets": "35.237.24.136:9735,idz7qlezif6hgmjkpmuelnsssyxea2lwan562a5gla7jmlxsl5cb2cad.onion:9735", + "as_number": 396982, + "city_id": 4589387, + "country_id": 6252001, + "subdivision_id": 4597040, + "longitude": -79.9746, + "latitude": 32.8608, + "iso_code": "US", + "as_organization": "Google Cloud", + "city": { + "en": "North Charleston", + "ja": "ノースチャールストン", + "pt-BR": "North Charleston", + "ru": "Норт-Чарлстон", + "zh-CN": "北查尔斯顿" + }, + "country": { + "de": "Vereinigte Staaten", + "en": "United States", + "es": "Estados Unidos", + "fr": "États Unis", + "ja": "アメリカ", + "pt-BR": "EUA", + "ru": "США", + "zh-CN": "美国" + }, + "subdivision": { + "en": "South Carolina", + "es": "Carolina del Sur", + "fr": "Caroline du Sud", + "ja": "サウスカロライナ州", + "pt-BR": "Carolina do Sul", + "ru": "Южная Каролина", + "zh-CN": "南卡罗来纳州" + }, + "active_channel_count": 46, + "capacity": "111724126", + "opened_channel_count": 165, + "closed_channel_count": 1 +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120`], + response: `{ + "public_key": "02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120", + "alias": "guggero", + "first_seen": 1608832520, + "updated_at": 1662440260, + "color": "#cccccc", + "sockets": "88.99.101.67:9735", + "as_number": 24940, + "city_id": null, + "country_id": 2921044, + "subdivision_id": null, + "longitude": 9.491, + "latitude": 51.2993, + "iso_code": "DE", + "as_organization": "Hetzner Online GmbH", + "city": null, + "country": { + "de": "Deutschland", + "en": "Germany", + "es": "Alemania", + "fr": "Allemagne", + "ja": "ドイツ連邦共和国", + "pt-BR": "Alemanha", + "ru": "Германия", + "zh-CN": "德国" + }, + "subdivision": null, + "active_channel_count": 12, + "capacity": "66577093", + "opened_channel_count": 16, + "closed_channel_count": 0 +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-historical-node-stats", + title: "GET Historical Node Stats", + description: { + default: "

Returns historical stats for a node with the given :pubKey.

" + }, + urlString: "/v1/lightning/nodes/:pubKey/statistics", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/nodes/%{1}/statistics`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`033ac2f9f7ff643c235cc247c521663924aff73b26b38118a6c6821460afcde1b3`], + response: `[ + { + "added": 1661817600, + "capacity": 31505027, + "channels": 55 + }, + { + "added": 1661731200, + "capacity": 31505027, + "channels": 55 + }, + { + "added": 1655078400, + "capacity": 26487523, + "channels": 43 + }, + { + "added": 1654992000, + "capacity": 32692287, + "channels": 57 + }, + { + "added": 1654905600, + "capacity": 32692287, + "channels": 57 + }, + ... +]` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`0225ff2ae6a3d9722b625072503c2f64f6eddb78d739379d2ee55a16b3b0ed0a17`], + response: `[ + { + "added": 1662422400, + "capacity": 7038263480, + "channels": 95 + }, + { + "added": 1662336000, + "capacity": 7038263480, + "channels": 95 + }, + { + "added": 1662249600, + "capacity": 7038263480, + "channels": 95 + }, + ... +]` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120`], + response: `[ + { + "added": 1662422400, + "capacity": 66577093, + "channels": 12 + }, + { + "added": 1662336000, + "capacity": 63477093, + "channels": 9 + }, + ... +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-channel", + title: "GET Channel", + description: { + default: "

Returns info about a Lightning channel with the given :channelId.

" + }, + urlString: "/v1/lightning/channels/:channelId", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/channels/%{1}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`768457472831193088`], + response: `{ + "id": "768457472831193088", + "short_id": "698908x1305x0", + "capacity": 16777215, + "transaction_id": "9f248ff82f6ff4c112c218438cfde8260623663bc85a360d09a13b9a9b083564", + "transaction_vout": 0, + "closing_transaction_id": null, + "closing_reason": null, + "updated_at": "2022-08-25T23:05:40.000Z", + "created": "2021-09-04T00:10:42.000Z", + "status": 1, + "node_left": { + "alias": "CoinGate", + "public_key": "0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3", + "channels": 1, + "capacity": 20000, + "base_fee_mtokens": 1000, + "cltv_delta": 0, + "fee_rate": 1, + "is_disabled": 0, + "max_htlc_mtokens": 16609443000, + "min_htlc_mtokens": 1000, + "updated_at": "2022-08-25T23:05:40.000Z", + "longitude": 8.6843, + "latitude": 50.1188 + }, + "node_right": { + "alias": "Blixt Wallet 🟡", + "public_key": "0230a5bca558e6741460c13dd34e636da28e52afd91cf93db87ed1b0392a7466eb", + "channels": 3, + "capacity": 34754430, + "base_fee_mtokens": 1000, + "cltv_delta": 0, + "fee_rate": 180, + "is_disabled": 0, + "max_htlc_mtokens": 16609443000, + "min_htlc_mtokens": 1000, + "updated_at": "2022-08-25T18:44:00.000Z", + "longitude": 9.491, + "latitude": 51.2993 + } +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`2478509215728271360`], + response: `{ + "id": "2478509215728271360", + "short_id": "2254191x4x0", + "capacity": 16777215, + "transaction_id": "6b711b07b019d73ad432f401c01ac6ea253fbe2778388e5a686b5777678556c7", + "transaction_vout": 0, + "closing_transaction_id": null, + "closing_reason": null, + "updated_at": "2022-08-31T08:30:42.000Z", + "created": "2022-06-05T16:26:31.000Z", + "status": 1, + "node_left": { + "alias": "scarce-city-testnet", + "public_key": "0304fa1da67d441b382e3b2142a1980840276d89b6477812da8d26487b5ffa938c", + "channels": 15, + "capacity": 104876207, + "base_fee_mtokens": 1000, + "cltv_delta": 0, + "fee_rate": 1, + "is_disabled": 0, + "max_htlc_mtokens": 16777215000, + "min_htlc_mtokens": 1000, + "updated_at": "2022-08-31T08:30:42.000Z", + "longitude": -123.1236, + "latitude": 49.2526 + }, + "node_right": { + "alias": "STRANGEIRON", + "public_key": "0225ff2ae6a3d9722b625072503c2f64f6eddb78d739379d2ee55a16b3b0ed0a17", + "channels": 95, + "capacity": 7038263480, + "base_fee_mtokens": 0, + "cltv_delta": 0, + "fee_rate": 10, + "is_disabled": 0, + "max_htlc_mtokens": 16609443000, + "min_htlc_mtokens": 1, + "updated_at": "2022-08-27T20:22:06.000Z", + "longitude": 144.9669, + "latitude": -37.8159 + } +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`58998694435160064`], + response: `{ + "id": "58998694435160064", + "short_id": "53659x5x0", + "capacity": 16777215, + "transaction_id": "cbb18e4b23c2a27736fa5be559fee7efcc855f2dfb1f16b125f686c307513ef3", + "transaction_vout": 0, + "closing_transaction_id": null, + "closing_reason": null, + "updated_at": "2022-09-04T10:15:51.000Z", + "created": "2021-09-02T07:08:40.000Z", + "status": 1, + "node_left": { + "alias": "STRANGESUCKER-v0.12.0-11-gea4143", + "public_key": "03b9e6c1dec203f47efc95d003314d22cbb12a1324de4b091fe7d68f321a56322f", + "channels": 4, + "capacity": 55778192, + "base_fee_mtokens": 0, + "cltv_delta": 0, + "fee_rate": 0, + "is_disabled": 0, + "max_htlc_mtokens": 16609443000, + "min_htlc_mtokens": 1, + "updated_at": "2022-09-04T10:15:51.000Z", + "longitude": 19.1477, + "latitude": 48.7386 + }, + "node_right": { + "alias": "guggero", + "public_key": "02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120", + "channels": 12, + "capacity": 66577093, + "base_fee_mtokens": 1000, + "cltv_delta": 0, + "fee_rate": 1, + "is_disabled": 0, + "max_htlc_mtokens": 16777215000, + "min_htlc_mtokens": 1000, + "updated_at": "2022-09-01T22:57:40.000Z", + "longitude": 9.491, + "latitude": 51.2993 + } +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-channels-from-txid", + title: "GET Channels from TXID", + description: { + default: "

Returns channels that correspond to the given :txid (multiple transaction IDs can be specified).

" + }, + urlString: "/v1/lightning/channels/txids?txId[]=:txid", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/channels/txids?txId[]=%{1}&txId[]=%{2}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`c3173549f502ede6440d5c48ea74af5607d88484c7a912bbef73d430049f8af4`,`d78f0b41a263af3df91fa4171cc2f60c40196aaf8f4bde5d1c8ff4474cfe753b`], + response: `[ + { + "inputs": {}, + "outputs": { + "1": { + "id": "819296691409584129", + "short_id": "745146x287x1", + "capacity": 300000000, + "transaction_id": "c3173549f502ede6440d5c48ea74af5607d88484c7a912bbef73d430049f8af4", + "transaction_vout": 1, + "closing_transaction_id": null, + "closing_reason": null, + "updated_at": "2022-08-25T18:44:07.000Z", + "created": "2022-07-16T00:11:33.000Z", + "status": 1, + "node_left": { + "alias": "River Financial 1", + "public_key": "03037dc08e9ac63b82581f79b662a4d0ceca8a8ca162b1af3551595b8f2d97b70a", + "base_fee_mtokens": 0, + "cltv_delta": 0, + "fee_rate": 500, + "is_disabled": 0, + "max_htlc_mtokens": 297000000000, + "min_htlc_mtokens": 1000, + "updated_at": "2022-08-23T17:53:43.000Z" + }, + "node_right": { + "alias": "0204a91bb5802ad0a799", + "public_key": "0204a91bb5802ad0a799acfd86ef566da03d80cc9e13acb01e680634bf64188a0d", + "base_fee_mtokens": 0, + "cltv_delta": 0, + "fee_rate": 152, + "is_disabled": 0, + "max_htlc_mtokens": 297000000000, + "min_htlc_mtokens": 1000, + "updated_at": "2022-08-25T18:44:07.000Z" + } + } + } + }, + { + "inputs": {}, + "outputs": { + "1": { + "id": "814662250034036737", + "short_id": "740931x2355x1", + "capacity": 300000000, + "transaction_id": "d78f0b41a263af3df91fa4171cc2f60c40196aaf8f4bde5d1c8ff4474cfe753b", + "transaction_vout": 1, + "closing_transaction_id": null, + "closing_reason": null, + "updated_at": "2022-08-28T18:54:40.000Z", + "created": "2022-06-15T16:18:33.000Z", + "status": 1, + "node_left": { + "alias": "bfx-lnd0", + "public_key": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "base_fee_mtokens": 1000, + "cltv_delta": 0, + "fee_rate": 1, + "is_disabled": 0, + "max_htlc_mtokens": 297000000000, + "min_htlc_mtokens": 1000, + "updated_at": "2022-08-25T18:44:03.000Z" + }, + "node_right": { + "alias": "River Financial 1", + "public_key": "03037dc08e9ac63b82581f79b662a4d0ceca8a8ca162b1af3551595b8f2d97b70a", + "base_fee_mtokens": 0, + "cltv_delta": 0, + "fee_rate": 750, + "is_disabled": 0, + "max_htlc_mtokens": 297000000000, + "min_htlc_mtokens": 1000, + "updated_at": "2022-08-28T18:54:40.000Z" + } + } + } + } +]` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`6b711b07b019d73ad432f401c01ac6ea253fbe2778388e5a686b5777678556c7`], + response: `[ + { + "inputs": {}, + "outputs": { + "0": { + "id": "2478509215728271360", + "short_id": "2254191x4x0", + "capacity": 16777215, + "transaction_id": "6b711b07b019d73ad432f401c01ac6ea253fbe2778388e5a686b5777678556c7", + "transaction_vout": 0, + "closing_transaction_id": null, + "closing_reason": null, + "updated_at": "2022-08-31T08:30:42.000Z", + "created": "2022-06-05T16:26:31.000Z", + "status": 1, + "node_left": { + "alias": "scarce-city-testnet", + "public_key": "0304fa1da67d441b382e3b2142a1980840276d89b6477812da8d26487b5ffa938c", + "base_fee_mtokens": 1000, + "cltv_delta": 0, + "fee_rate": 1, + "is_disabled": 0, + "max_htlc_mtokens": 16777215000, + "min_htlc_mtokens": 1000, + "updated_at": "2022-08-31T08:30:42.000Z" + }, + "node_right": { + "alias": "STRANGEIRON", + "public_key": "0225ff2ae6a3d9722b625072503c2f64f6eddb78d739379d2ee55a16b3b0ed0a17", + "base_fee_mtokens": 0, + "cltv_delta": 0, + "fee_rate": 10, + "is_disabled": 0, + "max_htlc_mtokens": 16609443000, + "min_htlc_mtokens": 1, + "updated_at": "2022-08-27T20:22:06.000Z" + } + } + } + } +]` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`cbb18e4b23c2a27736fa5be559fee7efcc855f2dfb1f16b125f686c307513ef3`], + response: `[ + { + "inputs": {}, + "outputs": { + "0": { + "id": "58998694435160064", + "short_id": "53659x5x0", + "capacity": 16777215, + "transaction_id": "cbb18e4b23c2a27736fa5be559fee7efcc855f2dfb1f16b125f686c307513ef3", + "transaction_vout": 0, + "closing_transaction_id": null, + "closing_reason": null, + "updated_at": "2022-09-04T10:15:51.000Z", + "created": "2021-09-02T07:08:40.000Z", + "status": 1, + "node_left": { + "alias": "STRANGESUCKER-v0.12.0-11-gea4143", + "public_key": "03b9e6c1dec203f47efc95d003314d22cbb12a1324de4b091fe7d68f321a56322f", + "base_fee_mtokens": 0, + "cltv_delta": 0, + "fee_rate": 0, + "is_disabled": 0, + "max_htlc_mtokens": 16609443000, + "min_htlc_mtokens": 1, + "updated_at": "2022-09-04T10:15:51.000Z" + }, + "node_right": { + "alias": "guggero", + "public_key": "02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120", + "base_fee_mtokens": 1000, + "cltv_delta": 0, + "fee_rate": 1, + "is_disabled": 0, + "max_htlc_mtokens": 16777215000, + "min_htlc_mtokens": 1000, + "updated_at": "2022-09-01T22:57:40.000Z" + } + } + } + } +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-channels-from-pubkey", + title: "GET Channels from Node Pubkey", + description: { + default: "

Returns a list of a node's channels given its :pubKey. Ten channels are returned at a time. Use :index for paging. :channelStatus can be open, active, or closed.

" + }, + urlString: "/v1/lightning/channels?public_key=:pubKey&status=:channelStatus", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/channels?public_key=%{1}&status=%{2}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2`,`open`], + response: `[ + { + "status": 1, + "closing_reason": null, + "capacity": 59200000, + "short_id": "751361x1324x1", + "id": "826130156244172801", + "fee_rate": 1, + "node": { + "alias": "ipayblue.com", + "public_key": "032fe854a231aeb2357523ee6ca263ae04ce53eee8a13767ecbb911b69fefd8ace", + "channels": 65, + "capacity": "856675361" + } + }, + { + "status": 1, + "closing_reason": null, + "capacity": 51000000, + "short_id": "750792x1586x1", + "id": "825504534145138689", + "fee_rate": 1, + "node": { + "alias": "Escher", + "public_key": "02b515c74f334dee09821bee299fcbd9668182730c5719b25a8f262b28893198b0", + "channels": 50, + "capacity": "2202925844" + } + }, + ... +]` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`0200202c1f23899d03bf3f37c87d348e6847bbd91e407df91a713c7dcf3442738b`, `open`], + response: `[ + { + "status": 1, + "closing_reason": null, + "closing_date": null, + "capacity": 8000000, + "short_id": "2223130x18x0", + "id": "2444357285058838528", + "fee_rate": 10, + "node": { + "alias": "Gilgamesh Lightning Testnet", + "public_key": "034997a34858a25dc453a722efc1545d8c7749cbd4587a8d2ef149d257babd8357", + "channels": 121, + "capacity": "512199932" + } + }, + { + "status": 0, + "closing_reason": null, + "closing_date": null, + "capacity": 1000000, + "short_id": "2223130x19x0", + "id": "2444357285058904064", + "fee_rate": 0, + "node": { + "alias": "routing.testnet.lnmarkets.com", + "public_key": "03bae2db4b57738c1ec1ffa1c5e5a4423968cc592b3b39cddf7d495e72919d6431", + "channels": 22, + "capacity": "246940161" + } + }, + ... +]` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120`, `open`], + response: `[ + { + "status": 1, + "closing_reason": null, + "closing_date": null, + "capacity": 16777215, + "short_id": "53659x5x0", + "id": "58998694435160064", + "fee_rate": 1, + "node": { + "alias": "STRANGESUCKER-v0.12.0-11-gea4143", + "public_key": "03b9e6c1dec203f47efc95d003314d22cbb12a1324de4b091fe7d68f321a56322f", + "channels": 4, + "capacity": "55778192" + } + }, + { + "status": 1, + "closing_reason": null, + "closing_date": null, + "capacity": 15000000, + "short_id": "17498x2x1", + "id": "19239254462955521", + "fee_rate": 1, + "node": { + "alias": "03870a4c4c54a9b2e705", + "public_key": "03870a4c4c54a9b2e705023d706843ffbac5b0e95e2b80d4b02dc7a9efb5380322", + "channels": 2, + "capacity": "30000000" + } + }, + ... +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-channel-geodata", + title: "GET Channel Geodata", + description: { + default: "

Returns a list of channels with corresponding node geodata.

" + }, + urlString: "/v1/lightning/channels-geo", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/channels-geo`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + [ + "03120ac28af913889cbc3cb86d7aff12bc0abe939f1fa9fb1980bdff8483197092", + "LIGHTNING2", + -77.2278, + 38.9567, + "03baa70886d9200af0ffbd3f9e18d96008331c858456b16e3a9b41e735c6208fef", + "LIGHTNING", + -77.2278, + 38.9567 + ], + [ + "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "bfx-lnd0", + 8.5671, + 47.3682, + "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "OpenNode.com", + -83.0061, + 39.9625 + ], + ... +]` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + [ + "038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9", + "aranguren.org", + 144.9669, + -37.8159, + "028c3640c57ffe47eb41db8225968833c5032f297aeba98672d6f7037090d59e3f", + "lndus0.next.zaphq.io", + -79.9746, + 32.8608 + ], + [ + "02be8f360e57600486b93dd33ea0872a4e14a259924ba4084f27d693a77d151158", + "lndus1.dev.zaphq.io", + -79.9746, + 32.8608, + "0273ec4a4c80e767aca1477592649ad6e709ad31e7435668043a9dceccb3020f35", + "lndwr1.dev.zaphq.io", + -79.9746, + 32.8608 + ], + ... +]` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + [ + "02dadf6c28f3284d591cd2a4189d1530c1ff82c07059ebea150a33ab76e7364b4a", + "珠美ノード⚡@wakiyamap", + 139.6895, + 35.6897, + "0271cf3881e6eadad960f47125434342e57e65b98a78afa99f9b4191c02dd7ab3b", + "eclair@wakiyamap", + 135.4911, + 34.7135 + ], + [ + "03b9e6c1dec203f47efc95d003314d22cbb12a1324de4b091fe7d68f321a56322f", + "STRANGESUCKER-v0.12.0-11-gea4143", + 19.1477, + 48.7386, + "02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120", + "guggero", + 9.491, + 51.2993 + ], + ... +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "lightning", + httpRequestMethod: "GET", + fragment: "get-channel-geodata-node", + title: "GET Channel Geodata for Node", + description: { + default: "

Returns a list of channels with corresponding geodata for a node with the given :pubKey.

" + }, + urlString: "/v1/lightning/channels-geo/:pubKey", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/lightning/channels-geo/%{1}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac`], + response: `[ + [ + "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "Bitrefill", + -77.4903, + 39.0469, + "024a2e265cd66066b78a788ae615acdc84b5b0dec9efac36d7ac87513015eaf6ed", + "Bitrefill", + -6.2591, + 53.3379 + ], + [ + "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "Bitrefill", + -77.4903, + 39.0469, + "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", + "Bitrefill Routing", + -6.2591, + 53.3379 + ], + ... +]` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`0273ec4a4c80e767aca1477592649ad6e709ad31e7435668043a9dceccb3020f35`], + response: `[ + [ + "039b1717db1193eb332d3c0bfdcce90a6aab60efa478b60963d3b406a8fc45134a", + "testnet.demo.btcpayserver.org", + -79.3503, + 43.7806, + "0273ec4a4c80e767aca1477592649ad6e709ad31e7435668043a9dceccb3020f35", + "lndwr1.dev.zaphq.io", + -79.9746, + 32.8608 + ], + [ + "0273ec4a4c80e767aca1477592649ad6e709ad31e7435668043a9dceccb3020f35", + "lndwr1.dev.zaphq.io", + -79.9746, + 32.8608, + "02c6fbedc6ca81d4db5883f1d01481c8187d5f85075729a658288a6d507f770ada", + "HAPPYTOLL", + -97.822, + 37.751 + ], + ... +]` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120`], + response: `[ + [ + "02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120", + "guggero", + 9.491, + 51.2993, + "0271cf3881e6eadad960f47125434342e57e65b98a78afa99f9b4191c02dd7ab3b", + "eclair@wakiyamap", + 135.4911, + 34.7135 + ], + [ + "02dadf6c28f3284d591cd2a4189d1530c1ff82c07059ebea150a33ab76e7364b4a", + "珠美ノード⚡@wakiyamap", + 139.6895, + 35.6897, + "02ad48db0d1a7f7c3d186ddc57f8e62c49a1234fb829af9ccd3be1a4596bc39120", + "guggero", + 9.491, + 51.2993 + ], + ... +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + } ]; export const faqData = [ diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 5c071a357..d9670936d 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -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 { diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html index 30d1a3585..8b486eff5 100644 --- a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html @@ -19,31 +19,31 @@ Fee rate - {{ channel.fee_rate }} ppm ({{ channel.fee_rate / 10000 | number }}%) + {{ channel.fee_rate ?? '-' }} ppm ({{ channel.fee_rate / 10000 | number }}%) Base fee - + Min HTLC - + Max HTLC - + Timelock delta - + diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index 2824b8dba..3942cda6e 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -16,7 +16,8 @@
- +
@@ -25,13 +26,17 @@ - + - - + + + + + +
CreatedCreated
Last update
Last update
Closing date
@@ -47,38 +52,57 @@
- -
- -
- -
-
- +
+
+ + + + + + + +
Capacity + + +
-
- -
-
- -
- - -
-

Opening transaction

- -
- -
- -
-

Closing transaction

   - -
- -
-
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+ + + +
+

Opening transaction

+ +
+ + +
+ +
+

Closing transaction

   + + +
+ + +
+
@@ -104,7 +128,7 @@
- +
@@ -148,4 +172,4 @@
- \ No newline at end of file + diff --git a/frontend/src/app/lightning/channel/channel.component.ts b/frontend/src/app/lightning/channel/channel.component.ts index b1e39dda7..c6d06683a 100644 --- a/frontend/src/app/lightning/channel/channel.component.ts +++ b/frontend/src/app/lightning/channel/channel.component.ts @@ -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); diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index af87cefa4..a51e03ef8 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -35,7 +35,8 @@ Node Alias   Status - Fee Rate + Fee Rate + Closing date Capacity Channel ID @@ -71,9 +72,12 @@ - + {{ channel.fee_rate }} ppm ({{ channel.fee_rate / 10000 | number }}%) + + + diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts index d601606fd..063e2c6a5 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts @@ -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()); diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index f0be2fe89..ded6b5821 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -120,7 +120,7 @@
-
+
@@ -128,7 +128,7 @@
-
+
diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.html b/frontend/src/app/lightning/nodes-map/nodes-map.component.html index b762b2d24..d739dd2c9 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.html +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.html @@ -1,16 +1,13 @@ -
+
-
+
- Lightning nodes world heat map - + Lightning nodes world map
(Tor nodes excluded)
-
diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.scss b/frontend/src/app/lightning/nodes-map/nodes-map.component.scss index 4e363a534..d7ad42b46 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.scss +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.scss @@ -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; +} diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts index d2fcb31e0..b783e225a 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts @@ -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; 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 `${country.name}
0 nodes

`; - } else { - return `${country.data.name}
${country.data.value} nodes

`; - } + 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 ` + ${alias}
+ ${liquidity}
+ ${data[5]} channels
+ ${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(); + }); } } diff --git a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.html b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.html index d3e8686b0..a8cfdcfb4 100644 --- a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.html +++ b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.html @@ -46,7 +46,7 @@ - {{ country.capacity | amountShortener: 1 }} + {{ country.capacity ?? 0 | amountShortener: 1 }} sats diff --git a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts index 09b00e032..9f1b3fe88 100644 --- a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts +++ b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts @@ -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) { diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html index 16f4265a2..543cf951c 100644 --- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html +++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html @@ -1,21 +1,71 @@
-

+

Lightning nodes in {{ country?.name }} {{ country?.flag }}

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Nodes{{ countryNodes.nodes.length }}
Liquidity + + + {{ countryNodes.sumLiquidity | amountShortener: 1 }} + sats + +   + + +
Channels{{ countryNodes.sumChannels }}
ISP Count{{ countryNodes.ispCount }}
Top ISP + + {{ countryNodes.topIsp.name }} [ASN {{ countryNodes.topIsp.id }}] + +
+
+
+
+ +
+
+
+
+
+ - + - - + + @@ -39,6 +89,32 @@ + + + + + + + + + + + + + +
Alias First seen Last update Capacity ChannelsCityLocation
{{ node.alias }}
+ + + + + + + + + + + +
diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts index 644e6741a..19394a828 100644 --- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts +++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts @@ -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; 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; } } diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html index ef1c71efd..093a8ad1a 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html @@ -10,14 +10,14 @@

-
Unknown capacity
+
Unknown capacity

-
Tor capacity
+
Tor capacity

@@ -80,19 +80,19 @@

-
Tagged ISPs
+
Clearnet capacity

-
Tagged capacity
+
Unknown capacity

-
Tagged nodes
+
Tor capacity

diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts index c60620f61..635e5bc74 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts @@ -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: { diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html index a8931d843..441dc429e 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html @@ -1,18 +1,68 @@
-

Lightning nodes on ISP: {{ isp?.name }} [AS {{isp?.id}}]

+

Lightning nodes on ISP: {{ isp?.name }}

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
ASN{{ isp?.id }}
Nodes{{ ispNodes.nodes.length }}
Liquidity + + + {{ ispNodes.sumLiquidity | amountShortener: 1 }} + sats + +   + + +
Channels{{ ispNodes.sumChannels }}
Top country + + {{ ispNodes.topCountry.country }} {{ ispNodes.topCountry.flag }} + +
+
+
+
+ +
+
+
+
+ - + - - + + @@ -36,6 +86,32 @@ + + + + + + + + + + + + + +
Alias First seen Last update Capacity ChannelsCityLocation
{{ node.alias }}
+ + + + + + + + + + + +
diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss index 02b47e8be..b829c5b59 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss @@ -59,4 +59,4 @@ @media (max-width: 576px) { display: none } -} \ No newline at end of file +} diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts index cc57056fc..24664aab0 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts @@ -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; 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; } } diff --git a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html index 5b96400c2..e82ff0ac8 100644 --- a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html +++ b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html @@ -9,7 +9,7 @@ Alias First seen - Capacity + Liquidity Channels Last update Location @@ -35,7 +35,7 @@ - {{ node?.city?.en ?? '-' }} + diff --git a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts index 23f248b0e..d1cec0780 100644 --- a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts @@ -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; 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 = { + 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); }) ); } diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html index 107609251..80e93f7ac 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html @@ -8,7 +8,7 @@ Alias - Capacity + Liquidity Channels First seen Last update @@ -35,7 +35,7 @@ - {{ node?.city?.en ?? '-' }} + diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts index c79c396ee..ad396ee31 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts @@ -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; 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 = { + 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) => { diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html index dd05e7a6a..ef2a05659 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html @@ -9,7 +9,7 @@ Alias Channels - Capacity + Liquidity First seen Last update Location @@ -35,9 +35,9 @@ - {{ node?.city?.en ?? '-' }} + - + diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts index 44e7aa518..f8a2ae52b 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts @@ -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; 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 = { + 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) => { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 5f036c575..8c0f5ecd0 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -267,10 +267,14 @@ export class ApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp/' + isp); } - getNodesPerCountry(): Observable { + getNodesPerCountry$(): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/countries'); } + getWorldNodes$(): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/world'); + } + getChannelsGeo$(publicKey?: string, style?: 'graph' | 'nodepage' | 'widget' | 'channelpage'): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels-geo' + diff --git a/frontend/src/app/shared/components/sats/sats.component.html b/frontend/src/app/shared/components/sats/sats.component.html index a648cdfcb..1b4ab9143 100644 --- a/frontend/src/app/shared/components/sats/sats.component.html +++ b/frontend/src/app/shared/components/sats/sats.component.html @@ -1,5 +1,8 @@ -‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number : digitsInfo }} -L- -tL- -t- -s-sats +{{ valueOverride }} +‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number : digitsInfo }} + + L- + tL- + t- + s-sats + \ No newline at end of file diff --git a/frontend/src/app/shared/components/sats/sats.component.ts b/frontend/src/app/shared/components/sats/sats.component.ts index d9801d249..39be66ecd 100644 --- a/frontend/src/app/shared/components/sats/sats.component.ts +++ b/frontend/src/app/shared/components/sats/sats.component.ts @@ -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; diff --git a/frontend/src/app/shared/components/timestamp/timestamp.component.html b/frontend/src/app/shared/components/timestamp/timestamp.component.html index 769b292d4..69abce53f 100644 --- a/frontend/src/app/shared/components/timestamp/timestamp.component.html +++ b/frontend/src/app/shared/components/timestamp/timestamp.component.html @@ -1,4 +1,7 @@ -‎{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }} -
- () -
+- + + ‎{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }} +
+ () +
+
diff --git a/frontend/src/app/shared/components/timestamp/timestamp.component.ts b/frontend/src/app/shared/components/timestamp/timestamp.component.ts index dc577a185..120a5dfe4 100644 --- a/frontend/src/app/shared/components/timestamp/timestamp.component.ts +++ b/frontend/src/app/shared/components/timestamp/timestamp.component.ts @@ -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; } } diff --git a/frontend/src/resources/profile/btcpayserver.svg b/frontend/src/resources/profile/btcpayserver.svg new file mode 100644 index 000000000..1c18a4cfe --- /dev/null +++ b/frontend/src/resources/profile/btcpayserver.svg @@ -0,0 +1 @@ +BTCPayServer diff --git a/frontend/src/resources/profile/electrum.jpg b/frontend/src/resources/profile/electrum.jpg deleted file mode 100644 index c1c063d75..000000000 Binary files a/frontend/src/resources/profile/electrum.jpg and /dev/null differ diff --git a/frontend/src/resources/profile/electrum.png b/frontend/src/resources/profile/electrum.png new file mode 100644 index 000000000..ca24d5f45 Binary files /dev/null and b/frontend/src/resources/profile/electrum.png differ diff --git a/frontend/src/resources/profile/lnbits.svg b/frontend/src/resources/profile/lnbits.svg new file mode 100644 index 000000000..0969df927 --- /dev/null +++ b/frontend/src/resources/profile/lnbits.svg @@ -0,0 +1 @@ + diff --git a/frontend/sync-assets.js b/frontend/sync-assets.js index a6b59bdb1..8937e2abb 100644 --- a/frontend/sync-assets.js +++ b/frontend/sync-assets.js @@ -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/'; } diff --git a/nginx-mempool.conf b/nginx-mempool.conf index 58d45f3cc..a6f701478 100644 --- a/nginx-mempool.conf +++ b/nginx-mempool.conf @@ -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 // 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; diff --git a/production/install b/production/install index 4eac1817b..40f89389f 100755 --- a/production/install +++ b/production/install @@ -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 diff --git a/production/mempool-restart-all b/production/mempool-restart-all deleted file mode 100755 index 13e551066..000000000 --- a/production/mempool-restart-all +++ /dev/null @@ -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 diff --git a/production/mempool.crontab b/production/mempool.crontab index cc1bcd878..0e7b6af6b 100644 --- a/production/mempool.crontab +++ b/production/mempool.crontab @@ -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 diff --git a/production/nginx/server-common.conf b/production/nginx/server-common.conf index 26e81f7fa..dedd36411 100644 --- a/production/nginx/server-common.conf +++ b/production/nginx/server-common.conf @@ -58,12 +58,6 @@ location = / { expires 5m; } -# used to rewrite resources from // 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 //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