Merge pull request #2670 from mononaut/expose-node-tlv-data
Show node tlv data & liquidity ads
This commit is contained in:
commit
a0b6719105
@ -4,7 +4,7 @@ import logger from '../logger';
|
|||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 42;
|
private static currentVersion = 43;
|
||||||
private queryTimeout = 120000;
|
private queryTimeout = 120000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -356,6 +356,10 @@ class DatabaseMigration {
|
|||||||
if (databaseSchemaVersion < 42 && isBitcoin === true) {
|
if (databaseSchemaVersion < 42 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
|
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 43 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -791,6 +795,19 @@ class DatabaseMigration {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCreateLNNodeRecordsTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS nodes_records (
|
||||||
|
public_key varchar(66) NOT NULL,
|
||||||
|
type int(10) unsigned NOT NULL,
|
||||||
|
payload blob NOT NULL,
|
||||||
|
UNIQUE KEY public_key_type (public_key, type),
|
||||||
|
INDEX (public_key),
|
||||||
|
FOREIGN KEY (public_key)
|
||||||
|
REFERENCES nodes (public_key)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
public async $truncateIndexedData(tables: string[]) {
|
public async $truncateIndexedData(tables: string[]) {
|
||||||
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
||||||
|
|
||||||
|
@ -105,6 +105,18 @@ class NodesApi {
|
|||||||
node.closed_channel_count = rows[0].closed_channel_count;
|
node.closed_channel_count = rows[0].closed_channel_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom records
|
||||||
|
query = `
|
||||||
|
SELECT type, payload
|
||||||
|
FROM nodes_records
|
||||||
|
WHERE public_key = ?
|
||||||
|
`;
|
||||||
|
[rows] = await DB.query(query, [public_key]);
|
||||||
|
node.custom_records = {};
|
||||||
|
for (const record of rows) {
|
||||||
|
node.custom_records[record.type] = Buffer.from(record.payload, 'binary').toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot get node information for ${public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
|
logger.err(`Cannot get node information for ${public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
@ -7,6 +7,15 @@ import { Common } from '../../common';
|
|||||||
* Convert a clightning "listnode" entry to a lnd node entry
|
* Convert a clightning "listnode" entry to a lnd node entry
|
||||||
*/
|
*/
|
||||||
export function convertNode(clNode: any): ILightningApi.Node {
|
export function convertNode(clNode: any): ILightningApi.Node {
|
||||||
|
let custom_records: { [type: number]: string } | undefined = undefined;
|
||||||
|
if (clNode.option_will_fund) {
|
||||||
|
try {
|
||||||
|
custom_records = { '1': Buffer.from(clNode.option_will_fund.compact_lease || '', 'hex').toString('base64') };
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot decode option_will_fund compact_lease for ${clNode.nodeid}). Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
custom_records = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
alias: clNode.alias ?? '',
|
alias: clNode.alias ?? '',
|
||||||
color: `#${clNode.color ?? ''}`,
|
color: `#${clNode.color ?? ''}`,
|
||||||
@ -23,6 +32,7 @@ export function convertNode(clNode: any): ILightningApi.Node {
|
|||||||
};
|
};
|
||||||
}) ?? [],
|
}) ?? [],
|
||||||
last_update: clNode?.last_timestamp ?? 0,
|
last_update: clNode?.last_timestamp ?? 0,
|
||||||
|
custom_records
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ export namespace ILightningApi {
|
|||||||
}[];
|
}[];
|
||||||
color: string;
|
color: string;
|
||||||
features: { [key: number]: Feature };
|
features: { [key: number]: Feature };
|
||||||
|
custom_records?: { [type: number]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Info {
|
export interface Info {
|
||||||
|
67
backend/src/repositories/NodeRecordsRepository.ts
Normal file
67
backend/src/repositories/NodeRecordsRepository.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { ResultSetHeader, RowDataPacket } from 'mysql2';
|
||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
|
export interface NodeRecord {
|
||||||
|
publicKey: string; // node public key
|
||||||
|
type: number; // TLV extension record type
|
||||||
|
payload: string; // base64 record payload
|
||||||
|
}
|
||||||
|
|
||||||
|
class NodesRecordsRepository {
|
||||||
|
public async $saveRecord(record: NodeRecord): Promise<void> {
|
||||||
|
try {
|
||||||
|
const payloadBytes = Buffer.from(record.payload, 'base64');
|
||||||
|
await DB.query(`
|
||||||
|
INSERT INTO nodes_records(public_key, type, payload)
|
||||||
|
VALUE (?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
payload = ?
|
||||||
|
`, [record.publicKey, record.type, payloadBytes, payloadBytes]);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this
|
||||||
|
logger.err(`Cannot save node record (${[record.publicKey, record.type, record.payload]}) into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
// We don't throw, not a critical issue if we miss some nodes records
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getRecordTypes(publicKey: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT type FROM nodes_records
|
||||||
|
WHERE public_key = ?
|
||||||
|
`;
|
||||||
|
const [rows] = await DB.query<RowDataPacket[][]>(query, [publicKey]);
|
||||||
|
return rows.map(row => row['type']);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot retrieve custom records for ${publicKey} from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $deleteUnusedRecords(publicKey: string, recordTypes: number[]): Promise<number> {
|
||||||
|
try {
|
||||||
|
let query;
|
||||||
|
if (recordTypes.length) {
|
||||||
|
query = `
|
||||||
|
DELETE FROM nodes_records
|
||||||
|
WHERE public_key = ?
|
||||||
|
AND type NOT IN (${recordTypes.map(type => `${type}`).join(',')})
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
query = `
|
||||||
|
DELETE FROM nodes_records
|
||||||
|
WHERE public_key = ?
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const [result] = await DB.query<ResultSetHeader>(query, [publicKey]);
|
||||||
|
return result.affectedRows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot delete unused custom records for ${publicKey} from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new NodesRecordsRepository();
|
@ -13,6 +13,7 @@ import fundingTxFetcher from './sync-tasks/funding-tx-fetcher';
|
|||||||
import NodesSocketsRepository from '../../repositories/NodesSocketsRepository';
|
import NodesSocketsRepository from '../../repositories/NodesSocketsRepository';
|
||||||
import { Common } from '../../api/common';
|
import { Common } from '../../api/common';
|
||||||
import blocks from '../../api/blocks';
|
import blocks from '../../api/blocks';
|
||||||
|
import NodeRecordsRepository from '../../repositories/NodeRecordsRepository';
|
||||||
|
|
||||||
class NetworkSyncService {
|
class NetworkSyncService {
|
||||||
loggerTimer = 0;
|
loggerTimer = 0;
|
||||||
@ -63,6 +64,7 @@ class NetworkSyncService {
|
|||||||
let progress = 0;
|
let progress = 0;
|
||||||
|
|
||||||
let deletedSockets = 0;
|
let deletedSockets = 0;
|
||||||
|
let deletedRecords = 0;
|
||||||
const graphNodesPubkeys: string[] = [];
|
const graphNodesPubkeys: string[] = [];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
|
const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
|
||||||
@ -84,8 +86,23 @@ class NetworkSyncService {
|
|||||||
addresses.push(socket.addr);
|
addresses.push(socket.addr);
|
||||||
}
|
}
|
||||||
deletedSockets += await NodesSocketsRepository.$deleteUnusedSockets(node.pub_key, addresses);
|
deletedSockets += await NodesSocketsRepository.$deleteUnusedSockets(node.pub_key, addresses);
|
||||||
|
|
||||||
|
const oldRecordTypes = await NodeRecordsRepository.$getRecordTypes(node.pub_key);
|
||||||
|
const customRecordTypes: number[] = [];
|
||||||
|
for (const [type, payload] of Object.entries(node.custom_records || {})) {
|
||||||
|
const numericalType = parseInt(type);
|
||||||
|
await NodeRecordsRepository.$saveRecord({
|
||||||
|
publicKey: node.pub_key,
|
||||||
|
type: numericalType,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
customRecordTypes.push(numericalType);
|
||||||
|
}
|
||||||
|
if (oldRecordTypes.reduce((changed, type) => changed || customRecordTypes.indexOf(type) === -1, false)) {
|
||||||
|
deletedRecords += await NodeRecordsRepository.$deleteUnusedRecords(node.pub_key, customRecordTypes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted`);
|
logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted. ${deletedRecords} custom records deleted.`);
|
||||||
|
|
||||||
// If a channel if not present in the graph, mark it as inactive
|
// If a channel if not present in the graph, mark it as inactive
|
||||||
await nodesApi.$setNodesInactive(graphNodesPubkeys);
|
await nodesApi.$setNodesInactive(graphNodesPubkeys);
|
||||||
|
31
frontend/src/app/lightning/node/liquidity-ad.ts
Normal file
31
frontend/src/app/lightning/node/liquidity-ad.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export interface ILiquidityAd {
|
||||||
|
funding_weight: number;
|
||||||
|
lease_fee_basis: number; // lease fee rate in parts-per-thousandth
|
||||||
|
lease_fee_base_sat: number; // fixed lease fee in sats
|
||||||
|
channel_fee_max_rate: number; // max routing fee rate in parts-per-thousandth
|
||||||
|
channel_fee_max_base: number; // max routing base fee in milli-sats
|
||||||
|
compact_lease?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseLiquidityAdHex(compact_lease: string): ILiquidityAd | false {
|
||||||
|
if (!compact_lease || compact_lease.length < 20 || compact_lease.length > 28) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const liquidityAd: ILiquidityAd = {
|
||||||
|
funding_weight: parseInt(compact_lease.slice(0, 4), 16),
|
||||||
|
lease_fee_basis: parseInt(compact_lease.slice(4, 8), 16),
|
||||||
|
channel_fee_max_rate: parseInt(compact_lease.slice(8, 12), 16),
|
||||||
|
lease_fee_base_sat: parseInt(compact_lease.slice(12, 20), 16),
|
||||||
|
channel_fee_max_base: compact_lease.length > 20 ? parseInt(compact_lease.slice(20), 16) : 0,
|
||||||
|
}
|
||||||
|
if (Object.values(liquidityAd).reduce((valid: boolean, value: number): boolean => (valid && !isNaN(value) && value >= 0), true)) {
|
||||||
|
liquidityAd.compact_lease = compact_lease;
|
||||||
|
return liquidityAd;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -125,6 +125,93 @@
|
|||||||
<app-clipboard [button]="true" [text]="node.socketsObject[selectedSocketIndex].socket" [leftPadding]="false"></app-clipboard>
|
<app-clipboard [button]="true" [text]="node.socketsObject[selectedSocketIndex].socket" [leftPadding]="false"></app-clipboard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="hasDetails" [hidden]="!showDetails" id="details" class="details mt-3">
|
||||||
|
<div class="box">
|
||||||
|
<ng-template [ngIf]="liquidityAd">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h5 class="mb-3" i18n="node.liquidity-ad">Liquidity ad</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="label" i18n="liquidity-ad.lease-fee-rate|Liquidity ad lease fee rate">Lease fee rate</td>
|
||||||
|
<td>
|
||||||
|
<span class="d-inline-block">
|
||||||
|
{{ liquidityAd.lease_fee_basis !== null ? ((liquidityAd.lease_fee_basis * 1000) | amountShortener : 2 : undefined : true) : '-' }} <span class="symbol">ppm {{ liquidityAd.lease_fee_basis !== null ? '(' + (liquidityAd.lease_fee_basis / 10 | amountShortener : 2 : undefined : true) + '%)' : '' }}</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label" i18n="liquidity-ad.lease-base-fee">Lease base fee</td>
|
||||||
|
<td>
|
||||||
|
<app-sats [valueOverride]="liquidityAd.lease_fee_base_sat === null ? '- ' : undefined" [satoshis]="liquidityAd.lease_fee_base_sat"></app-sats>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label" i18n="liquidity-ad.funding-weight">Funding weight</td>
|
||||||
|
<td [innerHTML]="'‎' + (liquidityAd.funding_weight | wuBytes: 2)"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="label" i18n="liquidity-ad.channel-fee-rate|Liquidity ad channel fee rate">Channel fee rate</td>
|
||||||
|
<td>
|
||||||
|
<span class="d-inline-block">
|
||||||
|
{{ liquidityAd.channel_fee_max_rate !== null ? ((liquidityAd.channel_fee_max_rate * 1000) | amountShortener : 2 : undefined : true) : '-' }} <span class="symbol">ppm {{ liquidityAd.channel_fee_max_rate !== null ? '(' + (liquidityAd.channel_fee_max_rate / 10 | amountShortener : 2 : undefined : true) + '%)' : '' }}</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label" i18n="liquidity-ad.channel-base-fee">Channel base fee</td>
|
||||||
|
<td>
|
||||||
|
<span *ngIf="liquidityAd.channel_fee_max_base !== null">
|
||||||
|
{{ liquidityAd.channel_fee_max_base | amountShortener : 0 }}
|
||||||
|
<span class="symbol" i18n="shared.m-sats">mSats</span>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="liquidityAd.channel_fee_max_base === null">
|
||||||
|
-
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label" i18n="liquidity-ad.compact-lease">Compact lease</td>
|
||||||
|
<td class="compact-lease">{{ liquidityAd.compact_lease }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template [ngIf]="tlvRecords?.length">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h5 class="mb-3" i18n="node.tlv.records">TLV extension records</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let recordItem of tlvRecords">
|
||||||
|
<td class="tlv-type">{{ recordItem.type }}</td>
|
||||||
|
<td class="tlv-payload">{{ recordItem.payload }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="hasDetails" class="text-right mt-3">
|
||||||
|
<button type="button" class="btn btn-outline-info btn-sm btn-details" (click)="toggleShowDetails()" i18n="node.details|Node Details">Details</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div *ngIf="!error">
|
<div *ngIf="!error">
|
||||||
<div class="row" *ngIf="node.as_number && node.active_channel_count">
|
<div class="row" *ngIf="node.as_number && node.active_channel_count">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
|
@ -72,3 +72,32 @@ app-fiat {
|
|||||||
height: 28px !important;
|
height: 28px !important;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlv-type {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlv-payload {
|
||||||
|
font-size: 12px;
|
||||||
|
width: 100%;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: normal;
|
||||||
|
font-family: "Courier New", Courier, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-lease {
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: normal;
|
||||||
|
font-family: "Courier New", Courier, monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
|
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
|
||||||
|
import { ILiquidityAd, parseLiquidityAdHex } from './liquidity-ad';
|
||||||
|
|
||||||
|
interface CustomRecord {
|
||||||
|
type: string;
|
||||||
|
payload: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-node',
|
selector: 'app-node',
|
||||||
@ -24,6 +30,10 @@ export class NodeComponent implements OnInit {
|
|||||||
channelListLoading = false;
|
channelListLoading = false;
|
||||||
clearnetSocketCount = 0;
|
clearnetSocketCount = 0;
|
||||||
torSocketCount = 0;
|
torSocketCount = 0;
|
||||||
|
hasDetails = false;
|
||||||
|
showDetails = false;
|
||||||
|
liquidityAd: ILiquidityAd;
|
||||||
|
tlvRecords: CustomRecord[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private lightningApiService: LightningApiService,
|
private lightningApiService: LightningApiService,
|
||||||
@ -36,6 +46,8 @@ export class NodeComponent implements OnInit {
|
|||||||
.pipe(
|
.pipe(
|
||||||
switchMap((params: ParamMap) => {
|
switchMap((params: ParamMap) => {
|
||||||
this.publicKey = params.get('public_key');
|
this.publicKey = params.get('public_key');
|
||||||
|
this.tlvRecords = [];
|
||||||
|
this.liquidityAd = null;
|
||||||
return this.lightningApiService.getNode$(params.get('public_key'));
|
return this.lightningApiService.getNode$(params.get('public_key'));
|
||||||
}),
|
}),
|
||||||
map((node) => {
|
map((node) => {
|
||||||
@ -79,6 +91,26 @@ export class NodeComponent implements OnInit {
|
|||||||
|
|
||||||
return node;
|
return node;
|
||||||
}),
|
}),
|
||||||
|
tap((node) => {
|
||||||
|
this.hasDetails = Object.keys(node.custom_records).length > 0;
|
||||||
|
for (const [type, payload] of Object.entries(node.custom_records)) {
|
||||||
|
if (typeof payload !== 'string') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed = false;
|
||||||
|
if (type === '1') {
|
||||||
|
const ad = parseLiquidityAdHex(payload);
|
||||||
|
if (ad) {
|
||||||
|
parsed = true;
|
||||||
|
this.liquidityAd = ad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!parsed) {
|
||||||
|
this.tlvRecords.push({ type, payload });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
catchError(err => {
|
catchError(err => {
|
||||||
this.error = err;
|
this.error = err;
|
||||||
return [{
|
return [{
|
||||||
@ -89,6 +121,10 @@ export class NodeComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleShowDetails(): void {
|
||||||
|
this.showDetails = !this.showDetails;
|
||||||
|
}
|
||||||
|
|
||||||
changeSocket(index: number) {
|
changeSocket(index: number) {
|
||||||
this.selectedSocketIndex = index;
|
this.selectedSocketIndex = index;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user