Merge pull request #3933 from mempool/nymkappa/feature-bits
Show raw and decoded lightning node features
This commit is contained in:
		
						commit
						965270dc7f
					
				@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
 | 
			
		||||
import { RowDataPacket } from 'mysql2';
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 63;
 | 
			
		||||
  private static currentVersion = 64;
 | 
			
		||||
  private queryTimeout = 3600_000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
  private uniqueLogs: string[] = [];
 | 
			
		||||
@ -543,6 +543,11 @@ class DatabaseMigration {
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
 | 
			
		||||
      await this.updateToSchemaVersion(63);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (databaseSchemaVersion < 64 && isBitcoin === true) {
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
 | 
			
		||||
      await this.updateToSchemaVersion(64);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import DB from '../../database';
 | 
			
		||||
import { ResultSetHeader } from 'mysql2';
 | 
			
		||||
import { ILightningApi } from '../lightning/lightning-api.interface';
 | 
			
		||||
import { ITopNodesPerCapacity, ITopNodesPerChannels } from '../../mempool.interfaces';
 | 
			
		||||
import { bin2hex } from '../../utils/format';
 | 
			
		||||
 | 
			
		||||
class NodesApi {
 | 
			
		||||
  public async $getWorldNodes(): Promise<any> {
 | 
			
		||||
@ -56,7 +57,8 @@ class NodesApi {
 | 
			
		||||
          UNIX_TIMESTAMP(updated_at) AS updated_at, color, sockets as sockets,
 | 
			
		||||
          as_number, city_id, country_id, subdivision_id, longitude, latitude,
 | 
			
		||||
          geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city,
 | 
			
		||||
          geo_names_country.names as country, geo_names_subdivision.names as subdivision
 | 
			
		||||
          geo_names_country.names as country, geo_names_subdivision.names as subdivision,
 | 
			
		||||
          features
 | 
			
		||||
        FROM nodes
 | 
			
		||||
        LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number
 | 
			
		||||
        LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id
 | 
			
		||||
@ -76,6 +78,23 @@ class NodesApi {
 | 
			
		||||
      node.city = JSON.parse(node.city);
 | 
			
		||||
      node.country = JSON.parse(node.country);
 | 
			
		||||
 | 
			
		||||
      // Features      
 | 
			
		||||
      node.features = JSON.parse(node.features);
 | 
			
		||||
      node.featuresBits = null;
 | 
			
		||||
      if (node.features) {
 | 
			
		||||
        let maxBit = 0;
 | 
			
		||||
        for (const feature of node.features) {
 | 
			
		||||
          maxBit = Math.max(maxBit, feature.bit);
 | 
			
		||||
        }
 | 
			
		||||
        maxBit = Math.ceil(maxBit / 4) * 4 - 1;
 | 
			
		||||
        
 | 
			
		||||
        node.featuresBits = new Array(maxBit + 1).fill(0);
 | 
			
		||||
        for (const feature of node.features) {
 | 
			
		||||
          node.featuresBits[feature.bit] = 1;
 | 
			
		||||
        }
 | 
			
		||||
        node.featuresBits = bin2hex(node.featuresBits.reverse().join(''));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Active channels and capacity
 | 
			
		||||
      const activeChannelsStats: any = await this.$getActiveChannelsStats(public_key);
 | 
			
		||||
      node.active_channel_count = activeChannelsStats.active_channel_count ?? 0;
 | 
			
		||||
@ -656,10 +675,19 @@ class NodesApi {
 | 
			
		||||
          alias_search,
 | 
			
		||||
          color,
 | 
			
		||||
          sockets,
 | 
			
		||||
          status
 | 
			
		||||
          status,
 | 
			
		||||
          features
 | 
			
		||||
        )
 | 
			
		||||
        VALUES (?, NOW(), FROM_UNIXTIME(?), ?, ?, ?, ?, 1)
 | 
			
		||||
        ON DUPLICATE KEY UPDATE updated_at = FROM_UNIXTIME(?), alias = ?, alias_search = ?, color = ?, sockets = ?, status = 1`;
 | 
			
		||||
        VALUES (?, NOW(), FROM_UNIXTIME(?), ?, ?, ?, ?, 1, ?)
 | 
			
		||||
        ON DUPLICATE KEY UPDATE
 | 
			
		||||
          updated_at = FROM_UNIXTIME(?),
 | 
			
		||||
          alias = ?,
 | 
			
		||||
          alias_search = ?,
 | 
			
		||||
          color = ?,
 | 
			
		||||
          sockets = ?,
 | 
			
		||||
          status = 1,
 | 
			
		||||
          features = ?
 | 
			
		||||
      `;
 | 
			
		||||
 | 
			
		||||
      await DB.query(query, [
 | 
			
		||||
        node.pub_key,
 | 
			
		||||
@ -668,11 +696,13 @@ class NodesApi {
 | 
			
		||||
        this.aliasToSearchText(node.alias),
 | 
			
		||||
        node.color,
 | 
			
		||||
        sockets,
 | 
			
		||||
        JSON.stringify(node.features),
 | 
			
		||||
        node.last_update,
 | 
			
		||||
        node.alias,
 | 
			
		||||
        this.aliasToSearchText(node.alias),
 | 
			
		||||
        node.color,
 | 
			
		||||
        sockets,
 | 
			
		||||
        JSON.stringify(node.features),
 | 
			
		||||
      ]);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$saveNode() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,91 @@ import { ILightningApi } from '../lightning-api.interface';
 | 
			
		||||
import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher';
 | 
			
		||||
import logger from '../../../logger';
 | 
			
		||||
import { Common } from '../../common';
 | 
			
		||||
import { hex2bin } from '../../../utils/format';
 | 
			
		||||
import config from '../../../config';
 | 
			
		||||
 | 
			
		||||
// https://github.com/lightningnetwork/lnd/blob/master/lnwire/features.go
 | 
			
		||||
export enum FeatureBits {
 | 
			
		||||
	DataLossProtectRequired = 0,
 | 
			
		||||
	DataLossProtectOptional = 1,
 | 
			
		||||
	InitialRoutingSync = 3,
 | 
			
		||||
	UpfrontShutdownScriptRequired = 4,
 | 
			
		||||
	UpfrontShutdownScriptOptional = 5,
 | 
			
		||||
	GossipQueriesRequired = 6,
 | 
			
		||||
	GossipQueriesOptional = 7,
 | 
			
		||||
	TLVOnionPayloadRequired = 8,
 | 
			
		||||
	TLVOnionPayloadOptional = 9,
 | 
			
		||||
	StaticRemoteKeyRequired = 12,
 | 
			
		||||
	StaticRemoteKeyOptional = 13,
 | 
			
		||||
	PaymentAddrRequired = 14,
 | 
			
		||||
	PaymentAddrOptional = 15,
 | 
			
		||||
	MPPRequired = 16,
 | 
			
		||||
	MPPOptional = 17,
 | 
			
		||||
	WumboChannelsRequired = 18,
 | 
			
		||||
	WumboChannelsOptional = 19,
 | 
			
		||||
	AnchorsRequired = 20,
 | 
			
		||||
	AnchorsOptional = 21,
 | 
			
		||||
	AnchorsZeroFeeHtlcTxRequired = 22,
 | 
			
		||||
	AnchorsZeroFeeHtlcTxOptional = 23,
 | 
			
		||||
	ShutdownAnySegwitRequired = 26,
 | 
			
		||||
	ShutdownAnySegwitOptional = 27,
 | 
			
		||||
	AMPRequired = 30,
 | 
			
		||||
	AMPOptional = 31,
 | 
			
		||||
	ExplicitChannelTypeRequired = 44,
 | 
			
		||||
	ExplicitChannelTypeOptional = 45,
 | 
			
		||||
	ScidAliasRequired = 46,
 | 
			
		||||
	ScidAliasOptional = 47,
 | 
			
		||||
	PaymentMetadataRequired = 48,
 | 
			
		||||
	PaymentMetadataOptional = 49,
 | 
			
		||||
	ZeroConfRequired = 50,
 | 
			
		||||
	ZeroConfOptional = 51,
 | 
			
		||||
	KeysendRequired = 54,
 | 
			
		||||
	KeysendOptional = 55,
 | 
			
		||||
	ScriptEnforcedLeaseRequired = 2022,
 | 
			
		||||
	ScriptEnforcedLeaseOptional = 2023,
 | 
			
		||||
	MaxBolt11Feature = 5114,
 | 
			
		||||
};
 | 
			
		||||
  
 | 
			
		||||
export const FeaturesMap = new Map<FeatureBits, string>([
 | 
			
		||||
	[FeatureBits.DataLossProtectRequired, 'data-loss-protect'],
 | 
			
		||||
	[FeatureBits.DataLossProtectOptional, 'data-loss-protect'],
 | 
			
		||||
	[FeatureBits.InitialRoutingSync, 'initial-routing-sync'],
 | 
			
		||||
	[FeatureBits.UpfrontShutdownScriptRequired, 'upfront-shutdown-script'],
 | 
			
		||||
	[FeatureBits.UpfrontShutdownScriptOptional, 'upfront-shutdown-script'],
 | 
			
		||||
	[FeatureBits.GossipQueriesRequired, 'gossip-queries'],
 | 
			
		||||
	[FeatureBits.GossipQueriesOptional, 'gossip-queries'],
 | 
			
		||||
	[FeatureBits.TLVOnionPayloadRequired, 'tlv-onion'],
 | 
			
		||||
	[FeatureBits.TLVOnionPayloadOptional, 'tlv-onion'],
 | 
			
		||||
	[FeatureBits.StaticRemoteKeyOptional, 'static-remote-key'],
 | 
			
		||||
	[FeatureBits.StaticRemoteKeyRequired, 'static-remote-key'],
 | 
			
		||||
	[FeatureBits.PaymentAddrOptional, 'payment-addr'],
 | 
			
		||||
	[FeatureBits.PaymentAddrRequired, 'payment-addr'],
 | 
			
		||||
	[FeatureBits.MPPOptional, 'multi-path-payments'],
 | 
			
		||||
	[FeatureBits.MPPRequired, 'multi-path-payments'],
 | 
			
		||||
	[FeatureBits.AnchorsRequired, 'anchor-commitments'],
 | 
			
		||||
	[FeatureBits.AnchorsOptional, 'anchor-commitments'],
 | 
			
		||||
	[FeatureBits.AnchorsZeroFeeHtlcTxRequired, 'anchors-zero-fee-htlc-tx'],
 | 
			
		||||
	[FeatureBits.AnchorsZeroFeeHtlcTxOptional, 'anchors-zero-fee-htlc-tx'],
 | 
			
		||||
	[FeatureBits.WumboChannelsRequired, 'wumbo-channels'],
 | 
			
		||||
	[FeatureBits.WumboChannelsOptional, 'wumbo-channels'],
 | 
			
		||||
	[FeatureBits.AMPRequired, 'amp'],
 | 
			
		||||
	[FeatureBits.AMPOptional, 'amp'],
 | 
			
		||||
	[FeatureBits.PaymentMetadataOptional, 'payment-metadata'],
 | 
			
		||||
	[FeatureBits.PaymentMetadataRequired, 'payment-metadata'],
 | 
			
		||||
	[FeatureBits.ExplicitChannelTypeOptional, 'explicit-commitment-type'],
 | 
			
		||||
	[FeatureBits.ExplicitChannelTypeRequired, 'explicit-commitment-type'],
 | 
			
		||||
	[FeatureBits.KeysendOptional, 'keysend'],
 | 
			
		||||
	[FeatureBits.KeysendRequired, 'keysend'],
 | 
			
		||||
	[FeatureBits.ScriptEnforcedLeaseRequired, 'script-enforced-lease'],
 | 
			
		||||
	[FeatureBits.ScriptEnforcedLeaseOptional, 'script-enforced-lease'],
 | 
			
		||||
	[FeatureBits.ScidAliasRequired, 'scid-alias'],
 | 
			
		||||
	[FeatureBits.ScidAliasOptional, 'scid-alias'],
 | 
			
		||||
	[FeatureBits.ZeroConfRequired, 'zero-conf'],
 | 
			
		||||
	[FeatureBits.ZeroConfOptional, 'zero-conf'],
 | 
			
		||||
	[FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'],
 | 
			
		||||
	[FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'],
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Convert a clightning "listnode" entry to a lnd node entry
 | 
			
		||||
 */
 | 
			
		||||
@ -17,10 +100,36 @@ export function convertNode(clNode: any): ILightningApi.Node {
 | 
			
		||||
      custom_records = undefined;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const nodeFeatures: ILightningApi.Feature[] = [];
 | 
			
		||||
  const nodeFeaturesBinary = hex2bin(clNode.features).split('').reverse().join('');
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < nodeFeaturesBinary.length; i++) {
 | 
			
		||||
    if (nodeFeaturesBinary[i] === '0') {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    const feature = FeaturesMap.get(i);
 | 
			
		||||
    if (!feature) {
 | 
			
		||||
      nodeFeatures.push({
 | 
			
		||||
        bit: i,
 | 
			
		||||
        name: 'unknown',
 | 
			
		||||
        is_required: i % 2 === 0,
 | 
			
		||||
        is_known: false
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      nodeFeatures.push({
 | 
			
		||||
        bit: i,
 | 
			
		||||
        name: feature,
 | 
			
		||||
        is_required: i % 2 === 0,
 | 
			
		||||
        is_known: true
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    alias: clNode.alias ?? '',
 | 
			
		||||
    color: `#${clNode.color ?? ''}`,
 | 
			
		||||
    features: [], // TODO parse and return clNode.feature
 | 
			
		||||
    features: nodeFeatures,
 | 
			
		||||
    pub_key: clNode.nodeid,
 | 
			
		||||
    addresses: clNode.addresses?.map((addr) => {
 | 
			
		||||
      let address = addr.address;
 | 
			
		||||
 | 
			
		||||
@ -79,6 +79,7 @@ export namespace ILightningApi {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Feature {
 | 
			
		||||
    bit: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
    is_required: boolean;
 | 
			
		||||
    is_known: boolean;
 | 
			
		||||
 | 
			
		||||
@ -41,8 +41,23 @@ class LndApi implements AbstractLightningApi {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getNetworkGraph(): Promise<ILightningApi.NetworkGraph> {
 | 
			
		||||
    return axios.get<ILightningApi.NetworkGraph>(config.LND.REST_API_URL + '/v1/graph', this.axiosConfig)
 | 
			
		||||
    const graph = await axios.get<ILightningApi.NetworkGraph>(config.LND.REST_API_URL + '/v1/graph', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
 | 
			
		||||
    for (const node of graph.nodes) {
 | 
			
		||||
      const nodeFeatures: ILightningApi.Feature[] = [];
 | 
			
		||||
      for (const bit in node.features) {        
 | 
			
		||||
        nodeFeatures.push({
 | 
			
		||||
          bit: parseInt(bit, 10),
 | 
			
		||||
          name: node.features[bit].name,  
 | 
			
		||||
          is_required: node.features[bit].is_required,
 | 
			
		||||
          is_known: node.features[bit].is_known,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      node.features = nodeFeatures;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return graph;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ import logger from '../../logger';
 | 
			
		||||
import channelsApi from '../../api/explorer/channels.api';
 | 
			
		||||
import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
 | 
			
		||||
import { ILightningApi } from '../../api/lightning/lightning-api.interface';
 | 
			
		||||
import { $lookupNodeLocation } from './sync-tasks/node-locations';
 | 
			
		||||
import lightningApi from '../../api/lightning/lightning-api-factory';
 | 
			
		||||
 | 
			
		||||
@ -26,4 +26,70 @@ export function formatBytes(bytes: number, toUnit: string, skipUnit = false): st
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return `${bytes.toFixed(2)}${skipUnit ? '' : ' ' + byteUnits[unitIndex]}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://stackoverflow.com/a/64235212
 | 
			
		||||
export function hex2bin(hex: string): string {
 | 
			
		||||
  if (!hex) {
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  hex = hex.replace('0x', '').toLowerCase();
 | 
			
		||||
  let out = '';
 | 
			
		||||
 | 
			
		||||
  for (const c of hex) {
 | 
			
		||||
    switch (c) {
 | 
			
		||||
      case '0': out += '0000'; break;
 | 
			
		||||
      case '1': out += '0001'; break;
 | 
			
		||||
      case '2': out += '0010'; break;
 | 
			
		||||
      case '3': out += '0011'; break;
 | 
			
		||||
      case '4': out += '0100'; break;
 | 
			
		||||
      case '5': out += '0101'; break;
 | 
			
		||||
      case '6': out += '0110'; break;
 | 
			
		||||
      case '7': out += '0111'; break;
 | 
			
		||||
      case '8': out += '1000'; break;
 | 
			
		||||
      case '9': out += '1001'; break;
 | 
			
		||||
      case 'a': out += '1010'; break;
 | 
			
		||||
      case 'b': out += '1011'; break;
 | 
			
		||||
      case 'c': out += '1100'; break;
 | 
			
		||||
      case 'd': out += '1101'; break;
 | 
			
		||||
      case 'e': out += '1110'; break;
 | 
			
		||||
      case 'f': out += '1111'; break;
 | 
			
		||||
      default: return '';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function bin2hex(bin: string): string {
 | 
			
		||||
  if (!bin) {
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let out = '';
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < bin.length; i += 4) {
 | 
			
		||||
    const c = bin.substring(i, i + 4);
 | 
			
		||||
    switch (c) {
 | 
			
		||||
      case '0000': out += '0'; break;
 | 
			
		||||
      case '0001': out += '1'; break;
 | 
			
		||||
      case '0010': out += '2'; break;
 | 
			
		||||
      case '0011': out += '3'; break;
 | 
			
		||||
      case '0100': out += '4'; break;
 | 
			
		||||
      case '0101': out += '5'; break;
 | 
			
		||||
      case '0110': out += '6'; break;
 | 
			
		||||
      case '0111': out += '7'; break;
 | 
			
		||||
      case '1000': out += '8'; break;
 | 
			
		||||
      case '1001': out += '9'; break;
 | 
			
		||||
      case '1010': out += 'a'; break;
 | 
			
		||||
      case '1011': out += 'b'; break;
 | 
			
		||||
      case '1100': out += 'c'; break;
 | 
			
		||||
      case '1101': out += 'd'; break;
 | 
			
		||||
      case '1110': out += 'e'; break;
 | 
			
		||||
      case '1111': out += 'f'; break;
 | 
			
		||||
      default: return '';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return out;
 | 
			
		||||
}
 | 
			
		||||
@ -21,7 +21,6 @@
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="box" *ngIf="!error">
 | 
			
		||||
 | 
			
		||||
    <div class="row">
 | 
			
		||||
      <div class="col-md">
 | 
			
		||||
        <table class="table table-borderless table-striped table-fixed">
 | 
			
		||||
@ -59,6 +58,9 @@
 | 
			
		||||
              <td i18n="lightning.avg-distance" class="text-truncate">Avg channel distance</td>
 | 
			
		||||
              <td class="direction-ltr">{{ avgDistance | amountShortener: 1 }} <span class="symbol">km</span> <span class="separator">·</span>{{ kmToMiles(avgDistance) | amountShortener: 1 }} <span class="symbol">mi</span></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr *ngIf="!node.geolocation" class="d-none d-md-table-row">
 | 
			
		||||
              <ng-container *ngTemplateOutlet="featurebits;context:{bits: node.featuresBits}"></ng-container>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      </div>
 | 
			
		||||
@ -100,11 +102,50 @@
 | 
			
		||||
                </td>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr *ngIf="node.geolocation && node.featuresBits">
 | 
			
		||||
              <ng-container *ngTemplateOutlet="featurebits;context:{bits: node.featuresBits}"></ng-container>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr *ngIf="!node.geolocation && node.featuresBits" class="d-table-row d-md-none">
 | 
			
		||||
              <ng-container *ngTemplateOutlet="featurebits;context:{bits: node.featuresBits}"></ng-container>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <ng-template #featurebits let-bits="bits">
 | 
			
		||||
    <td i18n="lightning.features" class="text-truncate label">Features</td>
 | 
			
		||||
    <td class="d-flex justify-content-between">
 | 
			
		||||
      <span class="text-truncate w-90">{{ bits }}</span>
 | 
			
		||||
      <button type="button" class="btn btn-outline-info btn-xs" (click)="toggleFeatures()" i18n="transaction.details|Transaction Details">Details</button>
 | 
			
		||||
    </td>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <div class="box mt-2" *ngIf="!error && showFeatures">
 | 
			
		||||
    <div class="row">
 | 
			
		||||
      <div class="col-md">
 | 
			
		||||
        <div class="mb-3">
 | 
			
		||||
          <h5>Raw bits</h5>
 | 
			
		||||
          <span class="text-wrap w-100"><small>{{ node.featuresBits }}</small></span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <h5>Decoded</h5>
 | 
			
		||||
        <table class="table table-borderless table-striped table-fixed">
 | 
			
		||||
          <thead>
 | 
			
		||||
            <th style="width: 13%">Bit</th>
 | 
			
		||||
            <th>Name</th>
 | 
			
		||||
            <th style="width: 25%; text-align: right">Required</th>
 | 
			
		||||
          </thead>
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <tr *ngFor="let feature of node.features">
 | 
			
		||||
              <td style="width: 13%">{{ feature.bit }}</td>
 | 
			
		||||
              <td>{{ feature.name }}</td>
 | 
			
		||||
              <td style="width: 25%; text-align: right">{{ feature.is_required }}</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="input-group mt-3" *ngIf="!error && node.socketsObject.length">
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@ export class NodeComponent implements OnInit {
 | 
			
		||||
  liquidityAd: ILiquidityAd;
 | 
			
		||||
  tlvRecords: CustomRecord[];
 | 
			
		||||
  avgChannelDistance$: Observable<number | null>;
 | 
			
		||||
 | 
			
		||||
  showFeatures = false;
 | 
			
		||||
  kmToMiles = kmToMiles;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
@ -164,4 +164,9 @@ export class NodeComponent implements OnInit {
 | 
			
		||||
  onLoadingEvent(e) {
 | 
			
		||||
    this.channelListLoading = e;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleFeatures() {
 | 
			
		||||
    this.showFeatures = !this.showFeatures;
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1164,3 +1164,10 @@ app-master-page, app-liquid-master-page, app-bisq-master-page {
 | 
			
		||||
app-global-footer {
 | 
			
		||||
  margin-top: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-xs {
 | 
			
		||||
  padding: 0.25rem 0.5rem;
 | 
			
		||||
  font-size: 0.875rem;
 | 
			
		||||
  line-height: 0.5;
 | 
			
		||||
  border-radius: 0.2rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user