diff --git a/backend/src/logger.ts b/backend/src/logger.ts index ea7e8cd3d..63774d513 100644 --- a/backend/src/logger.ts +++ b/backend/src/logger.ts @@ -74,7 +74,7 @@ class Logger { private getNetwork(): string { if (config.LIGHTNING.ENABLED) { - return 'lightning'; + return config.MEMPOOL.NETWORK === 'mainnet' ? 'lightning' : `${config.MEMPOOL.NETWORK}-lightning`; } if (config.BISQ.ENABLED) { return 'bisq'; diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index e71b12375..dd958a6e3 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -20,6 +20,10 @@ 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(); } diff --git a/frontend/src/app/components/transaction/transaction-preview.component.html b/frontend/src/app/components/transaction/transaction-preview.component.html index f9f62c417..44be1dcfa 100644 --- a/frontend/src/app/components/transaction/transaction-preview.component.html +++ b/frontend/src/app/components/transaction/transaction-preview.component.html @@ -2,6 +2,9 @@

Transaction

+ + {{txId.slice(0,-4)}}{{txId.slice(-4)}} +
@@ -13,104 +16,50 @@
- - {{ txId }} - +
+ + Confidential + + + + + ‎{{ (tx.status.confirmed ? tx.status.block_time : transactionTime) * 1000 | date:'yyyy-MM-dd HH:mm' }} + Fee {{ tx.fee | number }} sat +
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Timestamp - ‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }} -
First seen - ‎{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }} - ?
Amount - Confidential - - - -
Size
Weight
Inputs{{ tx.vin.length }}Coinbase
+
+ +
+

+ + +

+

+ {{ tx.feePerVsize | feeRounding }} sat/vB +

- -
- - - - - - - - - - - +
+ +
Fee{{ tx.fee | number }} sat
Fee rate - {{ tx.feePerVsize | feeRounding }} sat/vB - -   - - -
+ - - + + - - - - - - - - - - - - - - -
Effective fee rate -
- {{ tx.effectiveFeePerVsize | feeRounding }} sat/vB - - - -
-
Coinbase{{ tx.vin[0].scriptsig | hex2ascii }}
Virtual size
Locktime
Outputs{{ tx.vout.length }}
+ + + + + + + + + + + +
OP_RETURN{{ vout.scriptpubkey_asm | hex2ascii }}
+
diff --git a/frontend/src/app/components/transaction/transaction-preview.component.scss b/frontend/src/app/components/transaction/transaction-preview.component.scss index a8e2a0acb..7aefe0063 100644 --- a/frontend/src/app/components/transaction/transaction-preview.component.scss +++ b/frontend/src/app/components/transaction/transaction-preview.component.scss @@ -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; + } + } + } + } +} diff --git a/frontend/src/app/components/transaction/transaction-preview.component.ts b/frontend/src/app/components/transaction/transaction-preview.component.ts index 05ce623fb..d30789f6b 100644 --- a/frontend/src/app/components/transaction/transaction-preview.component.ts +++ b/frontend/src/app/components/transaction/transaction-preview.component.ts @@ -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(); 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(); diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html new file mode 100644 index 000000000..c4771c58c --- /dev/null +++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.scss b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.scss new file mode 100644 index 000000000..6de41b95f --- /dev/null +++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.scss @@ -0,0 +1,15 @@ +.bowtie { + .line { + fill: none; + + &.input { + stroke: url(#input-gradient); + } + &.output { + stroke: url(#output-gradient); + } + &.fee { + stroke: url(#fee-gradient); + } + } +} diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts new file mode 100644 index 000000000..427a282a9 --- /dev/null +++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts @@ -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}`; + } + } +} diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html index 382fffd47..37918b8df 100644 --- a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html @@ -4,7 +4,7 @@ {{ channel.public_key | shortenString : 12 }} - +
{{ channel.channels }} channels
@@ -51,4 +51,4 @@
-{{ i }} blocks \ No newline at end of file +{{ i }} blocks diff --git a/frontend/src/app/lightning/channel/channel-preview.component.html b/frontend/src/app/lightning/channel/channel-preview.component.html index c98929931..a847975c2 100644 --- a/frontend/src/app/lightning/channel/channel-preview.component.html +++ b/frontend/src/app/lightning/channel/channel-preview.component.html @@ -58,7 +58,7 @@
- +
diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index 6d0439e76..50bb52eaa 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -11,6 +11,7 @@ Inactive Active Closed +
diff --git a/frontend/src/app/lightning/channel/channel.component.scss b/frontend/src/app/lightning/channel/channel.component.scss index f19215f87..bf080b644 100644 --- a/frontend/src/app/lightning/channel/channel.component.scss +++ b/frontend/src/app/lightning/channel/channel.component.scss @@ -30,6 +30,10 @@ font-size: 20px; } +.badge { + margin-right: 5px; +} + app-fiat { display: block; font-size: 13px; diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index 0dd2de183..b8920f4bc 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -87,8 +87,6 @@ -

Channels

- diff --git a/frontend/src/app/lightning/node/node-preview.component.html b/frontend/src/app/lightning/node/node-preview.component.html index 0bb7255a6..a94882161 100644 --- a/frontend/src/app/lightning/node/node-preview.component.html +++ b/frontend/src/app/lightning/node/node-preview.component.html @@ -52,7 +52,7 @@
- +
diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 423b29afb..f2a6ce6f4 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -1,7 +1,7 @@
Lightning node
-

Active channels map

diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts index 91de48186..c6a2f8f5c 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts @@ -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; @@ -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, diff --git a/frontend/src/app/lightning/nodes-channels/node-channels.component.html b/frontend/src/app/lightning/nodes-channels/node-channels.component.html index 43a5fad60..8fc63793c 100644 --- a/frontend/src/app/lightning/nodes-channels/node-channels.component.html +++ b/frontend/src/app/lightning/nodes-channels/node-channels.component.html @@ -1,2 +1,9 @@ -
+
+

Active channels map

+
+
+
+ +
+
diff --git a/frontend/src/app/lightning/nodes-channels/node-channels.component.scss b/frontend/src/app/lightning/nodes-channels/node-channels.component.scss index e69de29bb..4d7b4de0e 100644 --- a/frontend/src/app/lightning/nodes-channels/node-channels.component.scss +++ b/frontend/src/app/lightning/nodes-channels/node-channels.component.scss @@ -0,0 +1,9 @@ +.loading-spinner { + min-height: 455px; + z-index: 100; +} + +.spinner-border { + position: relative; + top: 225px; +} \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-channels/node-channels.component.ts b/frontend/src/app/lightning/nodes-channels/node-channels.component.ts index 9d6d7df2b..074315b35 100644 --- a/frontend/src/app/lightning/nodes-channels/node-channels.component.ts +++ b/frontend/src/app/lightning/nodes-channels/node-channels.component.ts @@ -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; - 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(), ); } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 14d220fa1..b0e018941 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -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') { diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index f9de57834..c340fb50b 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -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, diff --git a/production/install b/production/install index 6d50e8bf7..71bcb7f6b 100755 --- a/production/install +++ b/production/install @@ -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) diff --git a/production/mempool-build-all b/production/mempool-build-all index c0e9a2c2a..6a4e0c028 100755 --- a/production/mempool-build-all +++ b/production/mempool-build-all @@ -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}" diff --git a/production/mempool-kill-all b/production/mempool-kill-all index ae48552c2..b75969155 100755 --- a/production/mempool-kill-all +++ b/production/mempool-kill-all @@ -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 diff --git a/production/mempool-start-all b/production/mempool-start-all index 94766d5ce..a6fdfa589 100755 --- a/production/mempool-start-all +++ b/production/mempool-start-all @@ -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 diff --git a/production/nginx-cache-warmer b/production/nginx-cache-warmer index a4ece6e0b..3c0fd8ef1 100755 --- a/production/nginx-cache-warmer +++ b/production/nginx-cache-warmer @@ -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 diff --git a/production/unfurl-build b/production/unfurl-build deleted file mode 100755 index 5b838e0ae..000000000 --- a/production/unfurl-build +++ /dev/null @@ -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 diff --git a/production/unfurl-kill b/production/unfurl-kill deleted file mode 100755 index ae48552c2..000000000 --- a/production/unfurl-kill +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env zsh -killall sh node diff --git a/production/unfurl-start b/production/unfurl-start deleted file mode 100755 index 29b5ddf3e..000000000 --- a/production/unfurl-start +++ /dev/null @@ -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' diff --git a/production/unfurler-config.liquid.json b/production/unfurler-config.liquid.json new file mode 100644 index 000000000..39df7e3fd --- /dev/null +++ b/production/unfurler-config.liquid.json @@ -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 + } +} diff --git a/production/unfurler-config.mainnet.json b/production/unfurler-config.mainnet.json new file mode 100644 index 000000000..752cd5706 --- /dev/null +++ b/production/unfurler-config.mainnet.json @@ -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 + } +} diff --git a/unfurler/package.json b/unfurler/package.json index 2d353bfdf..ca60201b3 100644 --- a/unfurler/package.json +++ b/unfurler/package.json @@ -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}\"" diff --git a/unfurler/puppeteer.config.json b/unfurler/puppeteer.config.json index 3de7b0652..b3a9b7fc4 100644 --- a/unfurler/puppeteer.config.json +++ b/unfurler/puppeteer.config.json @@ -41,6 +41,6 @@ "--use-mock-keychain", "--ignore-gpu-blacklist", "--ignore-gpu-blocklist", - "--use-gl=swiftshader" + "--use-gl=egl" ] }