Merge branch 'master' into nymkappa/bugfix/log-counter
This commit is contained in:
		
						commit
						76764936f9
					
				@ -129,6 +129,56 @@ class NodesApi {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getFeeHistogram(node_public_key: string): Promise<unknown> {
 | 
			
		||||
    try {
 | 
			
		||||
      const inQuery = `
 | 
			
		||||
        SELECT CASE WHEN fee_rate <= 10.0 THEN CEIL(fee_rate)
 | 
			
		||||
                    WHEN (fee_rate > 10.0 and fee_rate <= 100.0) THEN CEIL(fee_rate / 10.0) * 10.0
 | 
			
		||||
                    WHEN (fee_rate > 100.0 and fee_rate <= 1000.0) THEN CEIL(fee_rate / 100.0) * 100.0
 | 
			
		||||
                    WHEN fee_rate > 1000.0 THEN CEIL(fee_rate / 1000.0) * 1000.0
 | 
			
		||||
               END as bucket,
 | 
			
		||||
               count(short_id) as count,
 | 
			
		||||
               sum(capacity) as capacity
 | 
			
		||||
        FROM (
 | 
			
		||||
          SELECT CASE WHEN node1_public_key = ? THEN node2_fee_rate WHEN node2_public_key = ? THEN node1_fee_rate END as fee_rate,
 | 
			
		||||
                 short_id as short_id,
 | 
			
		||||
                 capacity as capacity
 | 
			
		||||
          FROM channels
 | 
			
		||||
          WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
 | 
			
		||||
        ) as fee_rate_table
 | 
			
		||||
        GROUP BY bucket;
 | 
			
		||||
      `;
 | 
			
		||||
      const [inRows]: any[] = await DB.query(inQuery, [node_public_key, node_public_key, node_public_key, node_public_key]);
 | 
			
		||||
 | 
			
		||||
      const outQuery = `
 | 
			
		||||
        SELECT CASE WHEN fee_rate <= 10.0 THEN CEIL(fee_rate)
 | 
			
		||||
                    WHEN (fee_rate > 10.0 and fee_rate <= 100.0) THEN CEIL(fee_rate / 10.0) * 10.0
 | 
			
		||||
                    WHEN (fee_rate > 100.0 and fee_rate <= 1000.0) THEN CEIL(fee_rate / 100.0) * 100.0
 | 
			
		||||
                    WHEN fee_rate > 1000.0 THEN CEIL(fee_rate / 1000.0) * 1000.0
 | 
			
		||||
               END as bucket,
 | 
			
		||||
               count(short_id) as count,
 | 
			
		||||
               sum(capacity) as capacity
 | 
			
		||||
        FROM (
 | 
			
		||||
          SELECT CASE WHEN node1_public_key = ? THEN node1_fee_rate WHEN node2_public_key = ? THEN node2_fee_rate END as fee_rate,
 | 
			
		||||
                 short_id as short_id,
 | 
			
		||||
                 capacity as capacity
 | 
			
		||||
          FROM channels
 | 
			
		||||
          WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
 | 
			
		||||
        ) as fee_rate_table
 | 
			
		||||
        GROUP BY bucket;
 | 
			
		||||
      `;
 | 
			
		||||
      const [outRows]: any[] = await DB.query(outQuery, [node_public_key, node_public_key, node_public_key, node_public_key]);
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        incoming: inRows.length > 0 ? inRows : [],
 | 
			
		||||
        outgoing: outRows.length > 0 ? outRows : [],
 | 
			
		||||
      };
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot get node fee distribution for ${node_public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getAllNodes(): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `SELECT * FROM nodes`;
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ class NodesRoutes {
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/connectivity', this.$getTopNodesByChannels)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/age', this.$getOldestNodes)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/fees/histogram', this.$getFeeHistogram)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/group/:name', this.$getNodeGroup)
 | 
			
		||||
    ;
 | 
			
		||||
@ -95,6 +96,22 @@ class NodesRoutes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getFeeHistogram(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const node = await nodesApi.$getFeeHistogram(req.params.public_key);
 | 
			
		||||
      if (!node) {
 | 
			
		||||
        res.status(404).send('Node not found');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
 | 
			
		||||
      res.json(node);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getNodesRanking(req: Request, res: Response): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const topCapacityNodes = await nodesApi.$getTopCapacityNodes(false);
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "ng": "./node_modules/@angular/cli/bin/ng.js",
 | 
			
		||||
    "tsc": "./node_modules/typescript/bin/tsc",
 | 
			
		||||
    "i18n-extract-from-source": "./node_modules/@angular/cli/bin/ng extract-i18n --out-file ./src/locale/messages.xlf",
 | 
			
		||||
    "i18n-extract-from-source": "npm run ng -- extract-i18n --out-file ./src/locale/messages.xlf",
 | 
			
		||||
    "i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force",
 | 
			
		||||
    "serve": "npm run generate-config && npm run ng -- serve -c local",
 | 
			
		||||
    "serve:stg": "npm run generate-config && npm run ng -- serve -c staging",
 | 
			
		||||
 | 
			
		||||
@ -74,12 +74,14 @@ let routes: Routes = [
 | 
			
		||||
            children: [],
 | 
			
		||||
            component: AddressComponent,
 | 
			
		||||
            data: {
 | 
			
		||||
              ogImage: true
 | 
			
		||||
              ogImage: true,
 | 
			
		||||
              networkSpecific: true,
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'tx',
 | 
			
		||||
            component: StartComponent,
 | 
			
		||||
            data: { networkSpecific: true },
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
                path: ':id',
 | 
			
		||||
@ -90,6 +92,7 @@ let routes: Routes = [
 | 
			
		||||
          {
 | 
			
		||||
            path: 'block',
 | 
			
		||||
            component: StartComponent,
 | 
			
		||||
            data: { networkSpecific: true },
 | 
			
		||||
              children: [
 | 
			
		||||
              {
 | 
			
		||||
                path: ':id',
 | 
			
		||||
@ -102,6 +105,7 @@ let routes: Routes = [
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'block-audit',
 | 
			
		||||
            data: { networkSpecific: true },
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
                path: ':id',
 | 
			
		||||
@ -121,12 +125,13 @@ let routes: Routes = [
 | 
			
		||||
          {
 | 
			
		||||
            path: 'lightning',
 | 
			
		||||
            loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
 | 
			
		||||
            data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true },
 | 
			
		||||
            data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'status',
 | 
			
		||||
        data: { networks: ['bitcoin', 'liquid'] },
 | 
			
		||||
        component: StatusViewComponent
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
@ -185,11 +190,13 @@ let routes: Routes = [
 | 
			
		||||
            children: [],
 | 
			
		||||
            component: AddressComponent,
 | 
			
		||||
            data: {
 | 
			
		||||
              ogImage: true
 | 
			
		||||
              ogImage: true,
 | 
			
		||||
              networkSpecific: true,
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'tx',
 | 
			
		||||
            data: { networkSpecific: true },
 | 
			
		||||
            component: StartComponent,
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
@ -200,6 +207,7 @@ let routes: Routes = [
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'block',
 | 
			
		||||
            data: { networkSpecific: true },
 | 
			
		||||
            component: StartComponent,
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
@ -213,6 +221,7 @@ let routes: Routes = [
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'block-audit',
 | 
			
		||||
            data: { networkSpecific: true },
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
                path: ':id',
 | 
			
		||||
@ -230,12 +239,14 @@ let routes: Routes = [
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'lightning',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'status',
 | 
			
		||||
        data: { networks: ['bitcoin', 'liquid'] },
 | 
			
		||||
        component: StatusViewComponent
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
@ -291,11 +302,13 @@ let routes: Routes = [
 | 
			
		||||
        children: [],
 | 
			
		||||
        component: AddressComponent,
 | 
			
		||||
        data: {
 | 
			
		||||
          ogImage: true
 | 
			
		||||
          ogImage: true,
 | 
			
		||||
          networkSpecific: true,
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'tx',
 | 
			
		||||
        data: { networkSpecific: true },
 | 
			
		||||
        component: StartComponent,
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
@ -306,6 +319,7 @@ let routes: Routes = [
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'block',
 | 
			
		||||
        data: { networkSpecific: true },
 | 
			
		||||
        component: StartComponent,
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
@ -319,6 +333,7 @@ let routes: Routes = [
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'block-audit',
 | 
			
		||||
        data: { networkSpecific: true },
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
            path: ':id',
 | 
			
		||||
@ -336,6 +351,7 @@ let routes: Routes = [
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'lightning',
 | 
			
		||||
        data: { networks: ['bitcoin'] },
 | 
			
		||||
        loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
@ -359,6 +375,7 @@ let routes: Routes = [
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'status',
 | 
			
		||||
    data: { networks: ['bitcoin', 'liquid'] },
 | 
			
		||||
    component: StatusViewComponent
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
@ -422,11 +439,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
              children: [],
 | 
			
		||||
              component: AddressComponent,
 | 
			
		||||
              data: {
 | 
			
		||||
                ogImage: true
 | 
			
		||||
                ogImage: true,
 | 
			
		||||
                networkSpecific: true,
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              path: 'tx',
 | 
			
		||||
              data: { networkSpecific: true },
 | 
			
		||||
              component: StartComponent,
 | 
			
		||||
              children: [
 | 
			
		||||
                {
 | 
			
		||||
@ -437,6 +456,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              path: 'block',
 | 
			
		||||
              data: { networkSpecific: true },
 | 
			
		||||
              component: StartComponent,
 | 
			
		||||
              children: [
 | 
			
		||||
                {
 | 
			
		||||
@ -450,18 +470,22 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              path: 'assets',
 | 
			
		||||
              data: { networks: ['liquid'] },
 | 
			
		||||
              component: AssetsNavComponent,
 | 
			
		||||
              children: [
 | 
			
		||||
                {
 | 
			
		||||
                  path: 'all',
 | 
			
		||||
                  data: { networks: ['liquid'] },
 | 
			
		||||
                  component: AssetsComponent,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  path: 'asset/:id',
 | 
			
		||||
                  data: { networkSpecific: true },
 | 
			
		||||
                  component: AssetComponent
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  path: 'group/:id',
 | 
			
		||||
                  data: { networkSpecific: true },
 | 
			
		||||
                  component: AssetGroupComponent
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
@ -482,6 +506,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'status',
 | 
			
		||||
          data: { networks: ['bitcoin', 'liquid'] },
 | 
			
		||||
          component: StatusViewComponent
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
@ -532,11 +557,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
          children: [],
 | 
			
		||||
          component: AddressComponent,
 | 
			
		||||
          data: {
 | 
			
		||||
            ogImage: true
 | 
			
		||||
            ogImage: true,
 | 
			
		||||
            networkSpecific: true,
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'tx',
 | 
			
		||||
          data: { networkSpecific: true },
 | 
			
		||||
          component: StartComponent,
 | 
			
		||||
          children: [
 | 
			
		||||
            {
 | 
			
		||||
@ -547,6 +574,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'block',
 | 
			
		||||
          data: { networkSpecific: true },
 | 
			
		||||
          component: StartComponent,
 | 
			
		||||
          children: [
 | 
			
		||||
            {
 | 
			
		||||
@ -560,22 +588,27 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'assets',
 | 
			
		||||
          data: { networks: ['liquid'] },
 | 
			
		||||
          component: AssetsNavComponent,
 | 
			
		||||
          children: [
 | 
			
		||||
            {
 | 
			
		||||
              path: 'featured',
 | 
			
		||||
              data: { networkSpecific: true },
 | 
			
		||||
              component: AssetsFeaturedComponent,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              path: 'all',
 | 
			
		||||
              data: { networks: ['liquid'] },
 | 
			
		||||
              component: AssetsComponent,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              path: 'asset/:id',
 | 
			
		||||
              data: { networkSpecific: true },
 | 
			
		||||
              component: AssetComponent
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              path: 'group/:id',
 | 
			
		||||
              data: { networkSpecific: true },
 | 
			
		||||
              component: AssetGroupComponent
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
@ -609,6 +642,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'status',
 | 
			
		||||
      data: { networks: ['bitcoin', 'liquid']},
 | 
			
		||||
      component: StatusViewComponent
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -20,14 +20,17 @@ const routes: Routes = [
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'markets',
 | 
			
		||||
      data: { networks: ['bisq'] },
 | 
			
		||||
      component: BisqDashboardComponent,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'transactions',
 | 
			
		||||
      data: { networks: ['bisq'] },
 | 
			
		||||
      component: BisqTransactionsComponent
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'market/:pair',
 | 
			
		||||
      data: { networkSpecific: true },
 | 
			
		||||
      component: BisqMarketComponent,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
@ -36,6 +39,7 @@ const routes: Routes = [
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'tx/:id',
 | 
			
		||||
      data: { networkSpecific: true },
 | 
			
		||||
      component: BisqTransactionComponent
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
@ -45,14 +49,17 @@ const routes: Routes = [
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'block/:id',
 | 
			
		||||
      data: { networkSpecific: true },
 | 
			
		||||
      component: BisqBlockComponent,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'address/:id',
 | 
			
		||||
      data: { networkSpecific: true },
 | 
			
		||||
      component: BisqAddressComponent,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'stats',
 | 
			
		||||
      data: { networks: ['bisq'] },
 | 
			
		||||
      component: BisqStatsComponent,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,8 @@ export class AppComponent implements OnInit {
 | 
			
		||||
    if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
 | 
			
		||||
      this.dir = 'rtl';
 | 
			
		||||
      this.class = 'rtl-layout';
 | 
			
		||||
    } else {
 | 
			
		||||
      this.class = 'ltr-layout';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tooltipConfig.animation = false;
 | 
			
		||||
 | 
			
		||||
@ -44,13 +44,13 @@
 | 
			
		||||
      <app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images>
 | 
			
		||||
    </button>
 | 
			
		||||
    <div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
 | 
			
		||||
      <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
 | 
			
		||||
      <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/signet'" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
 | 
			
		||||
      <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
 | 
			
		||||
      <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '/')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
 | 
			
		||||
      <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
 | 
			
		||||
      <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
 | 
			
		||||
      <h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
 | 
			
		||||
      <a ngbDropdownItem class="mainnet active" routerLink="/"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
 | 
			
		||||
      <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
 | 
			
		||||
      <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
 | 
			
		||||
      <a ngbDropdownItem class="mainnet active" [routerLink]="networkPaths['bisq'] || '/'"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
 | 
			
		||||
      <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '/')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
 | 
			
		||||
      <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { LanguageService } from '../../services/language.service';
 | 
			
		||||
import { EnterpriseService } from '../../services/enterprise.service';
 | 
			
		||||
import { NavigationService } from '../../services/navigation.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-bisq-master-page',
 | 
			
		||||
@ -15,17 +16,23 @@ export class BisqMasterPageComponent implements OnInit {
 | 
			
		||||
  env: Env;
 | 
			
		||||
  isMobile = window.innerWidth <= 767.98;
 | 
			
		||||
  urlLanguage: string;
 | 
			
		||||
  networkPaths: { [network: string]: string };
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private languageService: LanguageService,
 | 
			
		||||
    private enterpriseService: EnterpriseService,
 | 
			
		||||
    private navigationService: NavigationService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.env = this.stateService.env;
 | 
			
		||||
    this.connectionState$ = this.stateService.connectionState$;
 | 
			
		||||
    this.urlLanguage = this.languageService.getLanguageForUrl();
 | 
			
		||||
    this.navigationService.subnetPaths.subscribe((paths) => {
 | 
			
		||||
      console.log('network paths updated...');
 | 
			
		||||
      this.networkPaths = paths;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  collapse(): void {
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,6 @@
 | 
			
		||||
  left: 0;
 | 
			
		||||
  top: 75px;
 | 
			
		||||
  transform: translateX(50vw);
 | 
			
		||||
  transition: transform 1s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.position-container.liquid, .position-container.liquidtestnet {
 | 
			
		||||
@ -84,9 +83,9 @@
 | 
			
		||||
 | 
			
		||||
.time-toggle {
 | 
			
		||||
  color: white;
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
  font-size: 0.8rem;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  bottom: -1.5em;
 | 
			
		||||
  bottom: -1.8em;
 | 
			
		||||
  left: 1px;
 | 
			
		||||
  transform: translateX(-50%);
 | 
			
		||||
  background: none;
 | 
			
		||||
@ -97,14 +96,31 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blockchain-wrapper.ltr-transition .blocks-wrapper,
 | 
			
		||||
.blockchain-wrapper.ltr-transition .position-container,
 | 
			
		||||
.blockchain-wrapper.ltr-transition .time-toggle {
 | 
			
		||||
  transition: transform 1s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blockchain-wrapper.time-ltr .blocks-wrapper {
 | 
			
		||||
  transform: scaleX(-1);
 | 
			
		||||
.blockchain-wrapper.time-ltr {
 | 
			
		||||
  .blocks-wrapper {
 | 
			
		||||
    transform: scaleX(-1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .time-toggle {
 | 
			
		||||
    transform: translateX(-50%) scaleX(-1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blockchain-wrapper.time-ltr .time-toggle {
 | 
			
		||||
  transform: translateX(-50%) scaleX(-1);
 | 
			
		||||
:host-context(.ltr-layout) {
 | 
			
		||||
  .blockchain-wrapper.time-ltr .blocks-wrapper,
 | 
			
		||||
  .blockchain-wrapper .blocks-wrapper {
 | 
			
		||||
    direction: ltr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(.rtl-layout) {
 | 
			
		||||
  .blockchain-wrapper.time-ltr .blocks-wrapper,
 | 
			
		||||
  .blockchain-wrapper .blocks-wrapper {
 | 
			
		||||
    direction: rtl;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -31,17 +31,17 @@
 | 
			
		||||
    <button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="lightning">Lightning</button>
 | 
			
		||||
    <div ngbDropdownMenu aria-labelledby="dropdownBasic1">
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"
 | 
			
		||||
        i18n="lightning.nodes-networks">Lightning nodes per network</a>
 | 
			
		||||
        i18n="lightning.nodes-networks">Lightning Nodes Per Network</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/capacity' | relativeUrl]"
 | 
			
		||||
        i18n="lightning.capacity">Network capacity</a>
 | 
			
		||||
        i18n="lightning.network-capacity">Lightning Network Capacity</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-per-isp' | relativeUrl]"
 | 
			
		||||
        i18n="lightning.nodes-per-isp">Lightning nodes per ISP</a>
 | 
			
		||||
        i18n="lightning.nodes-per-isp">Lightning Nodes Per ISP</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-per-country' | relativeUrl]"
 | 
			
		||||
        i18n="lightning.nodes-per-country">Lightning nodes per country</a>
 | 
			
		||||
        i18n="lightning.nodes-per-country">Lightning Nodes Per Country</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-map' | relativeUrl]"
 | 
			
		||||
        i18n="lightning.lightning.nodes-heatmap">Lightning nodes world map</a>
 | 
			
		||||
        i18n="lightning.lightning.nodes-heatmap">Lightning Nodes World Map</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-channels-map' | relativeUrl]"
 | 
			
		||||
        i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</a>
 | 
			
		||||
        i18n="lightning.nodes-channels-world-map">Lightning Nodes Channels World Map</a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -49,13 +49,13 @@
 | 
			
		||||
      <app-svg-images [name]="network.val === '' ? 'liquid' : network.val" width="22" height="22" viewBox="0 0 125 125" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
 | 
			
		||||
    </button>
 | 
			
		||||
    <div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
 | 
			
		||||
      <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
 | 
			
		||||
      <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/signet'" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
 | 
			
		||||
      <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
 | 
			
		||||
      <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
 | 
			
		||||
      <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
 | 
			
		||||
      <a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
 | 
			
		||||
      <h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
 | 
			
		||||
      <a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
 | 
			
		||||
      <a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" routerLink="/"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
 | 
			
		||||
      <a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" routerLink="/testnet"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
 | 
			
		||||
      <a [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
 | 
			
		||||
      <a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
 | 
			
		||||
      <a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service';
 | 
			
		||||
import { merge, Observable, of} from 'rxjs';
 | 
			
		||||
import { LanguageService } from '../../services/language.service';
 | 
			
		||||
import { EnterpriseService } from '../../services/enterprise.service';
 | 
			
		||||
import { NavigationService } from '../../services/navigation.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-liquid-master-page',
 | 
			
		||||
@ -17,11 +18,13 @@ export class LiquidMasterPageComponent implements OnInit {
 | 
			
		||||
  officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
 | 
			
		||||
  network$: Observable<string>;
 | 
			
		||||
  urlLanguage: string;
 | 
			
		||||
  networkPaths: { [network: string]: string };
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private languageService: LanguageService,
 | 
			
		||||
    private enterpriseService: EnterpriseService,
 | 
			
		||||
    private navigationService: NavigationService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
@ -29,6 +32,10 @@ export class LiquidMasterPageComponent implements OnInit {
 | 
			
		||||
    this.connectionState$ = this.stateService.connectionState$;
 | 
			
		||||
    this.network$ = merge(of(''), this.stateService.networkChanged$);
 | 
			
		||||
    this.urlLanguage = this.languageService.getLanguageForUrl();
 | 
			
		||||
    this.navigationService.subnetPaths.subscribe((paths) => {
 | 
			
		||||
      console.log('network paths updated...');
 | 
			
		||||
      this.networkPaths = paths;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  collapse(): void {
 | 
			
		||||
 | 
			
		||||
@ -22,13 +22,13 @@
 | 
			
		||||
      <app-svg-images [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
 | 
			
		||||
    </button>
 | 
			
		||||
    <div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
 | 
			
		||||
      <a ngbDropdownItem class="mainnet" routerLink="/"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
 | 
			
		||||
      <a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
 | 
			
		||||
      <a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
 | 
			
		||||
      <a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
 | 
			
		||||
      <a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
 | 
			
		||||
      <a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
 | 
			
		||||
      <h6 *ngIf="env.LIQUID_ENABLED || env.BISQ_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
 | 
			
		||||
      <a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
 | 
			
		||||
      <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
 | 
			
		||||
      <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
 | 
			
		||||
      <a [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
 | 
			
		||||
      <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
 | 
			
		||||
      <a [href]="env.LIQUID_WEBSITE_URL + urlLanguage  + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service';
 | 
			
		||||
import { Observable, merge, of } from 'rxjs';
 | 
			
		||||
import { LanguageService } from '../../services/language.service';
 | 
			
		||||
import { EnterpriseService } from '../../services/enterprise.service';
 | 
			
		||||
import { NavigationService } from '../../services/navigation.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-master-page',
 | 
			
		||||
@ -18,11 +19,13 @@ export class MasterPageComponent implements OnInit {
 | 
			
		||||
  officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
 | 
			
		||||
  urlLanguage: string;
 | 
			
		||||
  subdomain = '';
 | 
			
		||||
  networkPaths: { [network: string]: string };
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private languageService: LanguageService,
 | 
			
		||||
    private enterpriseService: EnterpriseService,
 | 
			
		||||
    private navigationService: NavigationService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
@ -31,6 +34,10 @@ export class MasterPageComponent implements OnInit {
 | 
			
		||||
    this.network$ = merge(of(''), this.stateService.networkChanged$);
 | 
			
		||||
    this.urlLanguage = this.languageService.getLanguageForUrl();
 | 
			
		||||
    this.subdomain = this.enterpriseService.getSubdomain();
 | 
			
		||||
    this.navigationService.subnetPaths.subscribe((paths) => {
 | 
			
		||||
      console.log('network paths updated...');
 | 
			
		||||
      this.networkPaths = paths;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  collapse(): void {
 | 
			
		||||
 | 
			
		||||
@ -146,4 +146,10 @@
 | 
			
		||||
  .block-body {
 | 
			
		||||
    transform: scaleX(-1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(.rtl-layout) {
 | 
			
		||||
  #arrow-up {
 | 
			
		||||
    transform: translateX(70px);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -287,11 +287,12 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    this.arrowVisible = true;
 | 
			
		||||
 | 
			
		||||
    for (const block of this.mempoolBlocks) {
 | 
			
		||||
      for (let i = 0; i < block.feeRange.length - 1; i++) {
 | 
			
		||||
    let found = false;
 | 
			
		||||
    for (let txInBlockIndex = 0; txInBlockIndex < this.mempoolBlocks.length && !found; txInBlockIndex++) {
 | 
			
		||||
      const block = this.mempoolBlocks[txInBlockIndex];
 | 
			
		||||
      for (let i = 0; i < block.feeRange.length - 1 && !found; i++) {
 | 
			
		||||
        if (this.txFeePerVSize < block.feeRange[i + 1] && this.txFeePerVSize >= block.feeRange[i]) {
 | 
			
		||||
          const txInBlockIndex = this.mempoolBlocks.indexOf(block);
 | 
			
		||||
          const feeRangeIndex = block.feeRange.findIndex((val, index) => this.txFeePerVSize < block.feeRange[index + 1]);
 | 
			
		||||
          const feeRangeIndex = i;
 | 
			
		||||
          const feeRangeChunkSize = 1 / (block.feeRange.length - 1);
 | 
			
		||||
 | 
			
		||||
          const txFee = this.txFeePerVSize - block.feeRange[i];
 | 
			
		||||
@ -306,9 +307,13 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
            + ((1 - feePosition) * blockedFilledPercentage * this.blockWidth);
 | 
			
		||||
 | 
			
		||||
          this.rightPosition = arrowRightPosition;
 | 
			
		||||
          break;
 | 
			
		||||
          found = true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (this.txFeePerVSize >= block.feeRange[block.feeRange.length - 1]) {
 | 
			
		||||
        this.rightPosition = txInBlockIndex * (this.blockWidth + this.blockPadding);
 | 
			
		||||
        found = true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { AssetsService } from '../../services/assets.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Observable, of, Subject, zip, BehaviorSubject } from 'rxjs';
 | 
			
		||||
import { debounceTime, distinctUntilChanged, switchMap, catchError, map } from 'rxjs/operators';
 | 
			
		||||
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
 | 
			
		||||
import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith,  tap } from 'rxjs/operators';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
@ -24,7 +24,7 @@ export class SearchFormComponent implements OnInit {
 | 
			
		||||
  typeAhead$: Observable<any>;
 | 
			
		||||
  searchForm: FormGroup;
 | 
			
		||||
 | 
			
		||||
  regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/;
 | 
			
		||||
  regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
 | 
			
		||||
  regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
 | 
			
		||||
  regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
 | 
			
		||||
  regexBlockheight = /^[0-9]{1,9}$/;
 | 
			
		||||
@ -33,7 +33,7 @@ export class SearchFormComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  @Output() searchTriggered = new EventEmitter();
 | 
			
		||||
  @ViewChild('searchResults') searchResults: SearchResultsComponent;
 | 
			
		||||
  @HostListener('keydown', ['$event']) keydown($event) {
 | 
			
		||||
  @HostListener('keydown', ['$event']) keydown($event): void {
 | 
			
		||||
    this.handleKeyDown($event);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,7 @@ export class SearchFormComponent implements OnInit {
 | 
			
		||||
    private relativeUrlPipe: RelativeUrlPipe,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.stateService.networkChanged$.subscribe((network) => this.network = network);
 | 
			
		||||
 | 
			
		||||
    this.searchForm = this.formBuilder.group({
 | 
			
		||||
@ -61,70 +61,111 @@ export class SearchFormComponent implements OnInit {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.typeAhead$ = this.searchForm.get('searchText').valueChanges
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map((text) => {
 | 
			
		||||
          if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) {
 | 
			
		||||
            return text.substr(1);
 | 
			
		||||
          }
 | 
			
		||||
          return text.trim();
 | 
			
		||||
        }),
 | 
			
		||||
        debounceTime(200),
 | 
			
		||||
        distinctUntilChanged(),
 | 
			
		||||
        switchMap((text) => {
 | 
			
		||||
          if (!text.length) {
 | 
			
		||||
            return of([
 | 
			
		||||
              '',
 | 
			
		||||
              [],
 | 
			
		||||
              {
 | 
			
		||||
                nodes: [],
 | 
			
		||||
                channels: [],
 | 
			
		||||
              }
 | 
			
		||||
            ]);
 | 
			
		||||
          }
 | 
			
		||||
          this.isTypeaheading$.next(true);
 | 
			
		||||
          if (!this.stateService.env.LIGHTNING) {
 | 
			
		||||
            return zip(
 | 
			
		||||
              of(text),
 | 
			
		||||
              this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
 | 
			
		||||
              [{ nodes: [], channels: [] }],
 | 
			
		||||
              of(this.regexBlockheight.test(text)),
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
    const searchText$ = this.searchForm.get('searchText').valueChanges
 | 
			
		||||
    .pipe(
 | 
			
		||||
      map((text) => {
 | 
			
		||||
        if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) {
 | 
			
		||||
          return text.substr(1);
 | 
			
		||||
        }
 | 
			
		||||
        return text.trim();
 | 
			
		||||
      }),
 | 
			
		||||
      distinctUntilChanged(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const searchResults$ = searchText$.pipe(
 | 
			
		||||
      debounceTime(200),
 | 
			
		||||
      switchMap((text) => {
 | 
			
		||||
        if (!text.length) {
 | 
			
		||||
          return of([
 | 
			
		||||
            [],
 | 
			
		||||
            { nodes: [], channels: [] }
 | 
			
		||||
          ]);
 | 
			
		||||
        }
 | 
			
		||||
        this.isTypeaheading$.next(true);
 | 
			
		||||
        if (!this.stateService.env.LIGHTNING) {
 | 
			
		||||
          return zip(
 | 
			
		||||
            of(text),
 | 
			
		||||
            this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
 | 
			
		||||
            this.apiService.lightningSearch$(text).pipe(catchError(() => of({
 | 
			
		||||
            [{ nodes: [], channels: [] }],
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        return zip(
 | 
			
		||||
          this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
 | 
			
		||||
          this.apiService.lightningSearch$(text).pipe(catchError(() => of({
 | 
			
		||||
            nodes: [],
 | 
			
		||||
            channels: [],
 | 
			
		||||
          }))),
 | 
			
		||||
        );
 | 
			
		||||
      }),
 | 
			
		||||
      tap((result: any[]) => {
 | 
			
		||||
        this.isTypeaheading$.next(false);
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.typeAhead$ = combineLatest(
 | 
			
		||||
      [
 | 
			
		||||
        searchText$,
 | 
			
		||||
        searchResults$.pipe(
 | 
			
		||||
        startWith([
 | 
			
		||||
          [],
 | 
			
		||||
          {
 | 
			
		||||
            nodes: [],
 | 
			
		||||
            channels: [],
 | 
			
		||||
          }
 | 
			
		||||
        ]))
 | 
			
		||||
      ]
 | 
			
		||||
      ).pipe(
 | 
			
		||||
        map((latestData) => {
 | 
			
		||||
          const searchText = latestData[0];
 | 
			
		||||
          if (!searchText.length) {
 | 
			
		||||
            return {
 | 
			
		||||
              searchText: '',
 | 
			
		||||
              hashQuickMatch: false,
 | 
			
		||||
              blockHeight: false,
 | 
			
		||||
              txId: false,
 | 
			
		||||
              address: false,
 | 
			
		||||
              addresses: [],
 | 
			
		||||
              nodes: [],
 | 
			
		||||
              channels: [],
 | 
			
		||||
            }))),
 | 
			
		||||
          );
 | 
			
		||||
        }),
 | 
			
		||||
        map((result: any[]) => {
 | 
			
		||||
          this.isTypeaheading$.next(false);
 | 
			
		||||
          if (this.network === 'bisq') {
 | 
			
		||||
            return result[0].map((address: string) => 'B' + address);
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const result = latestData[1];
 | 
			
		||||
          const addressPrefixSearchResults = result[0];
 | 
			
		||||
          const lightningResults = result[1];
 | 
			
		||||
 | 
			
		||||
          if (this.network === 'bisq') {
 | 
			
		||||
            return searchText.map((address: string) => 'B' + address);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const matchesBlockHeight = this.regexBlockheight.test(searchText);
 | 
			
		||||
          const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
 | 
			
		||||
          const matchesBlockHash = this.regexBlockhash.test(searchText);
 | 
			
		||||
          const matchesAddress = this.regexAddress.test(searchText);
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            searchText: result[0],
 | 
			
		||||
            blockHeight: this.regexBlockheight.test(result[0]) ? [parseInt(result[0], 10)] : [],
 | 
			
		||||
            addresses: result[1],
 | 
			
		||||
            nodes: result[2].nodes,
 | 
			
		||||
            channels: result[2].channels,
 | 
			
		||||
            totalResults: result[1].length + result[2].nodes.length + result[2].channels.length,
 | 
			
		||||
            searchText: searchText,
 | 
			
		||||
            hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress),
 | 
			
		||||
            blockHeight: matchesBlockHeight,
 | 
			
		||||
            txId: matchesTxId,
 | 
			
		||||
            blockHash: matchesBlockHash,
 | 
			
		||||
            address: matchesAddress,
 | 
			
		||||
            addresses: addressPrefixSearchResults,
 | 
			
		||||
            nodes: lightningResults.nodes,
 | 
			
		||||
            channels: lightningResults.channels,
 | 
			
		||||
          };
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
  handleKeyDown($event) {
 | 
			
		||||
 | 
			
		||||
  handleKeyDown($event): void {
 | 
			
		||||
    this.searchResults.handleKeyDown($event);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  itemSelected() {
 | 
			
		||||
  itemSelected(): void {
 | 
			
		||||
    setTimeout(() => this.search());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  selectedResult(result: any) {
 | 
			
		||||
  selectedResult(result: any): void {
 | 
			
		||||
    if (typeof result === 'string') {
 | 
			
		||||
      this.search(result);
 | 
			
		||||
    } else if (typeof result === 'number') {
 | 
			
		||||
@ -136,7 +177,7 @@ export class SearchFormComponent implements OnInit {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  search(result?: string) {
 | 
			
		||||
  search(result?: string): void {
 | 
			
		||||
    const searchText = result || this.searchForm.value.searchText.trim();
 | 
			
		||||
    if (searchText) {
 | 
			
		||||
      this.isSearching = true;
 | 
			
		||||
@ -170,7 +211,7 @@ export class SearchFormComponent implements OnInit {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  navigate(url: string, searchText: string, extras?: any) {
 | 
			
		||||
  navigate(url: string, searchText: string, extras?: any): void {
 | 
			
		||||
    this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras);
 | 
			
		||||
    this.searchTriggered.emit();
 | 
			
		||||
    this.searchForm.setValue({
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,32 @@
 | 
			
		||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.blockHeight.length && !results.addresses.length && !results.nodes.length && !results.channels.length">
 | 
			
		||||
  <ng-template [ngIf]="results.blockHeight.length">
 | 
			
		||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.addresses.length && !results.nodes.length && !results.channels.length">
 | 
			
		||||
  <ng-template [ngIf]="results.blockHeight">
 | 
			
		||||
    <div class="card-title">Bitcoin Block Height</div>
 | 
			
		||||
    <button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
 | 
			
		||||
      Go to "{{ results.searchText }}"
 | 
			
		||||
    </button>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
  <ng-template [ngIf]="results.txId">
 | 
			
		||||
    <div class="card-title">Bitcoin Transaction</div>
 | 
			
		||||
    <button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
 | 
			
		||||
      Go to "{{ results.searchText | shortenString : 13 }}"
 | 
			
		||||
    </button>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
  <ng-template [ngIf]="results.address">
 | 
			
		||||
    <div class="card-title">Bitcoin Address</div>
 | 
			
		||||
    <button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
 | 
			
		||||
      Go to "{{ results.searchText | shortenString : isMobile ? 20 : 30 }}"
 | 
			
		||||
    </button>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
  <ng-template [ngIf]="results.blockHash">
 | 
			
		||||
    <div class="card-title">Bitcoin Block</div>
 | 
			
		||||
    <button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
 | 
			
		||||
      Go to "{{ results.searchText | shortenString : 13 }}"
 | 
			
		||||
    </button>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
  <ng-template [ngIf]="results.addresses.length">
 | 
			
		||||
    <div class="card-title" *ngIf="stateService.env.LIGHTNING">Bitcoin Addresses</div>
 | 
			
		||||
    <div class="card-title">Bitcoin Addresses</div>
 | 
			
		||||
    <ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index">
 | 
			
		||||
      <button (click)="clickItem(results.blockHeight.length + i)" [class.active]="(results.blockHeight.length + i) === activeIdx" type="button" role="option" class="dropdown-item">
 | 
			
		||||
      <button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" type="button" role="option" class="dropdown-item">
 | 
			
		||||
        <ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight>
 | 
			
		||||
      </button>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
@ -16,7 +34,7 @@
 | 
			
		||||
  <ng-template [ngIf]="results.nodes.length">
 | 
			
		||||
    <div class="card-title">Lightning Nodes</div>
 | 
			
		||||
    <ng-template ngFor [ngForOf]="results.nodes" let-node let-i="index">
 | 
			
		||||
      <button (click)="clickItem(results.blockHeight.length + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.blockHeight.length + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
 | 
			
		||||
      <button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
 | 
			
		||||
        <ngb-highlight [result]="node.alias" [term]="results.searchText"></ngb-highlight>  <span class="symbol">{{ node.public_key | shortenString : 10 }}</span>
 | 
			
		||||
      </button>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
@ -24,7 +42,7 @@
 | 
			
		||||
  <ng-template [ngIf]="results.channels.length">
 | 
			
		||||
    <div class="card-title">Lightning Channels</div>
 | 
			
		||||
    <ng-template ngFor [ngForOf]="results.channels" let-channel let-i="index">
 | 
			
		||||
      <button (click)="clickItem(results.blockHeight.length + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2"  [class.active]="results.blockHeight.length + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
 | 
			
		||||
      <button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2"  [class.active]="results.hashQuickMatch + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
 | 
			
		||||
        <ngb-highlight [result]="channel.short_id" [term]="results.searchText"></ngb-highlight>  <span class="symbol">{{ channel.id }}</span>
 | 
			
		||||
      </button>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ export class SearchResultsComponent implements OnChanges {
 | 
			
		||||
  ngOnChanges() {
 | 
			
		||||
    this.activeIdx = 0;
 | 
			
		||||
    if (this.results) {
 | 
			
		||||
      this.resultsFlattened = [...this.results.blockHeight, ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
 | 
			
		||||
      this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
 | 
			
		||||
<div *ngIf="countdown > 0" class="warning-label">{{ eventName }} in {{ countdown | number }} block{{ countdown === 1 ? '' : 's' }}!</div>
 | 
			
		||||
 | 
			
		||||
<div id="blockchain-container" dir="ltr" #blockchainContainer
 | 
			
		||||
<div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer
 | 
			
		||||
  (mousedown)="onMouseDown($event)"
 | 
			
		||||
  (dragstart)="onDragStart($event)"
 | 
			
		||||
>
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild } from '@angular/core';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { specialBlocks } from '../../app.constants';
 | 
			
		||||
 | 
			
		||||
@ -7,7 +8,7 @@ import { specialBlocks } from '../../app.constants';
 | 
			
		||||
  templateUrl: './start.component.html',
 | 
			
		||||
  styleUrls: ['./start.component.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class StartComponent implements OnInit {
 | 
			
		||||
export class StartComponent implements OnInit, OnDestroy {
 | 
			
		||||
  interval = 60;
 | 
			
		||||
  colors = ['#5E35B1', '#ffffff'];
 | 
			
		||||
 | 
			
		||||
@ -16,6 +17,8 @@ export class StartComponent implements OnInit {
 | 
			
		||||
  eventName = '';
 | 
			
		||||
  mouseDragStartX: number;
 | 
			
		||||
  blockchainScrollLeftInit: number;
 | 
			
		||||
  timeLtrSubscription: Subscription;
 | 
			
		||||
  timeLtr: boolean = this.stateService.timeLtr.value;
 | 
			
		||||
  @ViewChild('blockchainContainer') blockchainContainer: ElementRef;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
@ -23,6 +26,9 @@ export class StartComponent implements OnInit {
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
 | 
			
		||||
      this.timeLtr = !!ltr;
 | 
			
		||||
    });
 | 
			
		||||
    this.stateService.blocks$
 | 
			
		||||
      .subscribe((blocks: any) => {
 | 
			
		||||
        if (this.stateService.network !== '') {
 | 
			
		||||
@ -72,4 +78,8 @@ export class StartComponent implements OnInit {
 | 
			
		||||
    this.mouseDragStartX = null;
 | 
			
		||||
    this.stateService.setBlockScrollingInProgress(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.timeLtrSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </span>
 | 
			
		||||
    <span class="field col-sm-4 text-center"><ng-container *ngIf="transactionTime > 0">‎{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-container></span>
 | 
			
		||||
    <span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee </span>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
 | 
			
		||||
    <span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee</span> {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -190,12 +190,12 @@
 | 
			
		||||
 | 
			
		||||
    <br>
 | 
			
		||||
 | 
			
		||||
    <ng-container *ngIf="showFlow; else flowPlaceholder">
 | 
			
		||||
    <ng-container *ngIf="flowEnabled; else flowPlaceholder">
 | 
			
		||||
      <div class="title float-left">
 | 
			
		||||
        <h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="toggleGraph()" i18n="hide-flow-diagram">Hide flow diagram</button>
 | 
			
		||||
      <button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="toggleGraph()" i18n="hide-diagram">Hide diagram</button>
 | 
			
		||||
 | 
			
		||||
      <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
@ -208,7 +208,11 @@
 | 
			
		||||
            [lineLimit]="inOutLimit"
 | 
			
		||||
            [maxStrands]="graphExpanded ? maxInOut : 24"
 | 
			
		||||
            [network]="network"
 | 
			
		||||
            [tooltip]="true">
 | 
			
		||||
            [tooltip]="true"
 | 
			
		||||
            [inputIndex]="inputIndex" [outputIndex]="outputIndex"
 | 
			
		||||
            (selectInput)="selectInput($event)"
 | 
			
		||||
            (selectOutput)="selectOutput($event)"
 | 
			
		||||
          >
 | 
			
		||||
          </tx-bowtie-graph>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="toggle-wrapper" *ngIf="maxInOut > 24">
 | 
			
		||||
@ -234,13 +238,13 @@
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="title-buttons">
 | 
			
		||||
        <button *ngIf="!showFlow" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show">Show flow diagram</button>
 | 
			
		||||
        <button *ngIf="!flowEnabled" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show-diagram">Show diagram</button>
 | 
			
		||||
        <button type="button" class="btn btn-outline-info btn-sm" (click)="txList.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <app-transactions-list #txList [transactions]="[tx]" [errorUnblinded]="errorUnblinded" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list>
 | 
			
		||||
    <app-transactions-list #txList [transactions]="[tx]" [errorUnblinded]="errorUnblinded" [inputIndex]="inputIndex" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list>
 | 
			
		||||
 | 
			
		||||
    <div class="title text-left">
 | 
			
		||||
      <h2 i18n="transaction.details">Details</h2>
 | 
			
		||||
@ -325,7 +329,7 @@
 | 
			
		||||
 | 
			
		||||
    <br>
 | 
			
		||||
 | 
			
		||||
    <ng-container *ngIf="showFlow">
 | 
			
		||||
    <ng-container *ngIf="flowEnabled">
 | 
			
		||||
      <div class="title">
 | 
			
		||||
        <h2 i18n="transaction.flow|Transaction flow">Flow</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -7,34 +7,34 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title-block {
 | 
			
		||||
	flex-wrap: wrap;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  align-items: baseline;
 | 
			
		||||
  @media (min-width: 650px) {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
  h1 {
 | 
			
		||||
    margin: 0rem;
 | 
			
		||||
    margin-right: 15px;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.tx-link {
 | 
			
		||||
  display: flex;
 | 
			
		||||
	flex-grow: 1;
 | 
			
		||||
  margin-bottom: 0px;
 | 
			
		||||
  margin-top: 8px;
 | 
			
		||||
	@media (min-width: 650px) {
 | 
			
		||||
    align-self: end;
 | 
			
		||||
    margin-left: 15px;
 | 
			
		||||
    margin-top: 0px;
 | 
			
		||||
    margin-bottom: -3px;
 | 
			
		||||
	}
 | 
			
		||||
	@media (min-width: 768px) {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
  @media (min-width: 651px) {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    width: auto;
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    margin-bottom: 0px;
 | 
			
		||||
    top: 1px;
 | 
			
		||||
    position: relative;
 | 
			
		||||
	}
 | 
			
		||||
	@media (max-width: 768px) {
 | 
			
		||||
	  order: 3;
 | 
			
		||||
	}
 | 
			
		||||
  }
 | 
			
		||||
  @media (max-width: 650px) {
 | 
			
		||||
    order: 3;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.td-width {
 | 
			
		||||
 | 
			
		||||
@ -47,13 +47,17 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
  now = new Date().getTime();
 | 
			
		||||
  timeAvg$: Observable<number>;
 | 
			
		||||
  liquidUnblinding = new LiquidUnblinding();
 | 
			
		||||
  inputIndex: number;
 | 
			
		||||
  outputIndex: number;
 | 
			
		||||
  showFlow: boolean = true;
 | 
			
		||||
  graphExpanded: boolean = false;
 | 
			
		||||
  graphWidth: number = 1000;
 | 
			
		||||
  graphHeight: number = 360;
 | 
			
		||||
  inOutLimit: number = 150;
 | 
			
		||||
  maxInOut: number = 0;
 | 
			
		||||
  flowPrefSubscription: Subscription;
 | 
			
		||||
  hideFlow: boolean = this.stateService.hideFlow.value;
 | 
			
		||||
  overrideFlowPreference: boolean = null;
 | 
			
		||||
  flowEnabled: boolean;
 | 
			
		||||
 | 
			
		||||
  tooltipPosition: { x: number, y: number };
 | 
			
		||||
 | 
			
		||||
@ -77,6 +81,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
      (network) => (this.network = network)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.setFlowEnabled();
 | 
			
		||||
    this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
 | 
			
		||||
      this.hideFlow = !!hide;
 | 
			
		||||
      this.setFlowEnabled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.timeAvg$ = timer(0, 1000)
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap(() => this.stateService.difficultyAdjustment$),
 | 
			
		||||
@ -121,8 +131,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap((params: ParamMap) => {
 | 
			
		||||
          const urlMatch = (params.get('id') || '').split(':');
 | 
			
		||||
          this.txId = urlMatch[0];
 | 
			
		||||
          this.outputIndex = urlMatch[1] === undefined ? null : parseInt(urlMatch[1], 10);
 | 
			
		||||
          if (urlMatch.length === 2 && urlMatch[1].length === 64) {
 | 
			
		||||
            this.inputIndex = parseInt(urlMatch[0], 10);
 | 
			
		||||
            this.outputIndex = null;
 | 
			
		||||
            this.txId = urlMatch[1];
 | 
			
		||||
          } else {
 | 
			
		||||
            this.txId = urlMatch[0];
 | 
			
		||||
            this.outputIndex = urlMatch[1] === undefined ? null : parseInt(urlMatch[1], 10);
 | 
			
		||||
            this.inputIndex = null;
 | 
			
		||||
          }
 | 
			
		||||
          this.seoService.setTitle(
 | 
			
		||||
            $localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
 | 
			
		||||
          );
 | 
			
		||||
@ -237,11 +254,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
 | 
			
		||||
      if (params.showFlow === 'false') {
 | 
			
		||||
        this.showFlow = false;
 | 
			
		||||
        this.overrideFlowPreference = false;
 | 
			
		||||
      } else if (params.showFlow === 'true') {
 | 
			
		||||
        this.overrideFlowPreference = true;
 | 
			
		||||
      } else {
 | 
			
		||||
        this.showFlow = true;
 | 
			
		||||
        this.setGraphSize();
 | 
			
		||||
        this.overrideFlowPreference = null;
 | 
			
		||||
      }
 | 
			
		||||
      this.setFlowEnabled();
 | 
			
		||||
      this.setGraphSize();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -317,15 +337,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleGraph() {
 | 
			
		||||
    this.showFlow = !this.showFlow;
 | 
			
		||||
    const showFlow = !this.flowEnabled;
 | 
			
		||||
    this.stateService.hideFlow.next(!showFlow);
 | 
			
		||||
    this.router.navigate([], {
 | 
			
		||||
      relativeTo: this.route,
 | 
			
		||||
      queryParams: { showFlow: this.showFlow },
 | 
			
		||||
      queryParams: { showFlow: showFlow },
 | 
			
		||||
      queryParamsHandling: 'merge',
 | 
			
		||||
      fragment: 'flow'
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setFlowEnabled() {
 | 
			
		||||
    this.flowEnabled = (this.overrideFlowPreference != null ? this.overrideFlowPreference : !this.hideFlow);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  expandGraph() {
 | 
			
		||||
    this.graphExpanded = true;
 | 
			
		||||
  }
 | 
			
		||||
@ -334,6 +359,16 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
    this.graphExpanded = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  selectInput(input) {
 | 
			
		||||
    this.inputIndex = input;
 | 
			
		||||
    this.outputIndex = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  selectOutput(output) {
 | 
			
		||||
    this.outputIndex = output;
 | 
			
		||||
    this.inputIndex = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @HostListener('window:resize', ['$event'])
 | 
			
		||||
  setGraphSize(): void {
 | 
			
		||||
    if (this.graphContainer) {
 | 
			
		||||
@ -347,6 +382,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
    this.txReplacedSubscription.unsubscribe();
 | 
			
		||||
    this.blocksSubscription.unsubscribe();
 | 
			
		||||
    this.queryParamsSubscription.unsubscribe();
 | 
			
		||||
    this.flowPrefSubscription.unsubscribe();
 | 
			
		||||
    this.leaveTransaction();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -20,9 +20,9 @@
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <table class="table table-borderless smaller-text table-sm table-tx-vin">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > rowLimit) ? tx.vin.slice(0, rowLimit - 2) : tx.vin.slice(0, rowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
 | 
			
		||||
            <ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > inputRowLimit) ? tx.vin.slice(0, inputRowLimit - 2) : tx.vin.slice(0, inputRowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
 | 
			
		||||
              <tr [ngClass]="{
 | 
			
		||||
                'assetBox': assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded,
 | 
			
		||||
                'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
 | 
			
		||||
                'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
 | 
			
		||||
              }">
 | 
			
		||||
                <td class="arrow-td">
 | 
			
		||||
@ -146,7 +146,7 @@
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
            <tr *ngIf="tx.vin.length > rowLimit && tx['@vinLimit']">
 | 
			
		||||
            <tr *ngIf="tx.vin.length > inputRowLimit && tx['@vinLimit']">
 | 
			
		||||
              <td colspan="3" class="text-center">
 | 
			
		||||
                <button class="btn btn-sm btn-primary mt-2" (click)="loadMoreInputs(tx);"><span i18n="show-all">Show all</span> ({{ tx.vin.length }})</button>
 | 
			
		||||
              </td>
 | 
			
		||||
@ -158,7 +158,7 @@
 | 
			
		||||
      <div class="col mobile-bottomcol">
 | 
			
		||||
        <table class="table table-borderless smaller-text table-sm table-tx-vout">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] && !outputIndex ? ((tx.vout.length > rowLimit) ? tx.vout.slice(0, rowLimit - 2) : tx.vout.slice(0, rowLimit)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
 | 
			
		||||
            <ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] ? ((tx.vout.length > outputRowLimit) ? tx.vout.slice(0, outputRowLimit - 2) : tx.vout.slice(0, outputRowLimit)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
 | 
			
		||||
              <tr [ngClass]="{
 | 
			
		||||
                'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
 | 
			
		||||
                'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
 | 
			
		||||
@ -220,7 +220,7 @@
 | 
			
		||||
                      <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <ng-template #spent>
 | 
			
		||||
                      <a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].txid]" class="red">
 | 
			
		||||
                      <a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].vin + ':' + tx._outspends[vindex].txid]" class="red">
 | 
			
		||||
                        <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
                      </a>
 | 
			
		||||
                      <ng-template #outputNoTxId>
 | 
			
		||||
@ -257,7 +257,7 @@
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
            <tr *ngIf="tx.vout.length > rowLimit && tx['@voutLimit'] && !outputIndex">
 | 
			
		||||
            <tr *ngIf="tx.vout.length > outputRowLimit && tx['@voutLimit']">
 | 
			
		||||
              <td colspan="3" class="text-center">
 | 
			
		||||
                <button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vout.length }})</button>
 | 
			
		||||
              </td>
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() transactionPage = false;
 | 
			
		||||
  @Input() errorUnblinded = false;
 | 
			
		||||
  @Input() paginated = false;
 | 
			
		||||
  @Input() inputIndex: number;
 | 
			
		||||
  @Input() outputIndex: number;
 | 
			
		||||
  @Input() address: string = '';
 | 
			
		||||
  @Input() rowLimit = 12;
 | 
			
		||||
@ -37,6 +38,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
  showDetails$ = new BehaviorSubject<boolean>(false);
 | 
			
		||||
  assetsMinimal: any;
 | 
			
		||||
  transactionsLength: number = 0;
 | 
			
		||||
  inputRowLimit: number = 12;
 | 
			
		||||
  outputRowLimit: number = 12;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
@ -97,50 +100,57 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
    ).subscribe(() => this.ref.markForCheck());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(): void {
 | 
			
		||||
    if (!this.transactions || !this.transactions.length) {
 | 
			
		||||
      return;
 | 
			
		||||
  ngOnChanges(changes): void {
 | 
			
		||||
    if (changes.inputIndex || changes.outputIndex || changes.rowLimit) {
 | 
			
		||||
      this.inputRowLimit = Math.max(this.rowLimit, (this.inputIndex || 0) + 3);
 | 
			
		||||
      this.outputRowLimit = Math.max(this.rowLimit, (this.outputIndex || 0) + 3);
 | 
			
		||||
      if ((this.inputIndex || this.outputIndex) && !changes.transactions) {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          const assetBoxElements = document.getElementsByClassName('assetBox');
 | 
			
		||||
          if (assetBoxElements && assetBoxElements[0]) {
 | 
			
		||||
            assetBoxElements[0].scrollIntoView({block: "center"});
 | 
			
		||||
          }
 | 
			
		||||
        }, 10);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.transactionsLength = this.transactions.length;
 | 
			
		||||
    if (this.outputIndex) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        const assetBoxElements = document.getElementsByClassName('assetBox');
 | 
			
		||||
        if (assetBoxElements && assetBoxElements[0]) {
 | 
			
		||||
          assetBoxElements[0].scrollIntoView();
 | 
			
		||||
        }
 | 
			
		||||
      }, 10);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.transactions.forEach((tx) => {
 | 
			
		||||
      tx['@voutLimit'] = true;
 | 
			
		||||
      tx['@vinLimit'] = true;
 | 
			
		||||
      if (tx['addressValue'] !== undefined) {
 | 
			
		||||
    if (changes.transactions || changes.address) {
 | 
			
		||||
      if (!this.transactions || !this.transactions.length) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this.address) {
 | 
			
		||||
        const addressIn = tx.vout
 | 
			
		||||
          .filter((v: Vout) => v.scriptpubkey_address === this.address)
 | 
			
		||||
          .map((v: Vout) => v.value || 0)
 | 
			
		||||
          .reduce((a: number, b: number) => a + b, 0);
 | 
			
		||||
      this.transactionsLength = this.transactions.length;
 | 
			
		||||
 | 
			
		||||
        const addressOut = tx.vin
 | 
			
		||||
          .filter((v: Vin) => v.prevout && v.prevout.scriptpubkey_address === this.address)
 | 
			
		||||
          .map((v: Vin) => v.prevout.value || 0)
 | 
			
		||||
          .reduce((a: number, b: number) => a + b, 0);
 | 
			
		||||
 | 
			
		||||
        tx['addressValue'] = addressIn - addressOut;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
 | 
			
		||||
    if (txIds.length) {
 | 
			
		||||
      this.refreshOutspends$.next(txIds);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.stateService.env.LIGHTNING) {
 | 
			
		||||
      const txIds = this.transactions.filter((tx) => !tx._channels).map((tx) => tx.txid);
 | 
			
		||||
      this.transactions.forEach((tx) => {
 | 
			
		||||
        tx['@voutLimit'] = true;
 | 
			
		||||
        tx['@vinLimit'] = true;
 | 
			
		||||
        if (tx['addressValue'] !== undefined) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.address) {
 | 
			
		||||
          const addressIn = tx.vout
 | 
			
		||||
            .filter((v: Vout) => v.scriptpubkey_address === this.address)
 | 
			
		||||
            .map((v: Vout) => v.value || 0)
 | 
			
		||||
            .reduce((a: number, b: number) => a + b, 0);
 | 
			
		||||
 | 
			
		||||
          const addressOut = tx.vin
 | 
			
		||||
            .filter((v: Vin) => v.prevout && v.prevout.scriptpubkey_address === this.address)
 | 
			
		||||
            .map((v: Vin) => v.prevout.value || 0)
 | 
			
		||||
            .reduce((a: number, b: number) => a + b, 0);
 | 
			
		||||
 | 
			
		||||
          tx['addressValue'] = addressIn - addressOut;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
 | 
			
		||||
      if (txIds.length) {
 | 
			
		||||
        this.refreshChannels$.next(txIds);
 | 
			
		||||
        this.refreshOutspends$.next(txIds);
 | 
			
		||||
      }
 | 
			
		||||
      if (this.stateService.env.LIGHTNING) {
 | 
			
		||||
        const txIds = this.transactions.filter((tx) => !tx._channels).map((tx) => tx.txid);
 | 
			
		||||
        if (txIds.length) {
 | 
			
		||||
          this.refreshChannels$.next(txIds);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -42,9 +42,9 @@
 | 
			
		||||
        <ng-container [ngSwitch]="line.type">
 | 
			
		||||
          <span *ngSwitchCase="'input'" i18n="transaction.input">Input</span>
 | 
			
		||||
          <span *ngSwitchCase="'output'" i18n="transaction.output">Output</span>
 | 
			
		||||
          <span *ngSwitchCase="'fee'" i18n="transaction.fee">Fee</span>
 | 
			
		||||
          <span *ngSwitchCase="'fee'" i18n="transaction.fee|Transaction fee">Fee</span>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
        <span *ngIf="line.type !== 'fee'"> #{{ line.index }}</span>
 | 
			
		||||
        <span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span>
 | 
			
		||||
      </p>
 | 
			
		||||
      <p *ngIf="line.value == null && line.confidential" i18n="shared.confidential">Confidential</p>
 | 
			
		||||
      <p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
 | 
			
		||||
 | 
			
		||||
@ -41,6 +41,18 @@
 | 
			
		||||
        <stop offset="98%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
        <stop offset="100%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
      </linearGradient>
 | 
			
		||||
      <linearGradient id="input-highlight-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
      <stop offset="0%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
      <stop offset="2%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
        <stop offset="30%" stop-color="#1bd8f4" />
 | 
			
		||||
        <stop offset="100%" [attr.stop-color]="gradient[1]" />
 | 
			
		||||
      </linearGradient>
 | 
			
		||||
      <linearGradient id="output-highlight-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
        <stop offset="0%" [attr.stop-color]="gradient[1]" />
 | 
			
		||||
        <stop offset="70%" stop-color="#1bd8f4" />
 | 
			
		||||
        <stop offset="98%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
        <stop offset="100%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
      </linearGradient>
 | 
			
		||||
      <linearGradient id="fee-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
        <stop offset="0%" [attr.stop-color]="gradient[1]" />
 | 
			
		||||
        <stop offset="100%" stop-color="white" />
 | 
			
		||||
@ -56,20 +68,24 @@
 | 
			
		||||
      <path
 | 
			
		||||
        [attr.d]="input.path"
 | 
			
		||||
        class="line {{input.class}}"
 | 
			
		||||
        [class.highlight]="inputIndex != null && inputData[i].index === inputIndex"
 | 
			
		||||
        [style]="input.style"
 | 
			
		||||
        attr.marker-start="url(#{{input.class}}-arrow)"
 | 
			
		||||
        (pointerover)="onHover($event, 'input', i);"
 | 
			
		||||
        (pointerout)="onBlur($event, 'input', i);"
 | 
			
		||||
        (click)="onClick($event, 'input', inputData[i].index);"
 | 
			
		||||
      />
 | 
			
		||||
    </ng-container>
 | 
			
		||||
    <ng-container *ngFor="let output of outputs; let i = index">
 | 
			
		||||
      <path
 | 
			
		||||
        [attr.d]="output.path"
 | 
			
		||||
        class="line {{output.class}}"
 | 
			
		||||
        [class.highlight]="outputIndex != null && outputData[i].index === outputIndex"
 | 
			
		||||
        [style]="output.style"
 | 
			
		||||
        attr.marker-start="url(#{{output.class}}-arrow)"
 | 
			
		||||
        (pointerover)="onHover($event, 'output', i);"
 | 
			
		||||
        (pointerout)="onBlur($event, 'output', i);"
 | 
			
		||||
        (click)="onClick($event, 'output', outputData[i].index);"
 | 
			
		||||
      />
 | 
			
		||||
    </ng-container>
 | 
			
		||||
  </svg>
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,17 @@
 | 
			
		||||
      stroke: url(#fee-gradient);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.highlight {
 | 
			
		||||
      z-index: 8;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      &.input {
 | 
			
		||||
        stroke: url(#input-highlight-gradient);
 | 
			
		||||
      }
 | 
			
		||||
      &.output {
 | 
			
		||||
        stroke: url(#output-highlight-gradient);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      z-index: 10;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,11 @@
 | 
			
		||||
import { Component, OnInit, Input, OnChanges, HostListener } from '@angular/core';
 | 
			
		||||
import { Transaction } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, HostListener } from '@angular/core';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Outspend, Transaction } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { ReplaySubject, merge, Subscription } from 'rxjs';
 | 
			
		||||
import { tap, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
 | 
			
		||||
interface SvgLine {
 | 
			
		||||
  path: string;
 | 
			
		||||
@ -34,6 +40,11 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() minWeight = 2; //
 | 
			
		||||
  @Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen.
 | 
			
		||||
  @Input() tooltip = false;
 | 
			
		||||
  @Input() inputIndex: number;
 | 
			
		||||
  @Input() outputIndex: number;
 | 
			
		||||
 | 
			
		||||
  @Output() selectInput = new EventEmitter<number>();
 | 
			
		||||
  @Output() selectOutput = new EventEmitter<number>();
 | 
			
		||||
 | 
			
		||||
  inputData: Xput[];
 | 
			
		||||
  outputData: Xput[];
 | 
			
		||||
@ -45,6 +56,10 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  isLiquid: boolean = false;
 | 
			
		||||
  hoverLine: Xput | void = null;
 | 
			
		||||
  tooltipPosition = { x: 0, y: 0 };
 | 
			
		||||
  outspends: Outspend[] = [];
 | 
			
		||||
 | 
			
		||||
  outspendsSubscription: Subscription;
 | 
			
		||||
  refreshOutspends$: ReplaySubject<string> = new ReplaySubject();
 | 
			
		||||
 | 
			
		||||
  gradientColors = {
 | 
			
		||||
    '': ['#9339f4', '#105fb0'],
 | 
			
		||||
@ -61,12 +76,45 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
 | 
			
		||||
  gradient: string[] = ['#105fb0', '#105fb0'];
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private relativeUrlPipe: RelativeUrlPipe,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.initGraph();
 | 
			
		||||
 | 
			
		||||
    this.outspendsSubscription = merge(
 | 
			
		||||
      this.refreshOutspends$
 | 
			
		||||
        .pipe(
 | 
			
		||||
          switchMap((txid) => this.apiService.getOutspendsBatched$([txid])),
 | 
			
		||||
          tap((outspends: Outspend[][]) => {
 | 
			
		||||
            if (!this.tx || !outspends || !outspends.length) {
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
            this.outspends = outspends[0];
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
      this.stateService.utxoSpent$
 | 
			
		||||
        .pipe(
 | 
			
		||||
          tap((utxoSpent) => {
 | 
			
		||||
            for (const i in utxoSpent) {
 | 
			
		||||
              this.outspends[i] = {
 | 
			
		||||
                spent: true,
 | 
			
		||||
                txid: utxoSpent[i].txid,
 | 
			
		||||
                vin: utxoSpent[i].vin,
 | 
			
		||||
              };
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
    ).subscribe(() => {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(): void {
 | 
			
		||||
    this.initGraph();
 | 
			
		||||
    this.refreshOutspends$.next(this.tx.txid);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initGraph(): void {
 | 
			
		||||
@ -76,11 +124,12 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
    this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.width - (2 * this.midWidth)) / 6));
 | 
			
		||||
 | 
			
		||||
    const totalValue = this.calcTotalValue(this.tx);
 | 
			
		||||
    let voutWithFee = this.tx.vout.map(v => {
 | 
			
		||||
    let voutWithFee = this.tx.vout.map((v, i) => {
 | 
			
		||||
      return {
 | 
			
		||||
        type: v.scriptpubkey_type === 'fee' ? 'fee' : 'output',
 | 
			
		||||
        value: v?.value,
 | 
			
		||||
        address: v?.scriptpubkey_address || v?.scriptpubkey_type?.toUpperCase(),
 | 
			
		||||
        index: i,
 | 
			
		||||
        pegout: v?.pegout?.scriptpubkey_address,
 | 
			
		||||
        confidential: (this.isLiquid && v?.value === undefined),
 | 
			
		||||
      } as Xput;
 | 
			
		||||
@ -91,11 +140,12 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
    }
 | 
			
		||||
    const outputCount = voutWithFee.length;
 | 
			
		||||
 | 
			
		||||
    let truncatedInputs = this.tx.vin.map(v => {
 | 
			
		||||
    let truncatedInputs = this.tx.vin.map((v, i) => {
 | 
			
		||||
      return {
 | 
			
		||||
        type: 'input',
 | 
			
		||||
        value: v?.prevout?.value,
 | 
			
		||||
        address: v?.prevout?.scriptpubkey_address || v?.prevout?.scriptpubkey_type?.toUpperCase(),
 | 
			
		||||
        index: i,
 | 
			
		||||
        coinbase: v?.is_coinbase,
 | 
			
		||||
        pegin: v?.is_pegin,
 | 
			
		||||
        confidential: (this.isLiquid && v?.prevout?.value === undefined),
 | 
			
		||||
@ -306,8 +356,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
      };
 | 
			
		||||
    } else {
 | 
			
		||||
      this.hoverLine = {
 | 
			
		||||
        ...this.outputData[index],
 | 
			
		||||
        index
 | 
			
		||||
        ...this.outputData[index]
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -315,4 +364,29 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  onBlur(event, side, index): void {
 | 
			
		||||
    this.hoverLine = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onClick(event, side, index): void {
 | 
			
		||||
    if (side === 'input') {
 | 
			
		||||
      const input = this.tx.vin[index];
 | 
			
		||||
      if (input && input.txid && input.vout != null) {
 | 
			
		||||
        this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid + ':' + input.vout], {
 | 
			
		||||
          queryParamsHandling: 'merge',
 | 
			
		||||
          fragment: 'flow'
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        this.selectInput.emit(index);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      const output = this.tx.vout[index];
 | 
			
		||||
      const outspend = this.outspends[index];
 | 
			
		||||
      if (output && outspend && outspend.spent && outspend.txid) {
 | 
			
		||||
        this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.vin + ':' + outspend.txid], {
 | 
			
		||||
          queryParamsHandling: 'merge',
 | 
			
		||||
          fragment: 'flow'
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        this.selectOutput.emit(index);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -123,7 +123,7 @@
 | 
			
		||||
              <th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
 | 
			
		||||
              <th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
 | 
			
		||||
              <th class="table-cell-fiat" *ngIf="(network$ | async) === ''" i18n="dashboard.latest-transactions.USD">USD</th>
 | 
			
		||||
              <th class="table-cell-fees" i18n="dashboard.latest-transactions.fee">Fee</th>
 | 
			
		||||
              <th class="table-cell-fees" i18n="transaction.fee|Transaction fee">Fee</th>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr *ngFor="let transaction of transactions$ | async; let i = index;">
 | 
			
		||||
 | 
			
		||||
@ -39,6 +39,7 @@ if (browserWindowEnv.BASE_MODULE && (browserWindowEnv.BASE_MODULE === 'bisq' ||
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'faq',
 | 
			
		||||
      data: { networks: ['bitcoin'] },
 | 
			
		||||
      component: DocsComponent
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -37,10 +37,12 @@ const routes: Routes = [
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mining/pool/:slug',
 | 
			
		||||
        data: { networks: ['bitcoin'] },
 | 
			
		||||
        component: PoolComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mining',
 | 
			
		||||
        data: { networks: ['bitcoin'] },
 | 
			
		||||
        component: StartComponent,
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
@ -51,6 +53,7 @@ const routes: Routes = [
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mempool-block/:id',
 | 
			
		||||
        data: { networks: ['bitcoin', 'liquid'] },
 | 
			
		||||
        component: StartComponent,
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
@ -61,62 +64,77 @@ const routes: Routes = [
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'graphs',
 | 
			
		||||
        data: { networks: ['bitcoin', 'liquid'] },
 | 
			
		||||
        component: GraphsComponent,
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mempool',
 | 
			
		||||
            data: { networks: ['bitcoin', 'liquid'] },
 | 
			
		||||
            component: StatisticsComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/hashrate-difficulty',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: HashrateChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/pools-dominance',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: HashrateChartPoolsComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/pools',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: PoolRankingComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/block-fees',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: BlockFeesGraphComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/block-rewards',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: BlockRewardsGraphComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/block-fee-rates',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: BlockFeeRatesGraphComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/block-sizes-weights',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: BlockSizesWeightsGraphComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'lightning/nodes-networks',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: NodesNetworksChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'lightning/capacity',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: LightningStatisticsChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'lightning/nodes-per-isp',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: NodesPerISPChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'lightning/nodes-per-country',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: NodesPerCountryChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'lightning/nodes-map',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: NodesMap,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'lightning/nodes-channels-map',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: NodesChannelsMap,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
@ -125,6 +143,7 @@ const routes: Routes = [
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'mining/block-prediction',
 | 
			
		||||
            data: { networks: ['bitcoin'] },
 | 
			
		||||
            component: BlockPredictionGraphComponent,
 | 
			
		||||
          },
 | 
			
		||||
        ]
 | 
			
		||||
@ -141,6 +160,7 @@ const routes: Routes = [
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'tv',
 | 
			
		||||
    data: { networks: ['bitcoin', 'liquid'] },
 | 
			
		||||
    component: TelevisionComponent
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@
 | 
			
		||||
    <app-clipboard [text]="channel.public_key"></app-clipboard>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="box-right">
 | 
			
		||||
    <div class="second-line">{{ channel.channels }} channels</div>
 | 
			
		||||
    <div class="second-line"><ng-container *ngTemplateOutlet="xChannels; context: {$implicit: channel.channels }"></ng-container></div>
 | 
			
		||||
    <div class="second-line"><app-amount [satoshis]="channel.capacity" digitsInfo="1.2-2"></app-amount></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
  <table class="table table-borderless table-striped">
 | 
			
		||||
    <tbody>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td i18n="lightning.fee-rate">Fee rate</td>
 | 
			
		||||
        <td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
 | 
			
		||||
        <td>
 | 
			
		||||
          <span class="d-inline-block d-md-none">
 | 
			
		||||
            {{ channel.fee_rate !== null ? (channel.fee_rate | amountShortener : 2 : undefined : true) : '-' }} <span class="symbol">ppm {{ channel.fee_rate !== null ? '(' + (channel.fee_rate / 10000 | amountShortener : 2 : undefined : true) + '%)' : '' }}</span>
 | 
			
		||||
@ -33,19 +33,24 @@
 | 
			
		||||
            <span>
 | 
			
		||||
              <span *ngIf="channel.base_fee_mtokens !== null">
 | 
			
		||||
                {{ channel.base_fee_mtokens | amountShortener : 0 }}
 | 
			
		||||
                <span class="symbol">msats</span>
 | 
			
		||||
                <span class="symbol" i18n="shared.m-sats">mSats</span>
 | 
			
		||||
              </span>
 | 
			
		||||
              <span *ngIf="channel.base_fee_mtokens === null">
 | 
			
		||||
                -
 | 
			
		||||
              </span>
 | 
			
		||||
            </span>
 | 
			
		||||
            <span *ngIf="channel.base_fee_mtokens !== null" class="badge" [class]="channel.base_fee_mtokens === 0 ? 'badge-success' : 'badge-danger'"
 | 
			
		||||
              i18n-ngbTooltip="lightning.zero-base-fee"
 | 
			
		||||
              [ngbTooltip]="channel.base_fee_mtokens === 0 ? 'This channel supports zero base fee routing' :
 | 
			
		||||
                'This channel does not support zero base fee routing'"
 | 
			
		||||
              placement="bottom" i18n="lightning.zerobasefee">
 | 
			
		||||
              {{ channel.base_fee_mtokens === 0 ? 'Zero base fee' : 'Non-zero base fee' }}
 | 
			
		||||
            </span>
 | 
			
		||||
            <ng-template [ngIf]="channel.base_fee_mtokens !== null">
 | 
			
		||||
              <span class="badge badge-success" *ngIf="channel.base_fee_mtokens === 0; else nonZeroBaseFee"
 | 
			
		||||
                i18n-ngbTooltip="lightning.zero-base-fee-tooltip"
 | 
			
		||||
                ngbTooltip="This channel supports zero base fee routing"
 | 
			
		||||
                placement="bottom" i18n="lightning.zero-base-fee">Zero base fee</span>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
            <ng-template #nonZeroBaseFee>
 | 
			
		||||
              <span class="badge badge-danger"
 | 
			
		||||
                i18n-ngbTooltip="lightning.non-zero-base-fee-tooltip"
 | 
			
		||||
                ngbTooltip="This channel does not support zero base fee routing"
 | 
			
		||||
                placement="bottom" i18n="lightning.non-zero-base-fee">Non-zero base fee</span>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
          </span>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
@ -62,7 +67,7 @@
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td i18n="lightning.timelock-detla">Timelock delta</td>
 | 
			
		||||
        <td i18n="lightning.timelock-delta">Timelock delta</td>
 | 
			
		||||
        <td>
 | 
			
		||||
          <ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: channel.cltv_delta ?? '-' }"></ng-container>
 | 
			
		||||
        </td>
 | 
			
		||||
@ -72,3 +77,4 @@
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
 | 
			
		||||
<ng-template #xChannels let-i i18n="lightning.x-channels">{{ i }} channels</ng-template>
 | 
			
		||||
 | 
			
		||||
@ -7,9 +7,9 @@
 | 
			
		||||
      <h1 class="title">{{ channel.short_id }}</h1>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="badges mb-2">
 | 
			
		||||
      <span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
 | 
			
		||||
      <span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
 | 
			
		||||
      <span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
 | 
			
		||||
      <span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0" i18n="status.inactive">Inactive</span>
 | 
			
		||||
      <span class="badge rounded-pill badge-success" *ngIf="channel.status === 1" i18n="status.active">Active</span>
 | 
			
		||||
      <span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2" i18n="status.closed">Closed</span>
 | 
			
		||||
 | 
			
		||||
      <app-closing-type [type]="channel.closing_reason" *ngIf="channel.status === 2"></app-closing-type>
 | 
			
		||||
    </div>
 | 
			
		||||
@ -20,20 +20,20 @@
 | 
			
		||||
      <table class="table table-borderless table-striped">
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="channel.created">Created</td>
 | 
			
		||||
            <td i18n="lightning.created">Created</td>
 | 
			
		||||
            <td>{{ channel.created | date:'yyyy-MM-dd HH:mm' }}</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="channel.capacity">Capacity</td>
 | 
			
		||||
            <td i18n="lightning.capacity">Capacity</td>
 | 
			
		||||
            <td><app-amount [satoshis]="channel.capacity" [noFiat]="true"></app-amount></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="channel.fee-rate">Fee rate</td>
 | 
			
		||||
            <td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="dual-cell">
 | 
			
		||||
                <span>{{ channel.node_left.fee_rate }} <span class="symbol">ppm</span></span>
 | 
			
		||||
                <span>{{ channel.node_left.fee_rate }} <span class="symbol" i18n="lightning.ppm">ppm</span></span>
 | 
			
		||||
                <fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
                <span>{{ channel.node_right.fee_rate }} <span class="symbol">ppm</span></span>
 | 
			
		||||
                <span>{{ channel.node_right.fee_rate }} <span class="symbol" i18n="lightning.ppm">ppm</span></span>
 | 
			
		||||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
 | 
			
		||||
@ -8,9 +8,9 @@
 | 
			
		||||
    </span>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="badges mb-2">
 | 
			
		||||
    <span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
 | 
			
		||||
    <span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
 | 
			
		||||
    <span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
 | 
			
		||||
    <span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0" i18n="status.inactive">Inactive</span>
 | 
			
		||||
    <span class="badge rounded-pill badge-success" *ngIf="channel.status === 1" i18n="status.active">Active</span>
 | 
			
		||||
    <span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2" i18n="status.closed">Closed</span>
 | 
			
		||||
    <app-closing-type *ngIf="channel.closing_reason" [type]="channel.closing_reason"></app-closing-type>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,7 @@
 | 
			
		||||
          <table class="table table-borderless table-striped">
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="address.total-received">Capacity</td>
 | 
			
		||||
                <td i18n="lightning.capacity">Capacity</td>
 | 
			
		||||
                <td><app-sats [satoshis]="channel.capacity"></app-sats><app-fiat [value]="channel.capacity" digitsInfo="1.0-0"></app-fiat></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
@ -70,7 +70,7 @@
 | 
			
		||||
  <ng-container *ngIf="transactions$ | async as transactions">
 | 
			
		||||
    <ng-template [ngIf]="transactions[0]">
 | 
			
		||||
      <div class="d-flex">
 | 
			
		||||
        <h3>Opening transaction</h3>
 | 
			
		||||
        <h3 i18n="lightning.opening-transaction">Opening transaction</h3>
 | 
			
		||||
        <button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()"
 | 
			
		||||
          i18n="transaction.details|Transaction Details">Details</button>
 | 
			
		||||
      </div>
 | 
			
		||||
@ -79,7 +79,7 @@
 | 
			
		||||
    </ng-template>
 | 
			
		||||
    <ng-template [ngIf]="transactions[1]">
 | 
			
		||||
      <div class="closing-header d-flex">
 | 
			
		||||
        <h3 style="margin: 0;">Closing transaction</h3>  <app-closing-type [type]="channel.closing_reason">
 | 
			
		||||
        <h3 style="margin: 0;" i18n="lightning.closing-transaction">Closing transaction</h3>  <app-closing-type [type]="channel.closing_reason">
 | 
			
		||||
        </app-closing-type>
 | 
			
		||||
        <button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()"
 | 
			
		||||
          i18n="transaction.details|Transaction Details">Details</button>
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ export class ChannelComponent implements OnInit {
 | 
			
		||||
          return this.lightningApiService.getChannel$(params.get('short_id'))
 | 
			
		||||
            .pipe(
 | 
			
		||||
              tap((value) => {
 | 
			
		||||
                this.seoService.setTitle(`Channel: ${value.short_id}`);
 | 
			
		||||
                this.seoService.setTitle($localize`Channel: ${value.short_id}`);
 | 
			
		||||
              }),
 | 
			
		||||
              catchError((err) => {
 | 
			
		||||
                this.error = err;
 | 
			
		||||
 | 
			
		||||
@ -2,10 +2,10 @@
 | 
			
		||||
  <form [formGroup]="channelStatusForm" class="formRadioGroup">
 | 
			
		||||
    <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="status">
 | 
			
		||||
      <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
        <input ngbButton type="radio" [value]="'open'" fragment="open" i18n="open">Open
 | 
			
		||||
        <input ngbButton type="radio" [value]="'open'" fragment="open"><span i18n="open">Open</span>
 | 
			
		||||
      </label>
 | 
			
		||||
      <label ngbButtonLabel class="btn-primary btn-sm">
 | 
			
		||||
        <input ngbButton type="radio" [value]="'closed'" fragment="closed" i18n="closed">Closed
 | 
			
		||||
        <input ngbButton type="radio" [value]="'closed'" fragment="closed"><span i18n="closed">Closed</span>
 | 
			
		||||
      </label>
 | 
			
		||||
    </div>
 | 
			
		||||
  </form>
 | 
			
		||||
@ -32,12 +32,12 @@
 | 
			
		||||
  
 | 
			
		||||
<ng-template #tableHeader>
 | 
			
		||||
  <thead>
 | 
			
		||||
    <th class="alias text-left" i18n="nodes.alias">Node Alias</th>
 | 
			
		||||
    <th class="alias text-left d-none d-md-table-cell" i18n="channels.transaction"> </th>
 | 
			
		||||
    <th class="alias text-left" i18n="lightning.alias">Alias</th>
 | 
			
		||||
    <th class="alias text-left d-none d-md-table-cell"> </th>
 | 
			
		||||
    <th class="alias text-left d-none d-md-table-cell" i18n="status">Status</th>
 | 
			
		||||
    <th *ngIf="status !== 'closed'" class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th>
 | 
			
		||||
    <th *ngIf="status !== 'closed'" class="channels text-left d-none d-md-table-cell" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
 | 
			
		||||
    <th *ngIf="status === 'closed'" class="channels text-left d-none d-md-table-cell" i18n="channels.closing_date">Closing date</th>
 | 
			
		||||
    <th class="capacity text-right d-none d-md-table-cell" i18n="nodes.capacity">Capacity</th>
 | 
			
		||||
    <th class="capacity text-right d-none d-md-table-cell" i18n="lightning.capacity">Capacity</th>
 | 
			
		||||
    <th class="capacity text-right" i18n="channels.id">Channel ID</th>
 | 
			
		||||
  </thead>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -53,7 +53,7 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </td>
 | 
			
		||||
  <td class="alias text-left d-none d-md-table-cell">
 | 
			
		||||
    <div class="second-line">{{ node.channels }} channels</div>
 | 
			
		||||
    <div class="second-line"><ng-container *ngTemplateOutlet="xChannels; context: {$implicit: node.channels }"></ng-container></div>
 | 
			
		||||
    <div class="second-line">
 | 
			
		||||
      <app-amount *ngIf="node.capacity > 100000000; else smallnode" [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
 | 
			
		||||
      <ng-template #smallnode>
 | 
			
		||||
@ -63,10 +63,10 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </td>
 | 
			
		||||
  <td class="d-none d-md-table-cell">
 | 
			
		||||
    <span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0" i18n="lightning.inactive">Inactive</span>
 | 
			
		||||
    <span class="badge rounded-pill badge-success" *ngIf="channel.status === 1" i18n="lightning.active">Active</span>
 | 
			
		||||
    <span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0" i18n="status.inactive">Inactive</span>
 | 
			
		||||
    <span class="badge rounded-pill badge-success" *ngIf="channel.status === 1" i18n="status.active">Active</span>
 | 
			
		||||
    <ng-template [ngIf]="channel.status === 2">
 | 
			
		||||
      <span class="badge rounded-pill badge-secondary" *ngIf="!channel.closing_reason; else closingReason" i18n="lightning.closed">Closed</span>
 | 
			
		||||
      <span class="badge rounded-pill badge-secondary" *ngIf="!channel.closing_reason; else closingReason" i18n="status.closed">Closed</span>
 | 
			
		||||
      <ng-template #closingReason>
 | 
			
		||||
        <app-closing-type [type]="channel.closing_reason"></app-closing-type>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
@ -117,3 +117,5 @@
 | 
			
		||||
  </tbody>
 | 
			
		||||
</table>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #xChannels let-i i18n="lightning.x-channels">{{ i }} channels</ng-template>
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
<div class="widget-toggler">
 | 
			
		||||
  <a href="javascript:;" (click)="switchMode('avg')" class="toggler-option"
 | 
			
		||||
    [ngClass]="{'inactive': mode !== 'avg'}"><small>avg</small></a>
 | 
			
		||||
  <a href="" (click)="switchMode('avg')" class="toggler-option"
 | 
			
		||||
    [ngClass]="{'inactive': mode === 'avg'}"><small>avg</small></a>
 | 
			
		||||
  <span style="color: #ffffff66; font-size: 8px"> | </span>
 | 
			
		||||
  <a href="javascript:;" (click)="switchMode('med')" class="toggler-option"
 | 
			
		||||
    [ngClass]="{'inactive': mode !== 'med'}"><small>med</small></a>
 | 
			
		||||
  <a href="" (click)="switchMode('med')" class="toggler-option"
 | 
			
		||||
    [ngClass]="{'inactive': mode === 'med'}"><small>med</small></a>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward">
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
 | 
			
		||||
          {{ statistics.latest?.avg_capacity || 0 | number: '1.0-0' }}
 | 
			
		||||
          <span i18n="shared.sat-vbyte|sat/vB">sats</span>
 | 
			
		||||
          <span i18n="shared.sats">sats</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <span class="fiat" *ngIf="statistics.previous">
 | 
			
		||||
          <app-change [current]="statistics.latest?.avg_capacity" [previous]="statistics.previous?.avg_capacity"></app-change>
 | 
			
		||||
@ -29,7 +29,7 @@
 | 
			
		||||
        placement="bottom">
 | 
			
		||||
        <div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
 | 
			
		||||
          {{ statistics.latest?.avg_fee_rate || 0 | number: '1.0-0' }}
 | 
			
		||||
          <span i18n="shared.sat-vbyte|sat/vB">ppm</span>
 | 
			
		||||
          <span i18n="lightning.ppm">ppm</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <span class="fiat" *ngIf="statistics.previous">
 | 
			
		||||
          <app-change [current]="statistics.latest?.avg_fee_rate" [previous]="statistics.previous?.avg_fee_rate"></app-change>
 | 
			
		||||
@ -44,7 +44,7 @@
 | 
			
		||||
        <div class="card-text">
 | 
			
		||||
          <div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
 | 
			
		||||
            {{ statistics.latest?.avg_base_fee_mtokens || 0 | number: '1.0-0' }}
 | 
			
		||||
            <span i18n="shared.sat-vbyte|sat/vB">msats</span>
 | 
			
		||||
            <span i18n="shared.m-sats">mSats</span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <span class="fiat" *ngIf="statistics.previous">
 | 
			
		||||
            <app-change [current]="statistics.latest?.avg_base_fee_mtokens" [previous]="statistics.previous?.avg_base_fee_mtokens"></app-change>
 | 
			
		||||
@ -60,7 +60,7 @@
 | 
			
		||||
      <div class="card-text">
 | 
			
		||||
        <div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
 | 
			
		||||
          {{ statistics.latest?.med_capacity || 0 | number: '1.0-0' }}
 | 
			
		||||
          <span i18n="shared.sat-vbyte|sat/vB">sats</span>
 | 
			
		||||
          <span i18n="shared.sats">sats</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <span class="fiat" *ngIf="statistics.previous">
 | 
			
		||||
          <app-change [current]="statistics.latest?.med_capacity" [previous]="statistics.previous?.med_capacity"></app-change>
 | 
			
		||||
@ -75,7 +75,7 @@
 | 
			
		||||
        placement="bottom">
 | 
			
		||||
        <div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
 | 
			
		||||
          {{ statistics.latest?.med_fee_rate || 0 | number: '1.0-0' }}
 | 
			
		||||
          <span i18n="shared.sat-vbyte|sat/vB">ppm</span>
 | 
			
		||||
          <span i18n="lightning.ppm">ppm</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <span class="fiat" *ngIf="statistics.previous">
 | 
			
		||||
          <app-change [current]="statistics.latest?.med_fee_rate" [previous]="statistics.previous?.med_fee_rate"></app-change>
 | 
			
		||||
@ -90,7 +90,7 @@
 | 
			
		||||
        <div class="card-text">
 | 
			
		||||
          <div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
 | 
			
		||||
            {{ statistics.latest?.med_base_fee_mtokens || 0 | number: '1.0-0' }}
 | 
			
		||||
            <span i18n="shared.sat-vbyte|sat/vB">msats</span>
 | 
			
		||||
            <span i18n="shared.m-sats">mSats</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <span class="fiat" *ngIf="statistics.previous">
 | 
			
		||||
 | 
			
		||||
@ -18,5 +18,6 @@ export class ChannelsStatisticsComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  switchMode(mode: 'avg' | 'med') {
 | 
			
		||||
    this.mode = mode;
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<div class="box preview-box" *ngIf="nodes$ | async as nodes">
 | 
			
		||||
  <app-preview-title>
 | 
			
		||||
    <span i18n="lightning.node">Lightning node group</span>
 | 
			
		||||
    <span i18n="lightning.node-group">Lightning node group</span>
 | 
			
		||||
  </app-preview-title>
 | 
			
		||||
  <div class="row d-flex justify-content-between full-width-row">
 | 
			
		||||
    <div class="logo-wrapper">
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
<div class="container-xl full-height" style="min-height: 335px">
 | 
			
		||||
  <h5 class="mb-1" style="color: #ffffff66" i18n="lightning.node">Lightning node group</h5>
 | 
			
		||||
  <h5 class="mb-1" style="color: #ffffff66" i18n="lightning.node-group">Lightning node group</h5>
 | 
			
		||||
 | 
			
		||||
  <div class="header">
 | 
			
		||||
    <div class="logo-container">
 | 
			
		||||
 | 
			
		||||
@ -53,6 +53,10 @@ export class LightningApiService {
 | 
			
		||||
    return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getNodeFeeHistogram$(publicKey: string): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/fees/histogram');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getNodesRanking$(): Observable<INodesRanking> {
 | 
			
		||||
    return this.httpClient.get<INodesRanking>(this.apiBasePath + '/api/v1/lightning/nodes/rankings');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@
 | 
			
		||||
    <!-- Network capacity/channels/nodes -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="main-title">
 | 
			
		||||
        <span i18n="lightning.statistics-title">Network Statistics</span> 
 | 
			
		||||
        <span i18n="lightning.network-statistics-title">Network Statistics</span> 
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="card-wrapper">
 | 
			
		||||
        <div class="card" style="height: 123px">
 | 
			
		||||
@ -21,7 +21,7 @@
 | 
			
		||||
    <!-- Channels stats -->
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="main-title">
 | 
			
		||||
        <span i18n="lightning.statistics-title">Channels Statistics</span> 
 | 
			
		||||
        <span i18n="lightning.channel-statistics-title">Channels Statistics</span> 
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="card-wrapper">
 | 
			
		||||
        <div class="card" style="height: 123px">
 | 
			
		||||
@ -46,7 +46,7 @@
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <div class="card graph-card">
 | 
			
		||||
        <div class="card-body pl-2 pr-2 pt-1">
 | 
			
		||||
          <h5 class="card-title mt-3" i18n="lightning.network-history">Lightning network history</h5>
 | 
			
		||||
          <h5 class="card-title mt-3" i18n="lightning.network-history">Lightning Network History</h5>
 | 
			
		||||
          <app-lightning-statistics-chart [widget]=true></app-lightning-statistics-chart>
 | 
			
		||||
          <app-nodes-networks-chart [widget]=true></app-nodes-networks-chart>
 | 
			
		||||
          <div><a [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
 | 
			
		||||
@ -59,7 +59,7 @@
 | 
			
		||||
      <div class="card" style="height: 409px">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/liquidity' | relativeUrl]">
 | 
			
		||||
            <h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity ranking</h5>
 | 
			
		||||
            <h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
 | 
			
		||||
            <span> </span>
 | 
			
		||||
            <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
 | 
			
		||||
          </a>
 | 
			
		||||
@ -73,7 +73,7 @@
 | 
			
		||||
      <div class="card" style="height: 409px">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/connectivity' | relativeUrl]">
 | 
			
		||||
            <h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity ranking</h5>
 | 
			
		||||
            <h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>
 | 
			
		||||
            <span> </span>
 | 
			
		||||
            <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
 | 
			
		||||
          </a>
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@ export class LightningDashboardComponent implements OnInit {
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.seoService.setTitle($localize`Lightning Network`);
 | 
			
		||||
    this.seoService.setTitle($localize`:@@142e923d3b04186ac6ba23387265d22a2fa404e0:Lightning Explorer`);
 | 
			
		||||
 | 
			
		||||
    this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
 | 
			
		||||
    this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ import { ChannelBoxComponent } from './channel/channel-box/channel-box.component
 | 
			
		||||
import { ClosingTypeComponent } from './channel/closing-type/closing-type.component';
 | 
			
		||||
import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component';
 | 
			
		||||
import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component';
 | 
			
		||||
import { NodeFeeChartComponent } from './node-fee-chart/node-fee-chart.component';
 | 
			
		||||
import { GraphsModule } from '../graphs/graphs.module';
 | 
			
		||||
import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component';
 | 
			
		||||
import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component';
 | 
			
		||||
@ -38,6 +39,7 @@ import { GroupComponent } from './group/group.component';
 | 
			
		||||
    NodesListComponent,
 | 
			
		||||
    NodeStatisticsComponent,
 | 
			
		||||
    NodeStatisticsChartComponent,
 | 
			
		||||
    NodeFeeChartComponent,
 | 
			
		||||
    NodeComponent,
 | 
			
		||||
    ChannelsListComponent,
 | 
			
		||||
    ChannelComponent,
 | 
			
		||||
@ -73,6 +75,7 @@ import { GroupComponent } from './group/group.component';
 | 
			
		||||
    NodesListComponent,
 | 
			
		||||
    NodeStatisticsComponent,
 | 
			
		||||
    NodeStatisticsChartComponent,
 | 
			
		||||
    NodeFeeChartComponent,
 | 
			
		||||
    NodeComponent,
 | 
			
		||||
    ChannelsListComponent,
 | 
			
		||||
    ChannelComponent,
 | 
			
		||||
 | 
			
		||||
@ -21,10 +21,12 @@ const routes: Routes = [
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'node/:public_key',
 | 
			
		||||
          data: { networkSpecific: true },
 | 
			
		||||
          component: NodeComponent,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'channel/:short_id',
 | 
			
		||||
          data: { networkSpecific: true },
 | 
			
		||||
          component: ChannelComponent,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
<div class="full-container">
 | 
			
		||||
  <h2 i18n="lightning.node-fee-distribution">Fee distribution</h2>
 | 
			
		||||
  <div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
 | 
			
		||||
  <div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
    <div class="spinner-border text-light"></div>d
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,5 @@
 | 
			
		||||
.full-container {
 | 
			
		||||
  margin-top: 25px;
 | 
			
		||||
  margin-bottom: 25px;
 | 
			
		||||
  min-height: 100%;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,265 @@
 | 
			
		||||
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
 | 
			
		||||
import { EChartsOption } from 'echarts';
 | 
			
		||||
import { switchMap } from 'rxjs/operators';
 | 
			
		||||
import { download } from '../../shared/graphs.utils';
 | 
			
		||||
import { LightningApiService } from '../lightning-api.service';
 | 
			
		||||
import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
			
		||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-node-fee-chart',
 | 
			
		||||
  templateUrl: './node-fee-chart.component.html',
 | 
			
		||||
  styleUrls: ['./node-fee-chart.component.scss'],
 | 
			
		||||
  styles: [`
 | 
			
		||||
    .loadingGraphs {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 50%;
 | 
			
		||||
      left: calc(50% - 15px);
 | 
			
		||||
      z-index: 100;
 | 
			
		||||
    }
 | 
			
		||||
  `],
 | 
			
		||||
})
 | 
			
		||||
export class NodeFeeChartComponent implements OnInit {
 | 
			
		||||
  chartOptions: EChartsOption = {};
 | 
			
		||||
  chartInitOptions = {
 | 
			
		||||
    renderer: 'svg',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  @HostBinding('attr.dir') dir = 'ltr';
 | 
			
		||||
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  chartInstance: any = undefined;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string,
 | 
			
		||||
    private lightningApiService: LightningApiService,
 | 
			
		||||
    private activatedRoute: ActivatedRoute,
 | 
			
		||||
    private amountShortenerPipe: AmountShortenerPipe,
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
 | 
			
		||||
    this.activatedRoute.paramMap
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap((params: ParamMap) => {
 | 
			
		||||
          this.isLoading = true;
 | 
			
		||||
          return this.lightningApiService.getNodeFeeHistogram$(params.get('public_key'));
 | 
			
		||||
        }),
 | 
			
		||||
      ).subscribe((data) => {
 | 
			
		||||
        if (data && data.incoming && data.outgoing) {
 | 
			
		||||
          const outgoingHistogram = this.bucketsToHistogram(data.outgoing);
 | 
			
		||||
          const incomingHistogram = this.bucketsToHistogram(data.incoming);
 | 
			
		||||
          this.prepareChartOptions(outgoingHistogram, incomingHistogram);
 | 
			
		||||
        }
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bucketsToHistogram(buckets): { label: string, count: number, capacity: number}[] {
 | 
			
		||||
    const histogram = [];
 | 
			
		||||
    let increment = 1;
 | 
			
		||||
    let lower = -increment;
 | 
			
		||||
    let upper = 0;
 | 
			
		||||
 | 
			
		||||
    let nullBucket;
 | 
			
		||||
    if (buckets.length && buckets[0] && buckets[0].bucket == null) {
 | 
			
		||||
      nullBucket = buckets.shift();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    while (upper <= 5000) {
 | 
			
		||||
      let bucket;
 | 
			
		||||
      if (buckets.length && buckets[0] && upper >= Number(buckets[0].bucket)) {
 | 
			
		||||
        bucket = buckets.shift();
 | 
			
		||||
      }
 | 
			
		||||
      histogram.push({
 | 
			
		||||
        label: upper === 0 ? '0 ppm' : `${lower} - ${upper} ppm`,
 | 
			
		||||
        count: Number(bucket?.count || 0) + (upper === 0 ? Number(nullBucket?.count || 0) : 0),
 | 
			
		||||
        capacity: Number(bucket?.capacity || 0) + (upper === 0 ? Number(nullBucket?.capacity || 0) : 0),
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (upper >= increment * 10) {
 | 
			
		||||
        increment *= 10;
 | 
			
		||||
        lower = increment;
 | 
			
		||||
        upper = increment + increment;
 | 
			
		||||
      } else {
 | 
			
		||||
        lower += increment;
 | 
			
		||||
        upper += increment;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const rest = buckets.reduce((acc, bucket) => {
 | 
			
		||||
      acc.count += Number(bucket.count);
 | 
			
		||||
      acc.capacity += Number(bucket.capacity);
 | 
			
		||||
      return acc;
 | 
			
		||||
    }, { count: 0, capacity: 0 });
 | 
			
		||||
    histogram.push({
 | 
			
		||||
      label: `5000+ ppm`,
 | 
			
		||||
      count: rest.count,
 | 
			
		||||
      capacity: rest.capacity,
 | 
			
		||||
    });
 | 
			
		||||
    return histogram;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(outgoingData, incomingData): void {
 | 
			
		||||
    let title: object;
 | 
			
		||||
    if (outgoingData.length === 0) {
 | 
			
		||||
      title = {
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: 'grey',
 | 
			
		||||
          fontSize: 15
 | 
			
		||||
        },
 | 
			
		||||
        text: $localize`No data to display yet. Try again later.`,
 | 
			
		||||
        left: 'center',
 | 
			
		||||
        top: 'center'
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
      title: outgoingData.length === 0 ? title : undefined,
 | 
			
		||||
      animation: false,
 | 
			
		||||
      grid: {
 | 
			
		||||
        top: 30,
 | 
			
		||||
        bottom: 20,
 | 
			
		||||
        right: 20,
 | 
			
		||||
        left: 65,
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        show: !this.isMobile(),
 | 
			
		||||
        trigger: 'axis',
 | 
			
		||||
        axisPointer: {
 | 
			
		||||
          type: 'line'
 | 
			
		||||
        },
 | 
			
		||||
        backgroundColor: 'rgba(17, 19, 31, 1)',
 | 
			
		||||
        borderRadius: 4,
 | 
			
		||||
        shadowColor: 'rgba(0, 0, 0, 0.5)',
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: '#b1b1b1',
 | 
			
		||||
          align: 'left',
 | 
			
		||||
        },
 | 
			
		||||
        borderColor: '#000',
 | 
			
		||||
        formatter: (ticks): string => {
 | 
			
		||||
          return `
 | 
			
		||||
            <b style="color: white; margin-left: 2px">${ticks[0].data.label}</b><br>
 | 
			
		||||
            <br>
 | 
			
		||||
            <b style="color: white; margin-left: 2px">${ticks[0].marker} Outgoing</b><br>
 | 
			
		||||
            <span>Capacity: ${this.amountShortenerPipe.transform(ticks[0].data.capacity, 2, undefined, true)} sats</span><br>
 | 
			
		||||
            <span>Channels: ${ticks[0].data.count}</span><br>
 | 
			
		||||
            <br>
 | 
			
		||||
            <b style="color: white; margin-left: 2px">${ticks[1].marker} Incoming</b><br>
 | 
			
		||||
            <span>Capacity: ${this.amountShortenerPipe.transform(ticks[1].data.capacity, 2, undefined, true)} sats</span><br>
 | 
			
		||||
            <span>Channels: ${ticks[1].data.count}</span><br>
 | 
			
		||||
          `;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      xAxis: outgoingData.length === 0 ? undefined : {
 | 
			
		||||
        type: 'category',
 | 
			
		||||
        axisLine: { onZero: true },
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          align: 'center',
 | 
			
		||||
          fontSize: 11,
 | 
			
		||||
          lineHeight: 12,
 | 
			
		||||
          hideOverlap: true,
 | 
			
		||||
          padding: [0, 5],
 | 
			
		||||
        },
 | 
			
		||||
        data: outgoingData.map(bucket => bucket.label)
 | 
			
		||||
      },
 | 
			
		||||
      legend: outgoingData.length === 0 ? undefined : {
 | 
			
		||||
        padding: 10,
 | 
			
		||||
        data: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Outgoing Fees',
 | 
			
		||||
            inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              color: 'white',
 | 
			
		||||
            },
 | 
			
		||||
            icon: 'roundRect',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Incoming Fees',
 | 
			
		||||
            inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              color: 'white',
 | 
			
		||||
            },
 | 
			
		||||
            icon: 'roundRect',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      yAxis: outgoingData.length === 0 ? undefined : [
 | 
			
		||||
        {
 | 
			
		||||
          type: 'value',
 | 
			
		||||
          axisLabel: {
 | 
			
		||||
            color: 'rgb(110, 112, 121)',
 | 
			
		||||
            formatter: (val) => {
 | 
			
		||||
              return `${this.amountShortenerPipe.transform(Math.abs(val), 2, undefined, true)} sats`;
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          splitLine: {
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              type: 'dotted',
 | 
			
		||||
              color: '#ffffff66',
 | 
			
		||||
              opacity: 0.25,
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      series: outgoingData.length === 0 ? undefined : [
 | 
			
		||||
        {
 | 
			
		||||
          zlevel: 0,
 | 
			
		||||
          name: 'Outgoing Fees',
 | 
			
		||||
          data: outgoingData.map(bucket => ({
 | 
			
		||||
            value: bucket.capacity,
 | 
			
		||||
            label: bucket.label,
 | 
			
		||||
            capacity: bucket.capacity,
 | 
			
		||||
            count: bucket.count,
 | 
			
		||||
          })),
 | 
			
		||||
          type: 'bar',
 | 
			
		||||
          barWidth: '90%',
 | 
			
		||||
          barMaxWidth: 50,
 | 
			
		||||
          stack: 'fees',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          zlevel: 0,
 | 
			
		||||
          name: 'Incoming Fees',
 | 
			
		||||
          data: incomingData.map(bucket => ({
 | 
			
		||||
            value: -bucket.capacity,
 | 
			
		||||
            label: bucket.label,
 | 
			
		||||
            capacity: bucket.capacity,
 | 
			
		||||
            count: bucket.count,
 | 
			
		||||
          })),
 | 
			
		||||
          type: 'bar',
 | 
			
		||||
          barWidth: '90%',
 | 
			
		||||
          barMaxWidth: 50,
 | 
			
		||||
          stack: 'fees',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onChartInit(ec) {
 | 
			
		||||
    if (this.chartInstance !== undefined) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.chartInstance = ec;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isMobile() {
 | 
			
		||||
    return (window.innerWidth <= 767.98);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onSaveChart() {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    const prevBottom = this.chartOptions.grid.bottom;
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    this.chartOptions.grid.bottom = 40;
 | 
			
		||||
    this.chartOptions.backgroundColor = '#11131f';
 | 
			
		||||
    this.chartInstance.setOption(this.chartOptions);
 | 
			
		||||
    download(this.chartInstance.getDataURL({
 | 
			
		||||
      pixelRatio: 2,
 | 
			
		||||
    }), `node-fee-chart.svg`);
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    this.chartOptions.grid.bottom = prevBottom;
 | 
			
		||||
    this.chartOptions.backgroundColor = 'none';
 | 
			
		||||
    this.chartInstance.setOption(this.chartOptions);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -37,8 +37,9 @@
 | 
			
		||||
    }
 | 
			
		||||
    &.more-padding {
 | 
			
		||||
      padding-top: 10px;
 | 
			
		||||
    }  
 | 
			
		||||
    &:first-child{
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
      display: none;
 | 
			
		||||
      @media (min-width: 485px) {
 | 
			
		||||
        display: block;
 | 
			
		||||
@ -48,10 +49,7 @@
 | 
			
		||||
      }    
 | 
			
		||||
      @media (min-width: 992px) {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }    
 | 
			
		||||
    }
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
      }  
 | 
			
		||||
    }
 | 
			
		||||
    .card-text span {
 | 
			
		||||
      color: #ffffff66;
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<div class="box preview-box" *ngIf="(node$ | async) as node">
 | 
			
		||||
  <app-preview-title>
 | 
			
		||||
    <span i18n="lightning.node">lightning node</span>
 | 
			
		||||
    <span i18n="lightning.node">Lightning node</span>
 | 
			
		||||
  </app-preview-title>
 | 
			
		||||
  <div class="row d-flex justify-content-between full-width-row">
 | 
			
		||||
    <h1 class="title"></h1>
 | 
			
		||||
@ -29,13 +29,13 @@
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="lightning.active-channels-avg">Average size</td>
 | 
			
		||||
            <td i18n="lightning.avg-size">Average size</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <app-amount [satoshis]="node.avgCapacity" [noFiat]="true"></app-amount>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr *ngIf="node.city">
 | 
			
		||||
            <td i18n="location">Location</td>
 | 
			
		||||
            <td i18n="lightning.location">Location</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <span>{{ node.city.en }}</span>
 | 
			
		||||
            </td>
 | 
			
		||||
@ -47,7 +47,7 @@
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr *ngIf="!node.city && !node.country">
 | 
			
		||||
            <td i18n="location">Location</td>
 | 
			
		||||
            <td i18n="lightning.location">Location</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <span>unknown</span>
 | 
			
		||||
            </td>
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,6 @@
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="error" class="d-flex flex-column justify-content-around align-items-center mt-5 w-100" style="min-height: 100px">
 | 
			
		||||
    <span i18n="lightning.node-not-found">No node found for public key "{{ node.public_key | shortenString : 12}}"</span>
 | 
			
		||||
    <a [routerLink]="['/lightning' | relativeUrl]" i18n="lightning.back-to-lightning-dashboard">Back to the lightning dashboard</a>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="box" *ngIf="!error">
 | 
			
		||||
@ -45,7 +44,7 @@
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td i18n="location" class="text-truncate">Location</td>
 | 
			
		||||
              <td i18n="lightning.location" class="text-truncate">Location</td>
 | 
			
		||||
              <td *ngIf="node.geolocation">
 | 
			
		||||
                <app-geolocation [data]="node.geolocation" [type]="'node'"></app-geolocation>
 | 
			
		||||
              </td>
 | 
			
		||||
@ -61,19 +60,19 @@
 | 
			
		||||
        <table class="table table-borderless table-striped table-fixed">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td i18n="address.total-received" class="text-truncate label">First seen</td>
 | 
			
		||||
              <td i18n="transaction.first-seen|Transaction first seen" class="text-truncate label">First seen</td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <app-timestamp [unixTime]="node.first_seen"></app-timestamp>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td i18n="address.total-sent" class="text-truncate label">Last update</td>
 | 
			
		||||
              <td class="text-truncate label" i18n="lightning.last_update">Last update</td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <app-timestamp [unixTime]="node.updated_at"></app-timestamp>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td i18n="address.balance" class="text-truncate label">Color</td>
 | 
			
		||||
              <td i18n="lightning.color" class="text-truncate label">Color</td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <div [ngStyle]="{'color': node.color}">{{ node.color }}</div>
 | 
			
		||||
              </td>
 | 
			
		||||
@ -141,6 +140,8 @@
 | 
			
		||||
 | 
			
		||||
    <app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels>
 | 
			
		||||
 | 
			
		||||
    <app-node-fee-chart style="display:block;margin-bottom: 40px"></app-node-fee-chart>
 | 
			
		||||
 | 
			
		||||
    <div class="d-flex">
 | 
			
		||||
      <h2 *ngIf="channelsListStatus === 'open'">
 | 
			
		||||
        <span i18n="lightning.open-channels">Open channels</span>
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ export class NodeComponent implements OnInit {
 | 
			
		||||
          return this.lightningApiService.getNode$(params.get('public_key'));
 | 
			
		||||
        }),
 | 
			
		||||
        map((node) => {
 | 
			
		||||
          this.seoService.setTitle(`Node: ${node.alias}`);
 | 
			
		||||
          this.seoService.setTitle($localize`Node: ${node.alias}`);
 | 
			
		||||
 | 
			
		||||
          const socketsObject = [];
 | 
			
		||||
          for (const socket of node.sockets.split(',')) {
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
    <div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
 | 
			
		||||
      <div *ngIf="style === 'graph'" class="card-header">
 | 
			
		||||
        <div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
 | 
			
		||||
          <span i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</span>
 | 
			
		||||
          <span i18n="lightning.nodes-channels-world-map">Lightning Nodes Channels World Map</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -66,7 +66,7 @@ export class NodesChannelsMap implements OnInit {
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if (this.style === 'graph') {
 | 
			
		||||
      this.seoService.setTitle($localize`Lightning nodes channels world map`);
 | 
			
		||||
      this.seoService.setTitle($localize`Lightning Nodes Channels World Map`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (['nodepage', 'channelpage'].includes(this.style)) {
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,8 @@
 | 
			
		||||
  <table class="table table-borderless">
 | 
			
		||||
    <thead>
 | 
			
		||||
      <th class="alias text-left" i18n="nodes.alias">Alias</th>
 | 
			
		||||
      <th class="capacity text-right" [class]="show" i18n="node.capacity">Capacity</th>
 | 
			
		||||
      <th class="channels text-right" [class]="show" i18n="node.channels">Channels</th>
 | 
			
		||||
      <th class="capacity text-right" [class]="show" i18n="lightning.capacity">Capacity</th>
 | 
			
		||||
      <th class="channels text-right" [class]="show" i18n="lightning.channels">Channels</th>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody *ngIf="nodes$ | async as nodes; else skeleton">
 | 
			
		||||
      <tr *ngFor="let node of nodes; let i = index;">
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="!widget" class="card-header">
 | 
			
		||||
    <div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
 | 
			
		||||
      <span i18n="lightning.nodes-world-map">Lightning nodes world map</span>
 | 
			
		||||
      <span i18n="lightning.nodes-world-map">Lightning Nodes World Map</span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,7 @@ export class NodesMap implements OnInit, OnChanges {
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    if (!this.widget) {
 | 
			
		||||
      this.seoService.setTitle($localize`Lightning nodes world map`);
 | 
			
		||||
      this.seoService.setTitle($localize`:@@af8560ca50882114be16c951650f83bca73161a7:Lightning Nodes World Map`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.inputNodes$) {
 | 
			
		||||
@ -141,7 +141,7 @@ export class NodesMap implements OnInit, OnChanges {
 | 
			
		||||
          color: 'grey',
 | 
			
		||||
          fontSize: 15
 | 
			
		||||
        },
 | 
			
		||||
        text: $localize`No data to display yet`,
 | 
			
		||||
        text: $localize`No data to display yet. Try again later.`,
 | 
			
		||||
        left: 'center',
 | 
			
		||||
        top: 'center'
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
  <div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
 | 
			
		||||
    <div class="d-flex d-md-block align-items-baseline">
 | 
			
		||||
      <span i18n="lightning.nodes-networks">Lightning nodes per network</span>
 | 
			
		||||
      <span i18n="lightning.nodes-networks">Lightning Nodes Per Network</span>
 | 
			
		||||
      <button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
 | 
			
		||||
        <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ export class NodesNetworksChartComponent implements OnInit {
 | 
			
		||||
    if (this.widget) {
 | 
			
		||||
      this.miningWindowPreference = '3y';
 | 
			
		||||
    } else {
 | 
			
		||||
      this.seoService.setTitle($localize`Lightning nodes per network`);
 | 
			
		||||
      this.seoService.setTitle($localize`:@@b420668a91f8ebaf6e6409c4ba87f1d45961d2bd:Lightning Nodes Per Network`);
 | 
			
		||||
      this.miningWindowPreference = this.miningService.getDefaultTimespan('all');
 | 
			
		||||
    }
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
 | 
			
		||||
@ -128,7 +128,7 @@ export class NodesNetworksChartComponent implements OnInit {
 | 
			
		||||
          color: 'grey',
 | 
			
		||||
          fontSize: 11
 | 
			
		||||
        },
 | 
			
		||||
        text: $localize`Nodes per network`,
 | 
			
		||||
        text: $localize`:@@b420668a91f8ebaf6e6409c4ba87f1d45961d2bd:Lightning Nodes Per Network`,
 | 
			
		||||
        left: 'center',
 | 
			
		||||
        top: 11,
 | 
			
		||||
        zlevel: 10,
 | 
			
		||||
@ -139,7 +139,7 @@ export class NodesNetworksChartComponent implements OnInit {
 | 
			
		||||
      {
 | 
			
		||||
        zlevel: 1,
 | 
			
		||||
        yAxisIndex: 0,
 | 
			
		||||
        name: $localize`Unknown`,
 | 
			
		||||
        name: $localize`:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown`,
 | 
			
		||||
        showSymbol: false,
 | 
			
		||||
        symbol: 'none',
 | 
			
		||||
        data: data.unannounced_nodes,
 | 
			
		||||
@ -308,7 +308,7 @@ export class NodesNetworksChartComponent implements OnInit {
 | 
			
		||||
            icon: 'roundRect',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: $localize`Unknown`,
 | 
			
		||||
            name: $localize`:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown`,
 | 
			
		||||
            inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              color: 'white',
 | 
			
		||||
@ -320,7 +320,7 @@ export class NodesNetworksChartComponent implements OnInit {
 | 
			
		||||
          '$localize`Reachable on Darknet Only`': true,
 | 
			
		||||
          '$localize`Reachable on Clearnet Only`': true,
 | 
			
		||||
          '$localize`Reachable on Clearnet and Darknet`': true,
 | 
			
		||||
          '$localize`Unknown`': true,
 | 
			
		||||
          '$localize`:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown`': true,
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      yAxis: data.tor_nodes.length === 0 ? undefined : [
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
  <div class="card-header">
 | 
			
		||||
    <div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
 | 
			
		||||
      <span i18n="lightning.nodes-per-country">Lightning nodes per country</span>
 | 
			
		||||
      <span i18n="lightning.nodes-per-country">Lightning Nodes Per Country</span>
 | 
			
		||||
      <button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
 | 
			
		||||
        <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,7 @@ export class NodesPerCountryChartComponent implements OnInit {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.seoService.setTitle($localize`Lightning nodes per country`);
 | 
			
		||||
    this.seoService.setTitle($localize`:@@9d3ad4c6623870d96b65fb7a708fed6ce7c20044:Lightning Nodes Per Country`);
 | 
			
		||||
 | 
			
		||||
    this.nodesPerCountryObservable$ = this.apiService.getNodesPerCountry$()
 | 
			
		||||
      .pipe(
 | 
			
		||||
@ -100,7 +100,7 @@ export class NodesPerCountryChartComponent implements OnInit {
 | 
			
		||||
          borderColor: '#000',
 | 
			
		||||
          formatter: () => {
 | 
			
		||||
            return `<b style="color: white">${country.name.en} (${country.share}%)</b><br>` +
 | 
			
		||||
              $localize`${country.count.toString()} nodes<br>` +
 | 
			
		||||
              $localize`${country.count.toString()} nodes` + `<br>` +
 | 
			
		||||
              $localize`${this.amountShortenerPipe.transform(country.capacity / 100000000, 2)} BTC capacity`
 | 
			
		||||
            ;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<div class="container-xl full-height" style="min-height: 335px">
 | 
			
		||||
  <h1 i18n="lightning.nodes-in-country">
 | 
			
		||||
    <span>Lightning nodes in {{ country?.name }}</span>
 | 
			
		||||
  <h1>
 | 
			
		||||
    <span i18n="lightning.nodes-in-country">Lightning nodes in {{ country?.name }}</span>
 | 
			
		||||
    <span style="font-size: 50px; vertical-align:sub;"> {{ country?.flag }}</span>
 | 
			
		||||
  </h1>
 | 
			
		||||
 | 
			
		||||
@ -58,7 +58,7 @@
 | 
			
		||||
      
 | 
			
		||||
      <thead>
 | 
			
		||||
        <th class="alias text-left" i18n="lightning.alias">Alias</th>
 | 
			
		||||
        <th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
 | 
			
		||||
        <th class="timestamp-first text-left" i18n="transaction.first-seen|Transaction first seen">First seen</th>
 | 
			
		||||
        <th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
 | 
			
		||||
        <th class="capacity text-right" i18n="lightning.capacity">Capacity</th>
 | 
			
		||||
        <th class="channels text-right" i18n="lightning.channels">Channels</th>
 | 
			
		||||
 | 
			
		||||
@ -3,21 +3,21 @@
 | 
			
		||||
  <div *ngIf="widget">
 | 
			
		||||
    <div class="pool-distribution" *ngIf="(nodesPerAsObservable$ | async) as stats; else loadingReward">
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <h5 class="card-title d-inline-block" i18n="lightning.clearnet-capacity">Clearnet capacity</h5>
 | 
			
		||||
        <h5 class="card-title d-inline-block" i18n="lightning.clearnet-capacity">Clearnet Capacity</h5>
 | 
			
		||||
        <p class="card-text" i18n-ngbTooltip="lightning.clearnet-capacity-desc"
 | 
			
		||||
          ngbTooltip="How much liquidity is running on nodes advertising at least one clearnet IP address" placement="bottom">
 | 
			
		||||
          <app-amount [satoshis]="stats.clearnetCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown capacity</h5>
 | 
			
		||||
        <h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown Capacity</h5>
 | 
			
		||||
        <p class="card-text" i18n-ngbTooltip="lightning.unknown-capacity-desc"
 | 
			
		||||
        ngbTooltip="How much liquidity is running on nodes which ISP was not identifiable" placement="bottom">
 | 
			
		||||
          <app-amount [satoshis]="stats.unknownCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor capacity</h5>
 | 
			
		||||
        <h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor Capacity</h5>
 | 
			
		||||
        <p class="card-text" i18n-ngbTooltip="lightning.tor-capacity-desc"
 | 
			
		||||
        ngbTooltip="How much liquidity is running on nodes advertising only Tor addresses" placement="bottom">
 | 
			
		||||
          <app-amount [satoshis]="stats.torCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
 | 
			
		||||
@ -33,8 +33,8 @@
 | 
			
		||||
        <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <small class="d-block" style="color: #ffffff66; min-height: 25px" i18n="lightning.tor-nodes-excluded">
 | 
			
		||||
      <span>(Tor nodes excluded)</span>
 | 
			
		||||
    <small class="d-block" style="color: #ffffff66; min-height: 25px" >
 | 
			
		||||
      <span i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</span>
 | 
			
		||||
    </small>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -80,19 +80,19 @@
 | 
			
		||||
<ng-template #loadingReward>
 | 
			
		||||
  <div class="pool-distribution">
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title d-inline-block" i18n="lightning.clearnet-capacity">Clearnet capacity</h5>
 | 
			
		||||
      <h5 class="card-title d-inline-block" i18n="lightning.clearnet-capacity">Clearnet Capacity</h5>
 | 
			
		||||
      <p class="card-text">
 | 
			
		||||
        <span class="skeleton-loader skeleton-loader-big"></span>
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown capacity</h5>
 | 
			
		||||
      <h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown Capacity</h5>
 | 
			
		||||
      <p class="card-text">
 | 
			
		||||
        <span class="skeleton-loader skeleton-loader-big"></span>
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="item">
 | 
			
		||||
      <h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor capacity</h5>
 | 
			
		||||
      <h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor Capacity</h5>
 | 
			
		||||
      <p class="card-text">
 | 
			
		||||
        <span class="skeleton-loader skeleton-loader-big"></span>
 | 
			
		||||
      </p>
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,7 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    if (!this.widget) {
 | 
			
		||||
      this.seoService.setTitle($localize`Lightning nodes per ISP`);
 | 
			
		||||
      this.seoService.setTitle($localize`:@@8573a1576789bd2c4faeaed23037c4917812c6cf:Lightning Nodes Per ISP`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.nodesPerAsObservable$ = combineLatest([
 | 
			
		||||
@ -154,7 +154,7 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
			
		||||
          borderColor: '#000',
 | 
			
		||||
          formatter: () => {
 | 
			
		||||
            return `<b style="color: white">${isp[1]} (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)</b><br>` +
 | 
			
		||||
              $localize`${isp[4].toString()} nodes<br>` +
 | 
			
		||||
              $localize`${isp[4].toString()} nodes` + `<br>` +
 | 
			
		||||
              $localize`${this.amountShortenerPipe.transform(isp[2] / 100000000, 2)} BTC`
 | 
			
		||||
            ;
 | 
			
		||||
          }
 | 
			
		||||
@ -186,7 +186,7 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
			
		||||
        borderColor: '#000',
 | 
			
		||||
        formatter: () => {
 | 
			
		||||
          return `<b style="color: white">Other (${totalShareOther.toFixed(2)}%)</b><br>` +
 | 
			
		||||
            $localize`${nodeCountOther.toString()} nodes<br>` +
 | 
			
		||||
            $localize`${nodeCountOther.toString()} nodes` + `<br>` +
 | 
			
		||||
            $localize`${this.amountShortenerPipe.transform(capacityOther / 100000000, 2)} BTC`;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<div class="box preview-box" *ngIf="(nodes$ | async) as ispNodes">
 | 
			
		||||
  <app-preview-title>
 | 
			
		||||
    <span i18n="lightning.node">lightning ISP</span>
 | 
			
		||||
    <span i18n="lightning.node-isp">Lightning ISP</span>
 | 
			
		||||
  </app-preview-title>
 | 
			
		||||
  <div class="row d-flex justify-content-between full-width-row">
 | 
			
		||||
    <div class="title-wrapper">
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,7 @@
 | 
			
		||||
 | 
			
		||||
      <thead>
 | 
			
		||||
        <th class="alias text-left" i18n="lightning.alias">Alias</th>
 | 
			
		||||
        <th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
 | 
			
		||||
        <th class="timestamp-first text-left" i18n="transaction.first-seen|Transaction first seen">First seen</th>
 | 
			
		||||
        <th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
 | 
			
		||||
        <th class="capacity text-right" i18n="lightning.capacity">Capacity</th>
 | 
			
		||||
        <th class="channels text-right" i18n="lightning.channels">Channels</th>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<div [class]="!widget ? 'container-xl full-height' : ''">
 | 
			
		||||
  <h1 *ngIf="!widget" class="float-left" i18n="lightning.top-100-oldest-nodes">
 | 
			
		||||
    <span>Top 100 oldest lightning nodes</span>
 | 
			
		||||
  <h1 *ngIf="!widget" class="float-left">
 | 
			
		||||
    <span i18n="lightning.top-100-oldest-nodes">Top 100 oldest lightning nodes</span>
 | 
			
		||||
  </h1>
 | 
			
		||||
 | 
			
		||||
  <div [class]="widget ? 'widget' : 'full'">
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
      <thead>
 | 
			
		||||
        <th class="rank"></th>
 | 
			
		||||
        <th class="alias text-left" i18n="nodes.alias">Alias</th>
 | 
			
		||||
        <th  class="timestamp-first text-right" i18n="lightning.first_seen">First seen</th>
 | 
			
		||||
        <th  class="timestamp-first text-right" i18n="transaction.first-seen|Transaction first seen">First seen</th>
 | 
			
		||||
        <th *ngIf="!widget" class="capacity text-right" i18n="node.liquidity">Liquidity</th>
 | 
			
		||||
        <th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th>
 | 
			
		||||
        <th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
        <th class="alias text-left" i18n="nodes.alias">Alias</th>
 | 
			
		||||
        <th class="capacity text-right" i18n="node.liquidity">Liquidity</th>
 | 
			
		||||
        <th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th>
 | 
			
		||||
        <th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
 | 
			
		||||
        <th *ngIf="!widget" class="timestamp-first text-left" i18n="transaction.first-seen|Transaction first seen">First seen</th>
 | 
			
		||||
        <th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
 | 
			
		||||
        <th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
 | 
			
		||||
      </thead>
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@ export class TopNodesPerCapacity implements OnInit {
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    if (!this.widget) {
 | 
			
		||||
      this.seoService.setTitle($localize`Liquidity Ranking`);
 | 
			
		||||
      this.seoService.setTitle($localize`:@@2d9883d230a47fbbb2ec969e32a186597ea27405:Liquidity Ranking`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) {
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
        <th class="alias text-left" i18n="nodes.alias">Alias</th>
 | 
			
		||||
        <th class="channels text-right" i18n="node.channels">Channels</th>
 | 
			
		||||
        <th *ngIf="!widget" class="capacity text-right" i18n="lightning.liquidity">Liquidity</th>
 | 
			
		||||
        <th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
 | 
			
		||||
        <th *ngIf="!widget" class="timestamp-first text-left" i18n="transaction.first-seen|Transaction first seen">First seen</th>
 | 
			
		||||
        <th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
 | 
			
		||||
        <th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
 | 
			
		||||
      </thead>
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { map, Observable } from 'rxjs';
 | 
			
		||||
import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
 | 
			
		||||
import { SeoService } from '../../../services/seo.service';
 | 
			
		||||
import { isMobile } from '../../../shared/common.utils';
 | 
			
		||||
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
 | 
			
		||||
import { LightningApiService } from '../../lightning-api.service';
 | 
			
		||||
@ -21,14 +20,9 @@ export class TopNodesPerChannels implements OnInit {
 | 
			
		||||
 | 
			
		||||
  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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/liquidity' | relativeUrl]">
 | 
			
		||||
            <h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity ranking</h5>
 | 
			
		||||
            <h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
 | 
			
		||||
            <span> </span>
 | 
			
		||||
            <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
 | 
			
		||||
              style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
 | 
			
		||||
@ -19,7 +19,7 @@
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/connectivity' | relativeUrl]">
 | 
			
		||||
            <h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity ranking</h5>
 | 
			
		||||
            <h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>
 | 
			
		||||
            <span> </span>
 | 
			
		||||
            <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
 | 
			
		||||
              style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
  <div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
 | 
			
		||||
    <div class="d-flex d-md-block align-items-baseline">
 | 
			
		||||
      <span i18n="mining.channels-and-capacity">Channels & Capacity</span>
 | 
			
		||||
      <span i18n="lightning.network-capacity">Lightning Network Capacity</span>
 | 
			
		||||
      <button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
 | 
			
		||||
        <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
      </button>
 | 
			
		||||
@ -49,9 +49,7 @@
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="widget && (capacityObservable$ | async) as stats">
 | 
			
		||||
    <div *ngIf="stats.days === 0" class="indexing-message d-flex" i18n="lightning.indexing-in-progress">
 | 
			
		||||
      Indexing in progress
 | 
			
		||||
    </div>
 | 
			
		||||
    <div *ngIf="stats.days === 0" class="indexing-message d-flex" i18n="lightning.indexing-in-progress">Indexing in progress</div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
@ -63,7 +63,7 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
    if (this.widget) {
 | 
			
		||||
      this.miningWindowPreference = '3y';
 | 
			
		||||
    } else {
 | 
			
		||||
      this.seoService.setTitle($localize`Channels and Capacity`);
 | 
			
		||||
      this.seoService.setTitle($localize`:@@ea8db27e6db64f8b940711948c001a1100e5fe9f:Lightning Network Capacity`);
 | 
			
		||||
      this.miningWindowPreference = this.miningService.getDefaultTimespan('all');
 | 
			
		||||
    }
 | 
			
		||||
    this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
 | 
			
		||||
@ -119,7 +119,7 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
          color: 'grey',
 | 
			
		||||
          fontSize: 11
 | 
			
		||||
        },
 | 
			
		||||
        text: $localize`Channels & Capacity`,
 | 
			
		||||
        text: $localize`:@@ea8db27e6db64f8b940711948c001a1100e5fe9f:Lightning Network Capacity`,
 | 
			
		||||
        left: 'center',
 | 
			
		||||
        top: 11,
 | 
			
		||||
        zlevel: 10,
 | 
			
		||||
@ -191,7 +191,7 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
        padding: 10,
 | 
			
		||||
        data: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Channels',
 | 
			
		||||
            name: $localize`:@@807cf11e6ac1cde912496f764c176bdfdd6b7e19:Channels`,
 | 
			
		||||
            inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              color: 'white',
 | 
			
		||||
@ -199,7 +199,7 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
            icon: 'roundRect',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Capacity',
 | 
			
		||||
            name: $localize`:@@ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa:Capacity`,
 | 
			
		||||
            inactiveColor: 'rgb(110, 112, 121)',
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              color: 'white',
 | 
			
		||||
@ -279,7 +279,7 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
        {
 | 
			
		||||
          zlevel: 0,
 | 
			
		||||
          yAxisIndex: 1,
 | 
			
		||||
          name: $localize`Capacity`,
 | 
			
		||||
          name: $localize`:@@ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa:Capacity`,
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          symbol: 'none',
 | 
			
		||||
          stack: 'Total',
 | 
			
		||||
@ -341,7 +341,7 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
    this.chartInstance.setOption(this.chartOptions);
 | 
			
		||||
    download(this.chartInstance.getDataURL({
 | 
			
		||||
      pixelRatio: 2,
 | 
			
		||||
    }), `block-sizes-weights-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
 | 
			
		||||
    }), `lightning-network-capacity-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    this.chartOptions.grid.bottom = prevBottom;
 | 
			
		||||
    this.chartOptions.backgroundColor = 'none';
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										90
									
								
								frontend/src/app/services/navigation.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								frontend/src/app/services/navigation.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { Router, ActivatedRoute, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router';
 | 
			
		||||
import { BehaviorSubject } from 'rxjs';
 | 
			
		||||
import { filter, map } from 'rxjs/operators';
 | 
			
		||||
import { StateService } from './state.service';
 | 
			
		||||
 | 
			
		||||
const networkModules = {
 | 
			
		||||
  bitcoin: {
 | 
			
		||||
    subnets: [
 | 
			
		||||
      { name: 'mainnet', path: '' },
 | 
			
		||||
      { name: 'testnet', path: '/testnet' },
 | 
			
		||||
      { name: 'signet', path: '/signet' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  liquid: {
 | 
			
		||||
    subnets: [
 | 
			
		||||
      { name: 'liquid', path: '' },
 | 
			
		||||
      { name: 'liquidtestnet', path: '/testnet' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  bisq: {
 | 
			
		||||
    subnets: [
 | 
			
		||||
      { name: 'bisq', path: '' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
const networks = Object.keys(networkModules);
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
})
 | 
			
		||||
export class NavigationService {
 | 
			
		||||
  subnetPaths = new BehaviorSubject<Record<string,string>>({});
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.router.events.pipe(
 | 
			
		||||
      filter(event => event instanceof NavigationEnd),
 | 
			
		||||
      map(() => this.router.routerState.snapshot.root),
 | 
			
		||||
    ).subscribe((state) => {
 | 
			
		||||
      this.updateSubnetPaths(state);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // For each network (bitcoin/liquid/bisq), find and save the longest url path compatible with the current route
 | 
			
		||||
  updateSubnetPaths(root: ActivatedRouteSnapshot): void {
 | 
			
		||||
    let path = '';
 | 
			
		||||
    const networkPaths = {};
 | 
			
		||||
    let route = root;
 | 
			
		||||
    // traverse the router state tree until all network paths are set, or we reach the end of the tree
 | 
			
		||||
    while (!networks.reduce((acc, network) => acc && !!networkPaths[network], true) && route) {
 | 
			
		||||
      // 'networkSpecific' paths may correspond to valid routes on other networks, but aren't directly compatible
 | 
			
		||||
      // (e.g. we shouldn't link a mainnet transaction page to the same txid on testnet or liquid)
 | 
			
		||||
      if (route.data?.networkSpecific) {
 | 
			
		||||
        networks.forEach(network => {
 | 
			
		||||
          if (networkPaths[network] == null) {
 | 
			
		||||
            networkPaths[network] = path;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      // null or empty networks list is shorthand for "compatible with every network"
 | 
			
		||||
      if (route.data?.networks?.length) {
 | 
			
		||||
        // if the list is non-empty, only those networks are compatible
 | 
			
		||||
        networks.forEach(network => {
 | 
			
		||||
          if (!route.data.networks.includes(network)) {
 | 
			
		||||
            if (networkPaths[network] == null) {
 | 
			
		||||
              networkPaths[network] = path;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      if (route.url?.length) {
 | 
			
		||||
        path = [path, ...route.url.map(segment => segment.path).filter(path => {
 | 
			
		||||
          return path.length && !['testnet', 'signet'].includes(path);
 | 
			
		||||
        })].join('/');
 | 
			
		||||
      }
 | 
			
		||||
      route = route.firstChild;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const subnetPaths = {};
 | 
			
		||||
    Object.entries(networkModules).forEach(([key, network]) => {
 | 
			
		||||
      network.subnets.forEach(subnet => {
 | 
			
		||||
        subnetPaths[subnet.name] = subnet.path + (networkPaths[key] != null ? networkPaths[key] : path);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    this.subnetPaths.next(subnetPaths);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
 | 
			
		||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
 | 
			
		||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
 | 
			
		||||
import { Transaction } from '../interfaces/electrs.interface';
 | 
			
		||||
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
 | 
			
		||||
@ -110,9 +110,11 @@ export class StateService {
 | 
			
		||||
 | 
			
		||||
  blockScrolling$: Subject<boolean> = new Subject<boolean>();
 | 
			
		||||
  timeLtr: BehaviorSubject<boolean>;
 | 
			
		||||
  hideFlow: BehaviorSubject<boolean>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(PLATFORM_ID) private platformId: any,
 | 
			
		||||
    @Inject(LOCALE_ID) private locale: string,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private storageService: StorageService,
 | 
			
		||||
  ) {
 | 
			
		||||
@ -151,10 +153,23 @@ export class StateService {
 | 
			
		||||
 | 
			
		||||
    this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4;
 | 
			
		||||
 | 
			
		||||
    this.timeLtr = new BehaviorSubject<boolean>(this.storageService.getValue('time-preference-ltr') === 'true');
 | 
			
		||||
    const savedTimePreference = this.storageService.getValue('time-preference-ltr');
 | 
			
		||||
    const rtlLanguage = (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he'));
 | 
			
		||||
    // default time direction is right-to-left, unless locale is a RTL language
 | 
			
		||||
    this.timeLtr = new BehaviorSubject<boolean>(savedTimePreference === 'true' || (savedTimePreference == null && rtlLanguage));
 | 
			
		||||
    this.timeLtr.subscribe((ltr) => {
 | 
			
		||||
      this.storageService.setValue('time-preference-ltr', ltr ? 'true' : 'false');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const savedFlowPreference = this.storageService.getValue('flow-preference');
 | 
			
		||||
    this.hideFlow = new BehaviorSubject<boolean>(savedFlowPreference === 'hide');
 | 
			
		||||
    this.hideFlow.subscribe((hide) => {
 | 
			
		||||
      if (hide) {
 | 
			
		||||
        this.storageService.setValue('flow-preference', hide ? 'hide' : 'show');
 | 
			
		||||
      } else {
 | 
			
		||||
        this.storageService.removeItem('flow-preference');
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setNetworkBasedonUrl(url: string) {
 | 
			
		||||
 | 
			
		||||
@ -46,4 +46,12 @@ export class StorageService {
 | 
			
		||||
      console.log(e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeItem(key: string): void {
 | 
			
		||||
    try {
 | 
			
		||||
      localStorage.removeItem(key);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log(e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -112,14 +112,14 @@ class Server {
 | 
			
		||||
        const screenshot = await page.screenshot();
 | 
			
		||||
        return screenshot;
 | 
			
		||||
      } else if (success === false) {
 | 
			
		||||
        logger.warn(`failed to render page preview for ${action} due to client-side error, e.g. requested an invalid txid`);
 | 
			
		||||
        logger.warn(`failed to render ${path} for ${action} due to client-side error, e.g. requested an invalid txid`);
 | 
			
		||||
        page.repairRequested = true;
 | 
			
		||||
      } else {
 | 
			
		||||
        logger.warn(`failed to render page preview for ${action} due to puppeteer timeout`);
 | 
			
		||||
        logger.warn(`failed to render ${path} for ${action} due to puppeteer timeout`);
 | 
			
		||||
        page.repairRequested = true;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`failed to render page for ${action}: ` + (e instanceof Error ? e.message : `${e}`));
 | 
			
		||||
      logger.err(`failed to render ${path} for ${action}: ` + (e instanceof Error ? e.message : `${e}`));
 | 
			
		||||
      page.repairRequested = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -154,7 +154,7 @@ class Server {
 | 
			
		||||
        res.send(img);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(e instanceof Error ? e.message : `${e}`);
 | 
			
		||||
      logger.err(e instanceof Error ? e.message : `${e} ${req.params[0]}`);
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user