Merge branch 'master' into simon/channel-closing-type-header
This commit is contained in:
		
						commit
						622636e35f
					
				@ -70,7 +70,7 @@ class ChannelsRoutes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getChannelsByTransactionIds(req: Request, res: Response) {
 | 
			
		||||
  private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      if (!Array.isArray(req.query.txId)) {
 | 
			
		||||
        res.status(400).send('Not an array');
 | 
			
		||||
@ -83,27 +83,26 @@ class ChannelsRoutes {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      const channels = await channelsApi.$getChannelsByTransactionId(txIds);
 | 
			
		||||
      const inputs: any[] = [];
 | 
			
		||||
      const outputs: any[] = [];
 | 
			
		||||
      const result: any[] = [];
 | 
			
		||||
      for (const txid of txIds) {
 | 
			
		||||
        const foundChannelInputs = channels.find((channel) => channel.closing_transaction_id === txid);
 | 
			
		||||
        if (foundChannelInputs) {
 | 
			
		||||
          inputs.push(foundChannelInputs);
 | 
			
		||||
        } else {
 | 
			
		||||
          inputs.push(null);
 | 
			
		||||
        const inputs: any = {};
 | 
			
		||||
        const outputs: any = {};
 | 
			
		||||
        // Assuming that we only have one lightning close input in each transaction. This may not be true in the future
 | 
			
		||||
        const foundChannelsFromInput = channels.find((channel) => channel.closing_transaction_id === txid);
 | 
			
		||||
        if (foundChannelsFromInput) {
 | 
			
		||||
          inputs[0] = foundChannelsFromInput;
 | 
			
		||||
        }
 | 
			
		||||
        const foundChannelOutputs = channels.find((channel) => channel.transaction_id === txid);
 | 
			
		||||
        if (foundChannelOutputs) {
 | 
			
		||||
          outputs.push(foundChannelOutputs);
 | 
			
		||||
        } else {
 | 
			
		||||
          outputs.push(null);
 | 
			
		||||
        const foundChannelsFromOutputs = channels.filter((channel) => channel.transaction_id === txid);
 | 
			
		||||
        for (const output of foundChannelsFromOutputs) {
 | 
			
		||||
          outputs[output.transaction_vout] = output;
 | 
			
		||||
        }
 | 
			
		||||
        result.push({
 | 
			
		||||
          inputs,
 | 
			
		||||
          outputs,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      res.json({
 | 
			
		||||
        inputs: inputs,
 | 
			
		||||
        outputs: outputs,
 | 
			
		||||
      });
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import { isIP } from 'net';
 | 
			
		||||
import { Common } from '../../../api/common';
 | 
			
		||||
import channelsApi from '../../../api/explorer/channels.api';
 | 
			
		||||
import nodesApi from '../../../api/explorer/nodes.api';
 | 
			
		||||
import { ResultSetHeader } from 'mysql2';
 | 
			
		||||
 | 
			
		||||
const fsPromises = promises;
 | 
			
		||||
 | 
			
		||||
@ -19,7 +20,12 @@ class LightningStatsImporter {
 | 
			
		||||
    logger.info('Caching funding txs for currently existing channels');
 | 
			
		||||
    await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await this.$importHistoricalLightningStats();
 | 
			
		||||
    await this.$cleanupIncorrectSnapshot();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -367,6 +373,12 @@ class LightningStatsImporter {
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
        if (this.isIncorrectSnapshot(timestamp, graph)) {
 | 
			
		||||
          logger.debug(`Ignoring ${this.topologiesFolder}/${filename}, because we defined it as an incorrect snapshot`);
 | 
			
		||||
          ++totalProcessed;
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!logStarted) {
 | 
			
		||||
          logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`);
 | 
			
		||||
          logStarted = true;
 | 
			
		||||
@ -397,7 +409,7 @@ class LightningStatsImporter {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async cleanupTopology(graph) {
 | 
			
		||||
  cleanupTopology(graph): ILightningApi.NetworkGraph {
 | 
			
		||||
    const newGraph = {
 | 
			
		||||
      nodes: <ILightningApi.Node[]>[],
 | 
			
		||||
      edges: <ILightningApi.Channel[]>[],
 | 
			
		||||
@ -456,6 +468,69 @@ class LightningStatsImporter {
 | 
			
		||||
 | 
			
		||||
    return newGraph;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private isIncorrectSnapshot(timestamp, graph): boolean {
 | 
			
		||||
    if (timestamp >= 1549065600 /* 2019-02-02 */ && timestamp <= 1550620800 /* 2019-02-20 */ && graph.nodes.length < 2600) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    if (timestamp >= 1552953600 /* 2019-03-19 */ && timestamp <= 1556323200 /* 2019-05-27 */ && graph.nodes.length < 4000) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    if (timestamp >= 1557446400 /* 2019-05-10 */ && timestamp <= 1560470400 /* 2019-06-14 */ && graph.nodes.length < 4000) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    if (timestamp >= 1561680000 /* 2019-06-28 */ && timestamp <= 1563148800 /* 2019-07-15 */ && graph.nodes.length < 4000) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    if (timestamp >= 1571270400 /* 2019-11-17 */ && timestamp <= 1580601600 /* 2020-02-02 */ && graph.nodes.length < 4500) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    if (timestamp >= 1591142400 /* 2020-06-03 */ && timestamp <= 1592006400 /* 2020-06-13 */ && graph.nodes.length < 5500) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    if (timestamp >= 1632787200 /* 2021-09-28 */ && timestamp <= 1633564800 /* 2021-10-07 */ && graph.nodes.length < 13000) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    if (timestamp >= 1634256000 /* 2021-10-15 */ && timestamp <= 1645401600 /* 2022-02-21 */ && graph.nodes.length < 17000) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    if (timestamp >= 1654992000 /* 2022-06-12 */ && timestamp <= 1661472000 /* 2022-08-26 */ && graph.nodes.length < 14000) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $cleanupIncorrectSnapshot(): Promise<void> {
 | 
			
		||||
    // We do not run this one automatically because those stats are not supposed to be inserted in the first
 | 
			
		||||
    // place, but I write them here to remind us we manually run those queries
 | 
			
		||||
 | 
			
		||||
    // DELETE FROM lightning_stats
 | 
			
		||||
    // WHERE (
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1549065600 AND UNIX_TIMESTAMP(added) <= 1550620800 AND node_count < 2600 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1552953600 AND UNIX_TIMESTAMP(added) <= 1556323200 AND node_count < 4000 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1557446400 AND UNIX_TIMESTAMP(added) <= 1560470400 AND node_count < 4000 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1561680000 AND UNIX_TIMESTAMP(added) <= 1563148800 AND node_count < 4000 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1571270400 AND UNIX_TIMESTAMP(added) <= 1580601600 AND node_count < 4500 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1591142400 AND UNIX_TIMESTAMP(added) <= 1592006400 AND node_count < 5500 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1632787200 AND UNIX_TIMESTAMP(added) <= 1633564800 AND node_count < 13000 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1634256000 AND UNIX_TIMESTAMP(added) <= 1645401600 AND node_count < 17000 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1654992000 AND UNIX_TIMESTAMP(added) <= 1661472000 AND node_count < 14000
 | 
			
		||||
    // )
 | 
			
		||||
 | 
			
		||||
    // DELETE FROM node_stats
 | 
			
		||||
    // WHERE (
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1549065600 AND UNIX_TIMESTAMP(added) <= 1550620800 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1552953600 AND UNIX_TIMESTAMP(added) <= 1556323200 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1557446400 AND UNIX_TIMESTAMP(added) <= 1560470400 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1561680000 AND UNIX_TIMESTAMP(added) <= 1563148800 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1571270400 AND UNIX_TIMESTAMP(added) <= 1580601600 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1591142400 AND UNIX_TIMESTAMP(added) <= 1592006400 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1632787200 AND UNIX_TIMESTAMP(added) <= 1633564800 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1634256000 AND UNIX_TIMESTAMP(added) <= 1645401600 OR
 | 
			
		||||
    //   UNIX_TIMESTAMP(added) >= 1654992000 AND UNIX_TIMESTAMP(added) <= 1661472000 
 | 
			
		||||
    // )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new LightningStatsImporter;
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,9 @@
 | 
			
		||||
 | 
			
		||||
  <div class="page-title">
 | 
			
		||||
    <h1 i18n="shared.transaction">Transaction</h1>
 | 
			
		||||
    <a class="tx-link" [routerLink]="['/tx/' | relativeUrl, txId]">
 | 
			
		||||
      <span class="truncated">{{txId.slice(0,-4)}}</span><span class="last-four">{{txId.slice(-4)}}</span>
 | 
			
		||||
    </a>
 | 
			
		||||
    <div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
 | 
			
		||||
      <app-tx-features [tx]="tx"></app-tx-features>
 | 
			
		||||
      <span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1">
 | 
			
		||||
@ -13,104 +16,50 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <a [routerLink]="['/tx/' | relativeUrl, txId]" class="tx-link">
 | 
			
		||||
    {{ txId }}
 | 
			
		||||
  </a>
 | 
			
		||||
  <div class="top-data row">
 | 
			
		||||
    <span class="field col-sm-4 text-left">
 | 
			
		||||
      <ng-template [ngIf]="isLiquid && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
 | 
			
		||||
      <ng-template #defaultAmount>
 | 
			
		||||
        <app-amount [satoshis]="totalValue"></app-amount>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </span>
 | 
			
		||||
    <span class="field col-sm-4 text-center">‎{{ (tx.status.confirmed ? tx.status.block_time : transactionTime) * 1000 | date:'yyyy-MM-dd HH:mm' }}</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>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="col-sm">
 | 
			
		||||
      <table class="table table-borderless table-striped">
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr *ngIf="tx.status.confirmed; else firstSeen">
 | 
			
		||||
            <td i18n="block.timestamp">Timestamp</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              ‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <ng-template #firstSeen>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td i18n="transaction.first-seen|Transaction first seen">First seen</td>
 | 
			
		||||
              <td *ngIf="transactionTime > 0; else notSeen">
 | 
			
		||||
                ‎{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }}
 | 
			
		||||
              </td>
 | 
			
		||||
              <ng-template #notSeen>
 | 
			
		||||
                <td>?</td>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="td-width" i18n="dashboard.latest-transactions.amount">Amount</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
 | 
			
		||||
              <ng-template #defaultAmount>
 | 
			
		||||
                <app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="block.size">Size</td>
 | 
			
		||||
            <td [innerHTML]="'‎' + (tx.size | bytes: 2)"></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="block.weight">Weight</td>
 | 
			
		||||
            <td [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="transaction.inputs">Inputs</td>
 | 
			
		||||
            <td *ngIf="!isCoinbase(tx); else coinbaseInputs">{{ tx.vin.length }}</td>
 | 
			
		||||
            <ng-template #coinbaseInputs>
 | 
			
		||||
              <td i18n="transactions-list.coinbase">Coinbase</td>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
  <div class="row graph-wrapper">
 | 
			
		||||
    <tx-bowtie-graph [tx]="tx" [width]="1112" [height]="346" [network]="network"></tx-bowtie-graph>
 | 
			
		||||
    <div class="above-bow">
 | 
			
		||||
      <p class="field pair">
 | 
			
		||||
        <span [innerHTML]="'‎' + (tx.size | bytes: 2)"></span>
 | 
			
		||||
        <span [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></span>
 | 
			
		||||
      </p>
 | 
			
		||||
      <p class="field" *ngIf="!isCoinbase(tx)">
 | 
			
		||||
        {{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="col-sm">
 | 
			
		||||
      <table class="table table-borderless table-striped">
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
 | 
			
		||||
            <td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr *ngIf="!cpfpInfo || (!cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length); else cpfpFee">
 | 
			
		||||
            <td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              {{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
			
		||||
              <ng-template [ngIf]="tx.status.confirmed">
 | 
			
		||||
                 
 | 
			
		||||
                <app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <ng-template #cpfpFee>
 | 
			
		||||
    <div class="overlaid">
 | 
			
		||||
      <ng-container [ngSwitch]="extraData">
 | 
			
		||||
        <table class="opreturns" *ngSwitchCase="'coinbase'">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <div class="effective-fee-container">
 | 
			
		||||
                  {{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
 | 
			
		||||
                  <ng-template [ngIf]="tx.status.confirmed">
 | 
			
		||||
                    <app-tx-fee-rating class="d-none d-lg-inline ml-2" *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
 | 
			
		||||
                  </ng-template>
 | 
			
		||||
                </div>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td class="label">Coinbase</td>
 | 
			
		||||
              <td class="message">{{ tx.vin[0].scriptsig | hex2ascii }}</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
 | 
			
		||||
            <td [innerHTML]="'‎' + (tx.weight / 4 | vbytes: 2)"></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="transaction.locktime">Locktime</td>
 | 
			
		||||
            <td [innerHTML]="'‎' + (tx.locktime | number)"></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="transaction.outputs">Outputs</td>
 | 
			
		||||
            <td>{{ tx.vout.length }}</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
        <table class="opreturns" *ngSwitchCase="'opreturn'">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <ng-container *ngFor="let vout of opReturns.slice(0,3)">
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td class="label">OP_RETURN</td>
 | 
			
		||||
                <td *ngIf="vout.scriptpubkey_asm !== 'OP_RETURN'" class="message">{{ vout.scriptpubkey_asm | hex2ascii }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -10,26 +10,10 @@
 | 
			
		||||
	font-size: 28px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-small-height {
 | 
			
		||||
	line-height: 1.1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.arrow-green {
 | 
			
		||||
	color: #1a9436;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.arrow-red {
 | 
			
		||||
	color: #dc3545;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.row {
 | 
			
		||||
	flex-direction: row;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.effective-fee-container {
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title {
 | 
			
		||||
  h2 {
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
@ -46,8 +30,9 @@
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  align-items: baseline;
 | 
			
		||||
  margin-bottom: 2px;
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
 | 
			
		||||
  h1 {
 | 
			
		||||
    font-size: 52px;
 | 
			
		||||
@ -58,6 +43,43 @@
 | 
			
		||||
  .features {
 | 
			
		||||
    font-size: 24px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & > * {
 | 
			
		||||
    flex-grow: 0;
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .tx-link {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    flex-shrink: 1;
 | 
			
		||||
    margin: 0 1em;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
 | 
			
		||||
    .truncated {
 | 
			
		||||
      flex-grow: 1;
 | 
			
		||||
      flex-shrink: 1;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      text-overflow: ellipsis;
 | 
			
		||||
      margin-right: -2px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .last-four {
 | 
			
		||||
      flex-shrink: 0;
 | 
			
		||||
      flex-grow: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .features {
 | 
			
		||||
    align-self: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.top-data {
 | 
			
		||||
  font-size: 28px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table {
 | 
			
		||||
@ -68,8 +90,76 @@
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.field {
 | 
			
		||||
  font-size: 32px;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
 | 
			
		||||
  ::ng-deep .symbol {
 | 
			
		||||
    font-size: 24px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .label {
 | 
			
		||||
    color: #ffffff66;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.pair > *:first-child {
 | 
			
		||||
    margin-right: 1em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tx-link {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  display: inline;
 | 
			
		||||
  font-size: 28px;
 | 
			
		||||
  margin-bottom: 6px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.graph-wrapper {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  background: #181b2d;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  padding-bottom: 0;
 | 
			
		||||
 | 
			
		||||
  .above-bow {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 20px;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    margin: auto;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .overlaid {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
    font-size: 28px;
 | 
			
		||||
    max-width: 90%;
 | 
			
		||||
    margin: auto;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
 | 
			
		||||
    .opreturns {
 | 
			
		||||
      width: auto;
 | 
			
		||||
      margin: auto;
 | 
			
		||||
      table-layout: auto;
 | 
			
		||||
      background: #2d3348af;
 | 
			
		||||
      border-top-left-radius: 5px;
 | 
			
		||||
      border-top-right-radius: 5px;
 | 
			
		||||
 | 
			
		||||
      td {
 | 
			
		||||
        padding: 10px 10px;
 | 
			
		||||
 | 
			
		||||
        &.message {
 | 
			
		||||
          overflow: hidden;
 | 
			
		||||
          display: inline-block;
 | 
			
		||||
          vertical-align: bottom;
 | 
			
		||||
          text-overflow: ellipsis;
 | 
			
		||||
          white-space: nowrap;
 | 
			
		||||
          text-align: left;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -7,10 +7,9 @@ import {
 | 
			
		||||
  catchError,
 | 
			
		||||
  retryWhen,
 | 
			
		||||
  delay,
 | 
			
		||||
  map
 | 
			
		||||
} from 'rxjs/operators';
 | 
			
		||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from } from 'rxjs';
 | 
			
		||||
import { of, merge, Subscription, Observable, Subject, from } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { OpenGraphService } from 'src/app/services/opengraph.service';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
@ -37,6 +36,10 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
 | 
			
		||||
  showCpfpDetails = false;
 | 
			
		||||
  fetchCpfp$ = new Subject<string>();
 | 
			
		||||
  liquidUnblinding = new LiquidUnblinding();
 | 
			
		||||
  isLiquid = false;
 | 
			
		||||
  totalValue: number;
 | 
			
		||||
  opReturns: Vout[];
 | 
			
		||||
  extraData: 'none' | 'coinbase' | 'opreturn';
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
@ -49,7 +52,12 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.stateService.networkChanged$.subscribe(
 | 
			
		||||
      (network) => (this.network = network)
 | 
			
		||||
      (network) => {
 | 
			
		||||
        this.network = network;
 | 
			
		||||
        if (this.network === 'liquid' || this.network == 'liquidtestnet') {
 | 
			
		||||
          this.isLiquid = true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.fetchCpfpSubscription = this.fetchCpfp$
 | 
			
		||||
@ -152,6 +160,9 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
 | 
			
		||||
          this.tx.feePerVsize = tx.fee / (tx.weight / 4);
 | 
			
		||||
          this.isLoadingTx = false;
 | 
			
		||||
          this.error = undefined;
 | 
			
		||||
          this.totalValue = this.tx.vout.reduce((acc, v) => v.value + acc, 0);
 | 
			
		||||
          this.opReturns = this.getOpReturns(this.tx);
 | 
			
		||||
          this.extraData = this.chooseExtraData();
 | 
			
		||||
 | 
			
		||||
          if (!tx.status.confirmed && tx.firstSeen) {
 | 
			
		||||
            this.transactionTime = tx.firstSeen;
 | 
			
		||||
@ -217,6 +228,20 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
 | 
			
		||||
    return tx.vout.map((v: Vout) => v.value || 0).reduce((a: number, b: number) => a + b);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getOpReturns(tx: Transaction): Vout[] {
 | 
			
		||||
    return tx.vout.filter((v) => v.scriptpubkey_type === 'op_return' && v.scriptpubkey_asm !== 'OP_RETURN');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  chooseExtraData(): 'none' | 'opreturn' | 'coinbase' {
 | 
			
		||||
    if (this.isCoinbase(this.tx)) {
 | 
			
		||||
      return 'coinbase';
 | 
			
		||||
    } else if (this.opReturns?.length) {
 | 
			
		||||
      return 'opreturn';
 | 
			
		||||
    } else {
 | 
			
		||||
      return 'none';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.subscription.unsubscribe();
 | 
			
		||||
    this.fetchCpfpSubscription.unsubscribe();
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <table class="table table-borderless smaller-text table-sm table-tx-vin">
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <ng-template ngFor let-vin [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 > rowLimit) ? tx.vin.slice(0, rowLimit - 2) : tx.vin.slice(0, rowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
 | 
			
		||||
              <tr [ngClass]="{
 | 
			
		||||
                'assetBox': assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded,
 | 
			
		||||
                'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
 | 
			
		||||
@ -77,7 +77,7 @@
 | 
			
		||||
                          {{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
 | 
			
		||||
                        </ng-template>
 | 
			
		||||
                        <div>
 | 
			
		||||
                          <app-address-labels [vin]="vin" [channel]="channels && channels.inputs[i] || null"></app-address-labels>
 | 
			
		||||
                          <app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] || null"></app-address-labels>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </ng-template>
 | 
			
		||||
                    </ng-container>
 | 
			
		||||
@ -172,7 +172,7 @@
 | 
			
		||||
                    </span>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <app-address-labels [vout]="vout" [channel]="channels && channels.outputs[i] && channels.outputs[i].transaction_vout === vindex ? channels.outputs[i] : null"></app-address-labels>
 | 
			
		||||
                    <app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <ng-template #scriptpubkey_type>
 | 
			
		||||
                    <ng-template [ngIf]="vout.pegout" [ngIfElse]="defaultscriptpubkey_type">
 | 
			
		||||
@ -212,15 +212,15 @@
 | 
			
		||||
                  </ng-template>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="arrow-td">
 | 
			
		||||
                  <span *ngIf="!outspends[i] || vout.scriptpubkey_type === 'op_return' || vout.scriptpubkey_type === 'fee' ; else outspend" class="grey">
 | 
			
		||||
                  <span *ngIf="!tx._outspends || vout.scriptpubkey_type === 'op_return' || vout.scriptpubkey_type === 'fee' ; else outspend" class="grey">
 | 
			
		||||
                    <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
                  </span>
 | 
			
		||||
                  <ng-template #outspend>
 | 
			
		||||
                    <span *ngIf="!outspends[i][vindex] || !outspends[i][vindex].spent; else spent" class="green">
 | 
			
		||||
                    <span *ngIf="!tx._outspends[vindex] || !tx._outspends[vindex].spent; else spent" class="green">
 | 
			
		||||
                      <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <ng-template #spent>
 | 
			
		||||
                      <a *ngIf="outspends[i][vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, outspends[i][vindex].txid]" class="red">
 | 
			
		||||
                      <a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].txid]" class="red">
 | 
			
		||||
                        <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
                      </a>
 | 
			
		||||
                      <ng-template #outputNoTxId>
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() outputIndex: number;
 | 
			
		||||
  @Input() address: string = '';
 | 
			
		||||
  @Input() rowLimit = 12;
 | 
			
		||||
  @Input() channels: { inputs: any[], outputs: any[] };
 | 
			
		||||
 | 
			
		||||
  @Output() loadMore = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
@ -36,8 +35,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
  refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
 | 
			
		||||
  refreshChannels$: ReplaySubject<string[]> = new ReplaySubject();
 | 
			
		||||
  showDetails$ = new BehaviorSubject<boolean>(false);
 | 
			
		||||
  outspends: Outspend[][] = [];
 | 
			
		||||
  assetsMinimal: any;
 | 
			
		||||
  transactionsLength: number = 0;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
@ -47,7 +46,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
    private ref: ChangeDetectorRef,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
 | 
			
		||||
    this.stateService.networkChanged$.subscribe((network) => this.network = network);
 | 
			
		||||
 | 
			
		||||
@ -62,14 +61,17 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
        .pipe(
 | 
			
		||||
          switchMap((txIds) => this.apiService.getOutspendsBatched$(txIds)),
 | 
			
		||||
          tap((outspends: Outspend[][]) => {
 | 
			
		||||
            this.outspends = this.outspends.concat(outspends);
 | 
			
		||||
            const transactions = this.transactions.filter((tx) => !tx._outspends);
 | 
			
		||||
            outspends.forEach((outspend, i) => {
 | 
			
		||||
              transactions[i]._outspends = outspend;
 | 
			
		||||
            });
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
      this.stateService.utxoSpent$
 | 
			
		||||
        .pipe(
 | 
			
		||||
          tap((utxoSpent) => {
 | 
			
		||||
            for (const i in utxoSpent) {
 | 
			
		||||
              this.outspends[0][i] = {
 | 
			
		||||
              this.transactions[0]._outspends[i] = {
 | 
			
		||||
                spent: true,
 | 
			
		||||
                txid: utxoSpent[i].txid,
 | 
			
		||||
                vin: utxoSpent[i].vin,
 | 
			
		||||
@ -81,21 +83,23 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
          .pipe(
 | 
			
		||||
            filter(() => this.stateService.env.LIGHTNING),
 | 
			
		||||
            switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)),
 | 
			
		||||
            map((channels) => {
 | 
			
		||||
              this.channels = channels;
 | 
			
		||||
            tap((channels) => {
 | 
			
		||||
              const transactions = this.transactions.filter((tx) => !tx._channels);
 | 
			
		||||
              channels.forEach((channel, i) => {
 | 
			
		||||
                transactions[i]._channels = channel;
 | 
			
		||||
              });
 | 
			
		||||
            }),
 | 
			
		||||
          )
 | 
			
		||||
        ,
 | 
			
		||||
    ).subscribe(() => this.ref.markForCheck());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges() {
 | 
			
		||||
  ngOnChanges(): void {
 | 
			
		||||
    if (!this.transactions || !this.transactions.length) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.paginated) {
 | 
			
		||||
      this.outspends = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.transactionsLength = this.transactions.length;
 | 
			
		||||
    if (this.outputIndex) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        const assetBoxElements = document.getElementsByClassName('assetBox');
 | 
			
		||||
@ -126,14 +130,19 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
        tx['addressValue'] = addressIn - addressOut;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    const txIds = this.transactions.map((tx) => tx.txid);
 | 
			
		||||
    this.refreshOutspends$.next(txIds);
 | 
			
		||||
    if (!this.channels) {
 | 
			
		||||
      this.refreshChannels$.next(txIds);
 | 
			
		||||
    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);
 | 
			
		||||
      if (txIds.length) {
 | 
			
		||||
        this.refreshChannels$.next(txIds);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onScroll() {
 | 
			
		||||
  onScroll(): void {
 | 
			
		||||
    const scrollHeight = document.body.scrollHeight;
 | 
			
		||||
    const scrollTop = document.documentElement.scrollTop;
 | 
			
		||||
    if (scrollHeight > 0){
 | 
			
		||||
@ -148,11 +157,11 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
    return tx.vout.some((v: any) => v.value === undefined);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getTotalTxOutput(tx: Transaction) {
 | 
			
		||||
  getTotalTxOutput(tx: Transaction): number {
 | 
			
		||||
    return tx.vout.map((v: Vout) => v.value || 0).reduce((a: number, b: number) => a + b);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switchCurrency() {
 | 
			
		||||
  switchCurrency(): void {
 | 
			
		||||
    if (this.network === 'liquid' || this.network === 'liquidtestnet') {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -164,7 +173,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
    return tx.txid + tx.status.confirmed;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackByIndexFn(index: number) {
 | 
			
		||||
  trackByIndexFn(index: number): number {
 | 
			
		||||
    return index;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -177,7 +186,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
    return Math.pow(base, exponent);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleDetails() {
 | 
			
		||||
  toggleDetails(): void {
 | 
			
		||||
    if (this.showDetails$.value === true) {
 | 
			
		||||
      this.showDetails$.next(false);
 | 
			
		||||
    } else {
 | 
			
		||||
@ -185,7 +194,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadMoreInputs(tx: Transaction) {
 | 
			
		||||
  loadMoreInputs(tx: Transaction): void {
 | 
			
		||||
    tx['@vinLimit'] = false;
 | 
			
		||||
 | 
			
		||||
    this.electrsApiService.getTransaction$(tx.txid)
 | 
			
		||||
@ -196,7 +205,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.outspendsSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,44 @@
 | 
			
		||||
<svg *ngIf="inputs && outputs" class="bowtie" [attr.height]="(height + 10) + 'px'" [attr.width]="width + 'px'">
 | 
			
		||||
  <defs>
 | 
			
		||||
    <marker id="input-arrow" viewBox="-5 -5 10 10"
 | 
			
		||||
        refX="0" refY="0"
 | 
			
		||||
        markerUnits="strokeWidth"
 | 
			
		||||
        markerWidth="1.5" markerHeight="1"
 | 
			
		||||
        orient="auto">
 | 
			
		||||
      <path d="M -5 -5 L 0 0 L -5 5 L 1 5 L 1 -5 Z" stroke-width="0" [attr.fill]="gradient[0]"/>
 | 
			
		||||
    </marker>
 | 
			
		||||
    <marker id="output-arrow" viewBox="-5 -5 10 10"
 | 
			
		||||
        refX="0" refY="0"
 | 
			
		||||
        markerUnits="strokeWidth"
 | 
			
		||||
        markerWidth="1.5" markerHeight="1"
 | 
			
		||||
        orient="auto">
 | 
			
		||||
      <path d="M 1 -5 L 0 -5 L -5 0 L 0 5 L 1 5 Z" stroke-width="0" [attr.fill]="gradient[0]"/>
 | 
			
		||||
    </marker>
 | 
			
		||||
    <marker id="fee-arrow" viewBox="-5 -5 10 10"
 | 
			
		||||
        refX="0" refY="0"
 | 
			
		||||
        markerUnits="strokeWidth"
 | 
			
		||||
        markerWidth="1.5" markerHeight="1"
 | 
			
		||||
        orient="auto">
 | 
			
		||||
    </marker>
 | 
			
		||||
    <linearGradient id="input-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
      <stop offset="0%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
      <stop offset="100%" [attr.stop-color]="gradient[1]" />
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <linearGradient id="output-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
      <stop offset="0%" [attr.stop-color]="gradient[1]" />
 | 
			
		||||
      <stop offset="100%" [attr.stop-color]="gradient[0]" />
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <linearGradient id="fee-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
 | 
			
		||||
      <stop offset="0%" [attr.stop-color]="gradient[1]" />
 | 
			
		||||
      <stop offset="50%" [attr.stop-color]="gradient[1]" />
 | 
			
		||||
      <stop offset="100%" stop-color="transparent" />
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
  </defs>
 | 
			
		||||
  <path [attr.d]="middle.path" class="line middle" [style]="middle.style"/>
 | 
			
		||||
  <ng-container *ngFor="let input of inputs">
 | 
			
		||||
    <path [attr.d]="input.path" class="line {{input.class}}" [style]="input.style" attr.marker-start="url(#{{input.class}}-arrow)"/>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
  <ng-container *ngFor="let output of outputs">
 | 
			
		||||
    <path [attr.d]="output.path" class="line {{output.class}}" [style]="output.style" attr.marker-start="url(#{{output.class}}-arrow)" />
 | 
			
		||||
  </ng-container>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 2.0 KiB  | 
@ -0,0 +1,15 @@
 | 
			
		||||
.bowtie {
 | 
			
		||||
  .line {
 | 
			
		||||
    fill: none;
 | 
			
		||||
 | 
			
		||||
    &.input {
 | 
			
		||||
      stroke: url(#input-gradient);
 | 
			
		||||
    }
 | 
			
		||||
    &.output {
 | 
			
		||||
      stroke: url(#output-gradient);
 | 
			
		||||
    }
 | 
			
		||||
    &.fee {
 | 
			
		||||
      stroke: url(#fee-gradient);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,169 @@
 | 
			
		||||
import { Component, OnInit, Input, OnChanges } from '@angular/core';
 | 
			
		||||
import { Transaction } from '../../interfaces/electrs.interface';
 | 
			
		||||
 | 
			
		||||
interface SvgLine {
 | 
			
		||||
  path: string;
 | 
			
		||||
  style: string;
 | 
			
		||||
  class?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tx-bowtie-graph',
 | 
			
		||||
  templateUrl: './tx-bowtie-graph.component.html',
 | 
			
		||||
  styleUrls: ['./tx-bowtie-graph.component.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class TxBowtieGraphComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() tx: Transaction;
 | 
			
		||||
  @Input() network: string;
 | 
			
		||||
  @Input() width = 1200;
 | 
			
		||||
  @Input() height = 600;
 | 
			
		||||
  @Input() combinedWeight = 100;
 | 
			
		||||
  @Input() minWeight = 2; //
 | 
			
		||||
  @Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen.
 | 
			
		||||
 | 
			
		||||
  inputs: SvgLine[];
 | 
			
		||||
  outputs: SvgLine[];
 | 
			
		||||
  middle: SvgLine;
 | 
			
		||||
  isLiquid: boolean = false;
 | 
			
		||||
 | 
			
		||||
  gradientColors = {
 | 
			
		||||
    '': ['#9339f4', '#105fb0'],
 | 
			
		||||
    bisq: ['#9339f4', '#105fb0'],
 | 
			
		||||
    // liquid: ['#116761', '#183550'],
 | 
			
		||||
    liquid: ['#09a197', '#0f62af'],
 | 
			
		||||
    // 'liquidtestnet': ['#494a4a', '#272e46'],
 | 
			
		||||
    'liquidtestnet': ['#d2d2d2', '#979797'],
 | 
			
		||||
    // testnet: ['#1d486f', '#183550'],
 | 
			
		||||
    testnet: ['#4edf77', '#10a0af'],
 | 
			
		||||
    // signet: ['#6f1d5d', '#471850'],
 | 
			
		||||
    signet: ['#d24fc8', '#a84fd2'],
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  gradient: string[] = ['#105fb0', '#105fb0'];
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
 | 
			
		||||
    this.gradient = this.gradientColors[this.network];
 | 
			
		||||
    this.initGraph();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(): void {
 | 
			
		||||
    this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
 | 
			
		||||
    this.gradient = this.gradientColors[this.network];
 | 
			
		||||
    this.initGraph();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initGraph(): void {
 | 
			
		||||
    const totalValue = this.calcTotalValue(this.tx);
 | 
			
		||||
    const voutWithFee = this.tx.vout.map(v => { return { type: v.scriptpubkey_type === 'fee' ? 'fee' : 'output', value: v?.value }; });
 | 
			
		||||
 | 
			
		||||
    if (this.tx.fee && !this.isLiquid) {
 | 
			
		||||
      voutWithFee.unshift({ type: 'fee', value: this.tx.fee });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.inputs = this.initLines('in', this.tx.vin.map(v => { return {type: 'input', value: v?.prevout?.value }; }), totalValue, this.maxStrands);
 | 
			
		||||
    this.outputs = this.initLines('out', voutWithFee, totalValue, this.maxStrands);
 | 
			
		||||
 | 
			
		||||
    this.middle = {
 | 
			
		||||
      path: `M ${(this.width / 2) - 50} ${(this.height / 2) + 0.5} L ${(this.width / 2) + 50} ${(this.height / 2) + 0.5}`,
 | 
			
		||||
      style: `stroke-width: ${this.combinedWeight + 0.5}; stroke: ${this.gradient[1]}`
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  calcTotalValue(tx: Transaction): number {
 | 
			
		||||
    const totalOutput = this.tx.vout.reduce((acc, v) => (v.value == null ? 0 : v.value) + acc, 0);
 | 
			
		||||
    // simple sum of outputs + fee for bitcoin
 | 
			
		||||
    if (!this.isLiquid) {
 | 
			
		||||
      return this.tx.fee ? totalOutput + this.tx.fee : totalOutput;
 | 
			
		||||
    } else {
 | 
			
		||||
      const totalInput = this.tx.vin.reduce((acc, v) => (v?.prevout?.value == null ? 0 : v.prevout.value) + acc, 0);
 | 
			
		||||
      const confidentialInputCount = this.tx.vin.reduce((acc, v) => acc + (v?.prevout?.value == null ? 1 : 0), 0);
 | 
			
		||||
      const confidentialOutputCount = this.tx.vout.reduce((acc, v) => acc + (v.value == null ? 1 : 0), 0);
 | 
			
		||||
 | 
			
		||||
      // if there are unknowns on both sides, the total is indeterminate, so we'll just fudge it
 | 
			
		||||
      if (confidentialInputCount && confidentialOutputCount) {
 | 
			
		||||
        const knownInputCount = (tx.vin.length - confidentialInputCount) || 1;
 | 
			
		||||
        const knownOutputCount = (tx.vout.length - confidentialOutputCount) || 1;
 | 
			
		||||
        // assume confidential inputs/outputs have the same average value as the known ones
 | 
			
		||||
        const adjustedTotalInput = totalInput + ((totalInput / knownInputCount) * confidentialInputCount);
 | 
			
		||||
        const adjustedTotalOutput = totalOutput + ((totalOutput / knownOutputCount) * confidentialOutputCount);
 | 
			
		||||
        return Math.max(adjustedTotalInput, adjustedTotalOutput) || 1;
 | 
			
		||||
      } else {
 | 
			
		||||
        // otherwise knowing the actual total of one side suffices
 | 
			
		||||
        return Math.max(totalInput, totalOutput) || 1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initLines(side: 'in' | 'out', xputs: { type: string, value: number | void }[], total: number, maxVisibleStrands: number): SvgLine[] {
 | 
			
		||||
    const lines = [];
 | 
			
		||||
    let unknownCount = 0;
 | 
			
		||||
    let unknownTotal = total == null ? this.combinedWeight : total;
 | 
			
		||||
    xputs.forEach(put => {
 | 
			
		||||
      if (put.value == null) {
 | 
			
		||||
        unknownCount++;
 | 
			
		||||
      } else {
 | 
			
		||||
        unknownTotal -= put.value as number;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    const unknownShare = unknownTotal / unknownCount;
 | 
			
		||||
 | 
			
		||||
    // conceptual weights
 | 
			
		||||
    const weights = xputs.map((put): number => this.combinedWeight * (put.value == null ? unknownShare : put.value as number) / total);
 | 
			
		||||
    // actual displayed line thicknesses
 | 
			
		||||
    const minWeights = weights.map((w) => Math.max(this.minWeight - 1, w) + 1);
 | 
			
		||||
    const visibleStrands = Math.min(maxVisibleStrands, xputs.length);
 | 
			
		||||
    const visibleWeight = minWeights.slice(0, visibleStrands).reduce((acc, v) => v + acc, 0);
 | 
			
		||||
    const gaps = visibleStrands - 1;
 | 
			
		||||
 | 
			
		||||
    const innerTop = (this.height / 2) - (this.combinedWeight / 2);
 | 
			
		||||
    const innerBottom = innerTop + this.combinedWeight;
 | 
			
		||||
    // tracks the visual bottom of the endpoints of the previous line
 | 
			
		||||
    let lastOuter = 0;
 | 
			
		||||
    let lastInner = innerTop;
 | 
			
		||||
    // gap between strands
 | 
			
		||||
    const spacing = (this.height - visibleWeight) / gaps;
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < xputs.length; i++) {
 | 
			
		||||
      const weight = weights[i];
 | 
			
		||||
      const minWeight = minWeights[i];
 | 
			
		||||
      // set the vertical position of the (center of the) outer side of the line
 | 
			
		||||
      let outer = lastOuter + (minWeight / 2);
 | 
			
		||||
      const inner = Math.min(innerBottom + (minWeight / 2), Math.max(innerTop + (minWeight / 2), lastInner + (weight / 2)));
 | 
			
		||||
 | 
			
		||||
      // special case to center single input/outputs
 | 
			
		||||
      if (xputs.length === 1) {
 | 
			
		||||
        outer = (this.height / 2);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      lastOuter += minWeight + spacing;
 | 
			
		||||
      lastInner += weight;
 | 
			
		||||
      lines.push({
 | 
			
		||||
        path: this.makePath(side, outer, inner, minWeight),
 | 
			
		||||
        style: this.makeStyle(minWeight, xputs[i].type),
 | 
			
		||||
        class: xputs[i].type
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return lines;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  makePath(side: 'in' | 'out', outer: number, inner: number, weight: number): string {
 | 
			
		||||
    const start = side === 'in' ? (weight * 0.5) : this.width - (weight * 0.5);
 | 
			
		||||
    const center =  this.width / 2 + (side === 'in' ? -45 : 45 );
 | 
			
		||||
    const midpoint = (start + center) / 2;
 | 
			
		||||
    // correct for svg horizontal gradient bug
 | 
			
		||||
    if (Math.round(outer) === Math.round(inner)) {
 | 
			
		||||
      outer -= 1;
 | 
			
		||||
    }
 | 
			
		||||
    return `M ${start} ${outer} C ${midpoint} ${outer}, ${midpoint} ${inner}, ${center} ${inner}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  makeStyle(minWeight, type): string {
 | 
			
		||||
    if (type === 'fee') {
 | 
			
		||||
      return `stroke-width: ${minWeight}; stroke: url(#fee-gradient)`;
 | 
			
		||||
    } else {
 | 
			
		||||
      return `stroke-width: ${minWeight}`;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
import { IChannel } from './node-api.interface';
 | 
			
		||||
 | 
			
		||||
export interface Transaction {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  version: number;
 | 
			
		||||
@ -19,6 +21,13 @@ export interface Transaction {
 | 
			
		||||
  deleteAfter?: number;
 | 
			
		||||
  _unblinded?: any;
 | 
			
		||||
  _deduced?: boolean;
 | 
			
		||||
  _outspends?: Outspend[];
 | 
			
		||||
  _channels?: TransactionChannels;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionChannels {
 | 
			
		||||
  inputs: { [vin: number]: IChannel };
 | 
			
		||||
  outputs: { [vout: number]: IChannel };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Ancestor {
 | 
			
		||||
 | 
			
		||||
@ -189,3 +189,35 @@ export interface IOldestNodes {
 | 
			
		||||
  city?: any,
 | 
			
		||||
  country?: any,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IChannel {
 | 
			
		||||
  id: number;
 | 
			
		||||
  short_id: string;
 | 
			
		||||
  capacity: number;
 | 
			
		||||
  transaction_id: string;
 | 
			
		||||
  transaction_vout: number;
 | 
			
		||||
  closing_transaction_id: string;
 | 
			
		||||
  closing_reason: string;
 | 
			
		||||
  updated_at: string;
 | 
			
		||||
  created: string;
 | 
			
		||||
  status: number;
 | 
			
		||||
  node_left: Node,
 | 
			
		||||
  node_right: Node,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export interface INode {
 | 
			
		||||
  alias: string;
 | 
			
		||||
  public_key: string;
 | 
			
		||||
  channels: number;
 | 
			
		||||
  capacity: number;
 | 
			
		||||
  base_fee_mtokens: number;
 | 
			
		||||
  cltv_delta: number;
 | 
			
		||||
  fee_rate: number;
 | 
			
		||||
  is_disabled: boolean;
 | 
			
		||||
  max_htlc_mtokens: number;
 | 
			
		||||
  min_htlc_mtokens: number;
 | 
			
		||||
  updated_at: string;
 | 
			
		||||
  longitude: number;
 | 
			
		||||
  latitude: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@
 | 
			
		||||
    <a [routerLink]="['/lightning/node' | relativeUrl, channel.public_key]" >
 | 
			
		||||
      {{ channel.public_key | shortenString : 12 }}
 | 
			
		||||
    </a>
 | 
			
		||||
    <app-clipboard [text]="channel.node1_public_key"></app-clipboard>
 | 
			
		||||
    <app-clipboard [text]="channel.public_key"></app-clipboard>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="box-right">
 | 
			
		||||
    <div class="second-line">{{ channel.channels }} channels</div>
 | 
			
		||||
@ -51,4 +51,4 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
 | 
			
		||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
 | 
			
		||||
 | 
			
		||||
@ -58,7 +58,7 @@
 | 
			
		||||
      </table>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col-md map-col">
 | 
			
		||||
      <app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
 | 
			
		||||
      <app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" [placeholder]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -66,13 +66,13 @@
 | 
			
		||||
    <ng-container *ngIf="transactions$ | async as transactions">
 | 
			
		||||
      <ng-template [ngIf]="transactions[0]">
 | 
			
		||||
        <h3>Opening transaction</h3>
 | 
			
		||||
        <app-transactions-list [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [], outputs: [channel] }"></app-transactions-list>
 | 
			
		||||
        <app-transactions-list [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
      <ng-template [ngIf]="transactions[1]">
 | 
			
		||||
        <div class="closing-header">
 | 
			
		||||
          <h3 style="margin: 0;">Closing transaction</h3>  <app-closing-type [type]="channel.closing_reason"></app-closing-type>
 | 
			
		||||
        </div>
 | 
			
		||||
        <app-transactions-list [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [channel], outputs: [] }"></app-transactions-list>
 | 
			
		||||
        <app-transactions-list [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
			
		||||
import { forkJoin, Observable, of, share, zip } from 'rxjs';
 | 
			
		||||
import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { IChannel } from 'src/app/interfaces/node-api.interface';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
@ -62,10 +63,15 @@ export class ChannelComponent implements OnInit {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.transactions$ = this.channel$.pipe(
 | 
			
		||||
      switchMap((data) => {
 | 
			
		||||
      switchMap((channel: IChannel) => {
 | 
			
		||||
        return zip([
 | 
			
		||||
          data.transaction_id ? this.electrsApiService.getTransaction$(data.transaction_id) : of(null),
 | 
			
		||||
          data.closing_transaction_id ? this.electrsApiService.getTransaction$(data.closing_transaction_id) : of(null),
 | 
			
		||||
          channel.transaction_id ? this.electrsApiService.getTransaction$(channel.transaction_id) : of(null),
 | 
			
		||||
          channel.closing_transaction_id ? this.electrsApiService.getTransaction$(channel.closing_transaction_id).pipe(
 | 
			
		||||
            map((tx) => {
 | 
			
		||||
              tx._channels = { inputs: {0: channel}, outputs: {}};
 | 
			
		||||
              return tx;
 | 
			
		||||
            })
 | 
			
		||||
          ) : of(null),
 | 
			
		||||
        ]);
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -87,8 +87,6 @@
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #skeleton>
 | 
			
		||||
  <h2 class="float-left" i18n="lightning.channels">Channels</h2>
 | 
			
		||||
 | 
			
		||||
  <table class="table table-borderless">
 | 
			
		||||
  <ng-container *ngTemplateOutlet="tableHeader"></ng-container>
 | 
			
		||||
  <tbody>
 | 
			
		||||
 | 
			
		||||
@ -52,7 +52,7 @@
 | 
			
		||||
      </table>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col-md map-col">
 | 
			
		||||
      <app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
 | 
			
		||||
      <app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key" [fitContainer]="true" [placeholder]="true" [hasLocation]="!!node.as_number" (readyEvent)="onMapReady()"></app-nodes-channels-map>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<div class="container-xl" *ngIf="(node$ | async) as node">
 | 
			
		||||
  <h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5>
 | 
			
		||||
  <div class="title-container mb-2" *ngIf="!error">
 | 
			
		||||
    <h1 class="mb-0">{{ node.alias }}</h1>
 | 
			
		||||
    <h1 class="mb-0 text-truncate">{{ node.alias }}</h1>
 | 
			
		||||
    <span class="tx-link">
 | 
			
		||||
      <a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
 | 
			
		||||
        {{ node.public_key | shortenString : publicKeySize }}
 | 
			
		||||
@ -131,7 +131,6 @@
 | 
			
		||||
      <app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <h2 i18n="lightning.active-channels-map">Active channels map</h2>
 | 
			
		||||
    <app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels>
 | 
			
		||||
 | 
			
		||||
    <div class="d-flex justify-content-between">
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ export class NodesChannelsMap implements OnInit {
 | 
			
		||||
  @Input() channel: any[] = [];
 | 
			
		||||
  @Input() fitContainer = false;
 | 
			
		||||
  @Input() hasLocation = true;
 | 
			
		||||
  @Input() placeholder = false;
 | 
			
		||||
  @Output() readyEvent = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
  channelsObservable: Observable<any>; 
 | 
			
		||||
@ -201,11 +202,26 @@ export class NodesChannelsMap implements OnInit {
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(nodes, channels) {
 | 
			
		||||
    let title: object;
 | 
			
		||||
    if (channels.length === 0) {
 | 
			
		||||
    if (channels.length === 0 && !this.placeholder) {
 | 
			
		||||
      this.chartOptions = null;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // empty map fallback
 | 
			
		||||
    if (channels.length === 0 && this.placeholder) {
 | 
			
		||||
      title = {
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: 'white',
 | 
			
		||||
          fontSize: 18
 | 
			
		||||
        },
 | 
			
		||||
        text: $localize`No geolocation data available`,
 | 
			
		||||
        left: 'center',
 | 
			
		||||
        top: 'center'
 | 
			
		||||
      };
 | 
			
		||||
      this.zoom = 1.5;
 | 
			
		||||
      this.center = [0, 20];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
      silent: this.style === 'widget',
 | 
			
		||||
      title: title ?? undefined,
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,9 @@
 | 
			
		||||
<div *ngIf="channelsObservable$ | async" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
 | 
			
		||||
<div *ngIf="channelsObservable$ | async" style="min-height: 455px">
 | 
			
		||||
  <h2 i18n="lightning.active-channels-map">Active channels map</h2>
 | 
			
		||||
  <div echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div *ngIf="isLoading" class="text-center loading-spinner">
 | 
			
		||||
  <div class="spinner-border text-light"></div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,9 @@
 | 
			
		||||
.loading-spinner {
 | 
			
		||||
  min-height: 455px;
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.spinner-border {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  top: 225px;
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { formatNumber } from '@angular/common';
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnInit } from '@angular/core';
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts';
 | 
			
		||||
import { Observable, tap } from 'rxjs';
 | 
			
		||||
import { Observable, share, switchMap, tap } from 'rxjs';
 | 
			
		||||
import { lerpColor } from 'src/app/shared/graphs.utils';
 | 
			
		||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
 | 
			
		||||
import { LightningApiService } from '../lightning-api.service';
 | 
			
		||||
@ -25,7 +25,7 @@ export class NodeChannels implements OnChanges {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  channelsObservable$: Observable<any>;
 | 
			
		||||
  isLoading: true;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string,
 | 
			
		||||
@ -41,9 +41,20 @@ export class NodeChannels implements OnChanges {
 | 
			
		||||
 | 
			
		||||
    this.channelsObservable$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey, -1, 'active')
 | 
			
		||||
      .pipe(
 | 
			
		||||
        tap((response) => {
 | 
			
		||||
          const biggestCapacity = response.body[0].capacity;
 | 
			
		||||
          this.prepareChartOptions(response.body.map(channel => {
 | 
			
		||||
        switchMap((response) => {
 | 
			
		||||
          this.isLoading = true;
 | 
			
		||||
          if ((response.body?.length ?? 0) <= 0) {
 | 
			
		||||
            return [];
 | 
			
		||||
          }
 | 
			
		||||
          return [response.body];
 | 
			
		||||
        }),
 | 
			
		||||
        tap((body: any[]) => {
 | 
			
		||||
          if (body.length === 0) {
 | 
			
		||||
            this.isLoading = false;
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          const biggestCapacity = body[0].capacity;
 | 
			
		||||
          this.prepareChartOptions(body.map(channel => {
 | 
			
		||||
            return {
 | 
			
		||||
              name: channel.node.alias,
 | 
			
		||||
              value: channel.capacity,
 | 
			
		||||
@ -54,7 +65,9 @@ export class NodeChannels implements OnChanges {
 | 
			
		||||
              }
 | 
			
		||||
            };
 | 
			
		||||
          }));
 | 
			
		||||
        })
 | 
			
		||||
          this.isLoading = false;
 | 
			
		||||
        }),
 | 
			
		||||
        share(),
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -242,12 +242,12 @@ export class ApiService {
 | 
			
		||||
    return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/enterprise/info/` + name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getChannelByTxIds$(txIds: string[]): Observable<{ inputs: any[], outputs: any[] }> {
 | 
			
		||||
  getChannelByTxIds$(txIds: string[]): Observable<any[]> {
 | 
			
		||||
    let params = new HttpParams();
 | 
			
		||||
    txIds.forEach((txId: string) => {
 | 
			
		||||
      params = params.append('txId[]', txId);
 | 
			
		||||
    });
 | 
			
		||||
    return this.httpClient.get<{ inputs: any[], outputs: any[] }>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params });
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  lightningSearch$(searchText: string): Observable<any[]> {
 | 
			
		||||
 | 
			
		||||
@ -153,7 +153,12 @@ export class StateService {
 | 
			
		||||
    if (this.env.BASE_MODULE !== 'mempool' && this.env.BASE_MODULE !== 'liquid') {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const networkMatches = url.match(/^\/(bisq|testnet|liquidtestnet|liquid|signet)/);
 | 
			
		||||
    // horrible network regex breakdown:
 | 
			
		||||
    // /^\/                                         starts with a forward slash...
 | 
			
		||||
    // (?:[a-z]{2}(?:-[A-Z]{2})?\/)?                optional locale prefix (non-capturing)
 | 
			
		||||
    // (?:preview\/)?                               optional "preview" prefix (non-capturing)
 | 
			
		||||
    // (bisq|testnet|liquidtestnet|liquid|signet)/  network string (captured as networkMatches[1])
 | 
			
		||||
    const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(bisq|testnet|liquidtestnet|liquid|signet)/);
 | 
			
		||||
    switch (networkMatches && networkMatches[1]) {
 | 
			
		||||
      case 'liquid':
 | 
			
		||||
        if (this.network !== 'liquid') {
 | 
			
		||||
 | 
			
		||||
@ -63,6 +63,7 @@ import { StatusViewComponent } from '../components/status-view/status-view.compo
 | 
			
		||||
import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
 | 
			
		||||
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
 | 
			
		||||
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
 | 
			
		||||
import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component';
 | 
			
		||||
import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component';
 | 
			
		||||
import { TrademarkPolicyComponent } from '../components/trademark-policy/trademark-policy.component';
 | 
			
		||||
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
 | 
			
		||||
@ -138,6 +139,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
 | 
			
		||||
    StatusViewComponent,
 | 
			
		||||
    FeesBoxComponent,
 | 
			
		||||
    DifficultyComponent,
 | 
			
		||||
    TxBowtieGraphComponent,
 | 
			
		||||
    TermsOfServiceComponent,
 | 
			
		||||
    PrivacyPolicyComponent,
 | 
			
		||||
    TrademarkPolicyComponent,
 | 
			
		||||
@ -242,6 +244,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
 | 
			
		||||
    StatusViewComponent,
 | 
			
		||||
    FeesBoxComponent,
 | 
			
		||||
    DifficultyComponent,
 | 
			
		||||
    TxBowtieGraphComponent,
 | 
			
		||||
    TermsOfServiceComponent,
 | 
			
		||||
    PrivacyPolicyComponent,
 | 
			
		||||
    TrademarkPolicyComponent,
 | 
			
		||||
 | 
			
		||||
@ -1287,9 +1287,9 @@ case $OS in
 | 
			
		||||
        osPackageInstall ${CLN_PKG}
 | 
			
		||||
 | 
			
		||||
        echo "[*] Installing Core Lightning mainnet Cronjob"
 | 
			
		||||
        crontab_cln+='@reboot sleep 30 ; screen -dmS main lightningd --alias `hostname` --bitcoin-datadir /bitcoin\n'
 | 
			
		||||
        crontab_cln+='@reboot sleep 60 ; screen -dmS sig lightningd --alias `hostname` --bitcoin-datadir /bitcoin --network signet\n'
 | 
			
		||||
        crontab_cln+='@reboot sleep 60 ; screen -dmS main lightningd --alias `hostname` --bitcoin-datadir /bitcoin\n'
 | 
			
		||||
        crontab_cln+='@reboot sleep 90 ; screen -dmS tes lightningd --alias `hostname` --bitcoin-datadir /bitcoin --network testnet\n'
 | 
			
		||||
        crontab_cln+='@reboot sleep 120 ; screen -dmS sig lightningd --alias `hostname` --bitcoin-datadir /bitcoin --network signet\n'
 | 
			
		||||
        echo "${crontab_cln}" | crontab -u "${CLN_USER}" -
 | 
			
		||||
    ;;
 | 
			
		||||
    Debian)
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,10 @@ ELEMENTS_RPC_USER=$(grep '^rpcuser' /elements/elements.conf | cut -d '=' -f2)
 | 
			
		||||
ELEMENTS_RPC_PASS=$(grep '^rpcpassword' /elements/elements.conf | cut -d '=' -f2)
 | 
			
		||||
 | 
			
		||||
# get mysql credentials
 | 
			
		||||
. /mempool/mysql_credentials
 | 
			
		||||
MYSQL_CRED_FILE=${HOME}/mempool/mysql_credentials
 | 
			
		||||
if [ -f "${MYSQL_CRED_FILE}" ];then
 | 
			
		||||
    . ${MYSQL_CRED_FILE}
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ -f "${LOCKFILE}" ];then
 | 
			
		||||
    echo "upgrade already running? check lockfile ${LOCKFILE}"
 | 
			
		||||
@ -63,6 +66,19 @@ build_frontend()
 | 
			
		||||
    npm run build || exit 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build_unfurler()
 | 
			
		||||
{
 | 
			
		||||
    local site="$1"
 | 
			
		||||
    echo "[*] Building unfurler for ${site}"
 | 
			
		||||
    [ -z "${HASH}" ] && exit 1
 | 
			
		||||
    cd "$HOME/${site}/unfurler" || exit 1
 | 
			
		||||
    if [ ! -e "config.json" ];then
 | 
			
		||||
        cp "${HOME}/mempool/production/unfurler-config.${site}.json" "config.json"
 | 
			
		||||
    fi
 | 
			
		||||
    PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm install || exit 1
 | 
			
		||||
    npm run build || exit 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build_backend()
 | 
			
		||||
{
 | 
			
		||||
    local site="$1"
 | 
			
		||||
@ -128,6 +144,11 @@ for repo in $backend_repos;do
 | 
			
		||||
    update_repo "${repo}"
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
# build unfurlers
 | 
			
		||||
for repo in mainnet liquid;do
 | 
			
		||||
    build_unfurler "${repo}"
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
# build backends
 | 
			
		||||
for repo in $backend_repos;do
 | 
			
		||||
    build_backend "${repo}"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										49
									
								
								production/mempool-config.mainnet-lightning.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								production/mempool-config.mainnet-lightning.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
{
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "NETWORK": "mainnet",
 | 
			
		||||
    "BACKEND": "esplora",
 | 
			
		||||
    "HTTP_PORT": 8993,
 | 
			
		||||
    "INDEXING_BLOCKS_AMOUNT": 0,
 | 
			
		||||
    "API_URL_PREFIX": "/api/v1/"
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG": {
 | 
			
		||||
    "MIN_PRIORITY": "debug"
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "PORT": 8332,
 | 
			
		||||
    "USERNAME": "__BITCOIN_RPC_USER__",
 | 
			
		||||
    "PASSWORD": "__BITCOIN_RPC_PASS__"
 | 
			
		||||
  },
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": "http://127.0.0.1:4000"
 | 
			
		||||
  },
 | 
			
		||||
  "LIGHTNING": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "BACKEND": "cln",
 | 
			
		||||
    "GRAPH_REFRESH_INTERVAL": 60,
 | 
			
		||||
    "TOPOLOGY_FOLDER": "/cln/topology/output"
 | 
			
		||||
  },
 | 
			
		||||
  "LND": {
 | 
			
		||||
    "REST_API_URL": "https://127.0.0.1:8888",
 | 
			
		||||
    "TLS_CERT_PATH": "/lnd/.lnd/tls.cert",
 | 
			
		||||
    "MACAROON_PATH": "/lnd/.lnd/data/chain/bitcoin/mainnet/readonly.macaroon"
 | 
			
		||||
  },
 | 
			
		||||
  "CLIGHTNING": {
 | 
			
		||||
    "SOCKET": "/cln/.lightning/bitcoin/lightning-rpc"
 | 
			
		||||
  },
 | 
			
		||||
  "MAXMIND": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoIP2-City.mmdb"
 | 
			
		||||
  },
 | 
			
		||||
  "STATISTICS": {
 | 
			
		||||
    "ENABLED": false
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "HOST": "127.0.0.1",
 | 
			
		||||
    "PORT": 3306,
 | 
			
		||||
    "DATABASE": "mempool_mainnet_lightning",
 | 
			
		||||
    "USERNAME": "mempool_mainnet_lightning",
 | 
			
		||||
    "PASSWORD": "mempool_mainnet_lightning"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								production/mempool-config.signet-lightning.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								production/mempool-config.signet-lightning.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
{
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "NETWORK": "signet",
 | 
			
		||||
    "BACKEND": "esplora",
 | 
			
		||||
    "HTTP_PORT": 8991,
 | 
			
		||||
    "INDEXING_BLOCKS_AMOUNT": 0,
 | 
			
		||||
    "API_URL_PREFIX": "/api/v1/"
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG": {
 | 
			
		||||
    "MIN_PRIORITY": "debug"
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "PORT": 38332,
 | 
			
		||||
    "USERNAME": "__BITCOIN_RPC_USER__",
 | 
			
		||||
    "PASSWORD": "__BITCOIN_RPC_PASS__"
 | 
			
		||||
  },
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": "http://127.0.0.1:4003"
 | 
			
		||||
  },
 | 
			
		||||
  "LIGHTNING": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "BACKEND": "cln",
 | 
			
		||||
    "GRAPH_REFRESH_INTERVAL": 60,
 | 
			
		||||
    "TOPOLOGY_FOLDER": ""
 | 
			
		||||
  },
 | 
			
		||||
  "CLIGHTNING": {
 | 
			
		||||
    "SOCKET": "/cln/.lightning/signet/lightning-rpc"
 | 
			
		||||
  },
 | 
			
		||||
  "MAXMIND": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoIP2-City.mmdb"
 | 
			
		||||
  },
 | 
			
		||||
  "STATISTICS": {
 | 
			
		||||
    "ENABLED": false
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "HOST": "127.0.0.1",
 | 
			
		||||
    "PORT": 3306,
 | 
			
		||||
    "USERNAME": "mempool_signet_lightning",
 | 
			
		||||
    "PASSWORD": "mempool_signet_lightning",
 | 
			
		||||
    "DATABASE": "mempool_signet_lightning"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								production/mempool-config.testnet-lightning.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								production/mempool-config.testnet-lightning.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
{
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "NETWORK": "testnet",
 | 
			
		||||
    "BACKEND": "esplora",
 | 
			
		||||
    "HTTP_PORT": 8992,
 | 
			
		||||
    "INDEXING_BLOCKS_AMOUNT": 0,
 | 
			
		||||
    "API_URL_PREFIX": "/api/v1/"
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG": {
 | 
			
		||||
    "MIN_PRIORITY": "debug"
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "PORT": 18332,
 | 
			
		||||
    "USERNAME": "__BITCOIN_RPC_USER__",
 | 
			
		||||
    "PASSWORD": "__BITCOIN_RPC_PASS__"
 | 
			
		||||
  },
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": "http://127.0.0.1:4002"
 | 
			
		||||
  },
 | 
			
		||||
  "LIGHTNING": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "BACKEND": "cln",
 | 
			
		||||
    "GRAPH_REFRESH_INTERVAL": 60,
 | 
			
		||||
    "TOPOLOGY_FOLDER": ""
 | 
			
		||||
  },
 | 
			
		||||
  "CLIGHTNING": {
 | 
			
		||||
    "SOCKET": "/cln/.lightning/testnet/lightning-rpc"
 | 
			
		||||
  },
 | 
			
		||||
  "MAXMIND": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoIP2-City.mmdb"
 | 
			
		||||
  },
 | 
			
		||||
  "STATISTICS": {
 | 
			
		||||
    "ENABLED": false
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
    "ENABLED": true,
 | 
			
		||||
    "HOST": "127.0.0.1",
 | 
			
		||||
    "PORT": 3306,
 | 
			
		||||
    "USERNAME": "mempool_testnet_lightning",
 | 
			
		||||
    "PASSWORD": "mempool_testnet_lightning",
 | 
			
		||||
    "DATABASE": "mempool_testnet_lightning"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,2 +1,8 @@
 | 
			
		||||
#!/usr/bin/env zsh
 | 
			
		||||
killall sh node
 | 
			
		||||
killall sh
 | 
			
		||||
killall node
 | 
			
		||||
killall chrome
 | 
			
		||||
killall xinit
 | 
			
		||||
for pid in `ps uaxww|grep warmer|grep zsh|awk '{print $2}'`;do
 | 
			
		||||
    kill $pid
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,29 @@
 | 
			
		||||
export NVM_DIR="$HOME/.nvm"
 | 
			
		||||
source "$NVM_DIR/nvm.sh"
 | 
			
		||||
 | 
			
		||||
# start all mempool backends that exist
 | 
			
		||||
for site in mainnet mainnet-lightning testnet testnet-lightning signet signet-lightning bisq liquid liquidtestnet;do
 | 
			
		||||
    cd "${HOME}/${site}/backend/" && \
 | 
			
		||||
    echo "starting mempool backend: ${site}" && \
 | 
			
		||||
    screen -dmS "${site}" sh -c 'while true;do npm run start-production;sleep 1;done'
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
# only start unfurler if GPU present
 | 
			
		||||
if pciconf -lv|grep -i nvidia >/dev/null 2>&1;then
 | 
			
		||||
    export DISPLAY=:0
 | 
			
		||||
    screen -dmS x startx
 | 
			
		||||
    sleep 3
 | 
			
		||||
    for site in mainnet liquid;do
 | 
			
		||||
        cd "$HOME/${site}/unfurler" && \
 | 
			
		||||
        echo "starting mempool unfurler: ${site}" && \
 | 
			
		||||
        screen -dmS "unfurler-${site}" sh -c 'while true;do npm run unfurler;sleep 2;done'
 | 
			
		||||
    done
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# start nginx warm cacher
 | 
			
		||||
for site in mainnet;do
 | 
			
		||||
    echo "starting mempool cache warmer: ${site}"
 | 
			
		||||
    screen -dmS "warmer-${site}" $HOME/mempool/production/nginx-cache-warmer
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
exit 0
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,12 @@
 | 
			
		||||
hostname=$(hostname)
 | 
			
		||||
slugs=(`curl -sSL https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json | jq -r '.slugs[]'`)
 | 
			
		||||
 | 
			
		||||
warm()
 | 
			
		||||
{
 | 
			
		||||
	echo "$1"
 | 
			
		||||
	curl -i -s "$1" | head -1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
while true
 | 
			
		||||
do for url in / \
 | 
			
		||||
	'/api/v1/blocks' \
 | 
			
		||||
@ -81,14 +87,14 @@ do for url in / \
 | 
			
		||||
	'/api/v1/lightning/channels-geo?style=graph' \
 | 
			
		||||
 | 
			
		||||
	do
 | 
			
		||||
		curl -s "https://${hostname}${url}" >/dev/null
 | 
			
		||||
		warm "https://${hostname}${url}"
 | 
			
		||||
	done
 | 
			
		||||
 | 
			
		||||
	for slug in $slugs
 | 
			
		||||
	do
 | 
			
		||||
		curl -s "https://${hostname}/api/v1/mining/pool/${slug}" >/dev/null
 | 
			
		||||
		curl -s "https://${hostname}/api/v1/mining/pool/${slug}/hashrate" >/dev/null
 | 
			
		||||
		curl -s "https://${hostname}/api/v1/mining/pool/${slug}/blocks" >/dev/null
 | 
			
		||||
		warm "https://${hostname}/api/v1/mining/pool/${slug}"
 | 
			
		||||
		warm "https://${hostname}/api/v1/mining/pool/${slug}/hashrate"
 | 
			
		||||
		warm "https://${hostname}/api/v1/mining/pool/${slug}/blocks"
 | 
			
		||||
	done
 | 
			
		||||
 | 
			
		||||
	sleep 10
 | 
			
		||||
 | 
			
		||||
@ -1,62 +0,0 @@
 | 
			
		||||
#!/usr/bin/env zsh
 | 
			
		||||
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:$HOME/bin
 | 
			
		||||
HOSTNAME=$(hostname)
 | 
			
		||||
LOCATION=$(hostname|cut -d . -f2)
 | 
			
		||||
LOCKFILE="${HOME}/lock"
 | 
			
		||||
REF=$(echo "${1:=origin/master}"|sed -e 's!:!/!')
 | 
			
		||||
 | 
			
		||||
if [ -f "${LOCKFILE}" ];then
 | 
			
		||||
    echo "upgrade already running? check lockfile ${LOCKFILE}"
 | 
			
		||||
    exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# on exit, remove lockfile but preserve exit code
 | 
			
		||||
trap "rv=\$?; rm -f "${LOCKFILE}"; exit \$rv" INT TERM EXIT
 | 
			
		||||
 | 
			
		||||
# create lockfile
 | 
			
		||||
touch "${LOCKFILE}"
 | 
			
		||||
 | 
			
		||||
# notify logged in users
 | 
			
		||||
echo "Upgrading unfurler to ${REF}" | wall
 | 
			
		||||
 | 
			
		||||
update_repo()
 | 
			
		||||
{
 | 
			
		||||
    echo "[*] Upgrading unfurler to ${REF}"
 | 
			
		||||
    cd "$HOME/unfurl/unfurler" || exit 1
 | 
			
		||||
 | 
			
		||||
    git fetch origin || exit 1
 | 
			
		||||
    for remote in origin;do
 | 
			
		||||
        git remote add "${remote}" "https://github.com/${remote}/mempool" >/dev/null 2>&1
 | 
			
		||||
        git fetch "${remote}" || exit 1
 | 
			
		||||
    done
 | 
			
		||||
 | 
			
		||||
    if [ $(git tag -l "${REF}") ];then
 | 
			
		||||
        git reset --hard "tags/${REF}" || exit 1
 | 
			
		||||
    elif [ $(git branch -r -l "origin/${REF}") ];then
 | 
			
		||||
        git reset --hard "origin/${REF}" || exit 1
 | 
			
		||||
    else
 | 
			
		||||
        git reset --hard "${REF}" || exit 1
 | 
			
		||||
    fi
 | 
			
		||||
    export HASH=$(git rev-parse HEAD)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build_backend()
 | 
			
		||||
{
 | 
			
		||||
    echo "[*] Building backend for unfurler"
 | 
			
		||||
    [ -z "${HASH}" ] && exit 1
 | 
			
		||||
    cd "$HOME/unfurl/unfurler" || exit 1
 | 
			
		||||
    if [ ! -e "config.json" ];then
 | 
			
		||||
        cp "${HOME}/unfurl/production/mempool-config.unfurl.json" "config.json"
 | 
			
		||||
    fi
 | 
			
		||||
    npm install || exit 1
 | 
			
		||||
    npm run build || exit 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
update_repo
 | 
			
		||||
build_backend
 | 
			
		||||
 | 
			
		||||
# notify everyone
 | 
			
		||||
echo "${HOSTNAME} unfurl updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general mempool.dev
 | 
			
		||||
echo "${HOSTNAME} unfurl updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general "mempool.ops.${LOCATION}"
 | 
			
		||||
 | 
			
		||||
exit 0
 | 
			
		||||
@ -1,2 +0,0 @@
 | 
			
		||||
#!/usr/bin/env zsh
 | 
			
		||||
killall sh node
 | 
			
		||||
@ -1,6 +0,0 @@
 | 
			
		||||
#!/usr/bin/env zsh
 | 
			
		||||
export NVM_DIR="$HOME/.nvm"
 | 
			
		||||
source "$NVM_DIR/nvm.sh"
 | 
			
		||||
 | 
			
		||||
cd "${HOME}/unfurl/unfurler/" && \
 | 
			
		||||
screen -dmS "unfurl" sh -c 'while true;do npm run start-production;sleep 1;done'
 | 
			
		||||
							
								
								
									
										17
									
								
								production/unfurler-config.liquid.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								production/unfurler-config.liquid.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
{
 | 
			
		||||
  "SERVER": {
 | 
			
		||||
    "HOST": "https://liquid.network",
 | 
			
		||||
    "HTTP_PORT": 8002
 | 
			
		||||
  },
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "HTTP_HOST": "https://liquid.network",
 | 
			
		||||
    "HTTP_PORT": 443,
 | 
			
		||||
    "NETWORK": "liquid"
 | 
			
		||||
  },
 | 
			
		||||
  "PUPPETEER": {
 | 
			
		||||
    "CLUSTER_SIZE": 8,
 | 
			
		||||
    "EXEC_PATH": "/usr/local/bin/chrome",
 | 
			
		||||
    "MAX_PAGE_AGE": 86400,
 | 
			
		||||
    "RENDER_TIMEOUT": 3000
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								production/unfurler-config.mainnet.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								production/unfurler-config.mainnet.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
{
 | 
			
		||||
  "SERVER": {
 | 
			
		||||
    "HOST": "https://mempool.space",
 | 
			
		||||
    "HTTP_PORT": 8001
 | 
			
		||||
  },
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "HTTP_HOST": "https://mempool.space",
 | 
			
		||||
    "HTTP_PORT": 443,
 | 
			
		||||
    "NETWORK": "bitcoin"
 | 
			
		||||
  },
 | 
			
		||||
  "PUPPETEER": {
 | 
			
		||||
    "CLUSTER_SIZE": 8,
 | 
			
		||||
    "EXEC_PATH": "/usr/local/bin/chrome",
 | 
			
		||||
    "MAX_PAGE_AGE": 86400,
 | 
			
		||||
    "RENDER_TIMEOUT": 3000
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -11,7 +11,7 @@
 | 
			
		||||
    "tsc": "./node_modules/typescript/bin/tsc",
 | 
			
		||||
    "build": "npm run tsc",
 | 
			
		||||
    "start": "node --max-old-space-size=2048 dist/index.js",
 | 
			
		||||
    "start-production": "node --max-old-space-size=4096 dist/index.js",
 | 
			
		||||
    "unfurler": "node --max-old-space-size=4096 dist/index.js",
 | 
			
		||||
    "lint": "./node_modules/.bin/eslint . --ext .ts",
 | 
			
		||||
    "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
 | 
			
		||||
    "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
 | 
			
		||||
 | 
			
		||||
@ -41,6 +41,6 @@
 | 
			
		||||
    "--use-mock-keychain",
 | 
			
		||||
    "--ignore-gpu-blacklist",
 | 
			
		||||
    "--ignore-gpu-blocklist",
 | 
			
		||||
    "--use-gl=swiftshader"
 | 
			
		||||
    "--use-gl=egl"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user