From 746a045c4863e362729cc13c07c203e348bb711b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 10 Jun 2024 22:02:14 +0000 Subject: [PATCH 1/8] Refactor address page component with AddressStats class --- .../components/address/address.component.html | 10 +- .../components/address/address.component.ts | 123 +++++++++++++----- 2 files changed, 92 insertions(+), 41 deletions(-) diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 26e5d8203..98b432521 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -28,16 +28,16 @@ Total received - + Total sent - + Balance - + @@ -76,8 +76,8 @@

  - {{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transaction - {{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transactions + {{ (transactions?.length | number) || '?' }} of {{ mempoolStats.tx_count + chainStats.tx_count | number }} transaction + {{ (transactions?.length | number) || '?' }} of {{ mempoolStats.tx_count + chainStats.tx_count | number }} transactions

diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index e79ad45e2..4ef67ed5f 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { ElectrsApiService } from '../../services/electrs-api.service'; import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; -import { Address, ScriptHash, Transaction } from '../../interfaces/electrs.interface'; +import { Address, ChainStats, Transaction } from '../../interfaces/electrs.interface'; import { WebsocketService } from '../../services/websocket.service'; import { StateService } from '../../services/state.service'; import { AudioService } from '../../services/audio.service'; @@ -12,6 +12,78 @@ import { SeoService } from '../../services/seo.service'; import { seoDescriptionNetwork } from '../../shared/common.utils'; import { AddressInformation } from '../../interfaces/node-api.interface'; +class AddressStats implements ChainStats { + address: string; + scriptpubkey?: string; + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; + + constructor (stats: ChainStats, address: string, scriptpubkey?: string) { + Object.assign(this, stats); + this.address = address; + this.scriptpubkey = scriptpubkey; + } + + public addTx(tx: Transaction): void { + for (const vin of tx.vin) { + if (vin.prevout?.scriptpubkey_address === this.address || (this.scriptpubkey === vin.prevout?.scriptpubkey)) { + this.spendTxo(vin.prevout.value); + } + } + for (const vout of tx.vout) { + if (vout.scriptpubkey_address === this.address || (this.scriptpubkey === vout.scriptpubkey)) { + this.fundTxo(vout.value); + } + } + this.tx_count++; + } + + public removeTx(tx: Transaction): void { + for (const vin of tx.vin) { + if (vin.prevout?.scriptpubkey_address === this.address || (this.scriptpubkey === vin.prevout?.scriptpubkey)) { + this.unspendTxo(vin.prevout.value); + } + } + for (const vout of tx.vout) { + if (vout.scriptpubkey_address === this.address || (this.scriptpubkey === vout.scriptpubkey)) { + this.unfundTxo(vout.value); + } + } + this.tx_count--; + } + + private fundTxo(value: number): void { + this.funded_txo_sum += value; + this.funded_txo_count++; + } + + private unfundTxo(value: number): void { + this.funded_txo_sum -= value; + this.funded_txo_count--; + } + + private spendTxo(value: number): void { + this.spent_txo_sum += value; + this.spent_txo_count++; + } + + private unspendTxo(value: number): void { + this.spent_txo_sum -= value; + this.spent_txo_count--; + } + + get balance(): number { + return this.funded_txo_sum - this.spent_txo_sum; + } + + get utxos(): number { + return this.funded_txo_count - this.spent_txo_count; + } +} + @Component({ selector: 'app-address', templateUrl: './address.component.html', @@ -35,9 +107,9 @@ export class AddressComponent implements OnInit, OnDestroy { addressInfo: null | AddressInformation = null; fullyLoaded = false; - txCount = 0; - received = 0; - sent = 0; + chainStats: AddressStats; + mempoolStats: AddressStats; + now = Date.now() / 1000; balancePeriod: 'all' | '1m' = 'all'; @@ -55,7 +127,7 @@ export class AddressComponent implements OnInit, OnDestroy { private seoService: SeoService, ) { } - ngOnInit() { + ngOnInit(): void { this.stateService.networkChanged$.subscribe((network) => this.network = network); this.websocketService.want(['blocks']); @@ -175,7 +247,7 @@ export class AddressComponent implements OnInit, OnDestroy { }); this.transactions = this.tempTransactions; - if (this.transactions.length === this.txCount) { + if (this.transactions.length === (this.mempoolStats.tx_count + this.chainStats.tx_count)) { this.fullyLoaded = true; } this.isLoadingTransactions = false; @@ -196,11 +268,13 @@ export class AddressComponent implements OnInit, OnDestroy { this.mempoolTxSubscription = this.stateService.mempoolTransactions$ .subscribe(tx => { this.addTransaction(tx); + this.mempoolStats.addTx(tx); }); this.mempoolRemovedTxSubscription = this.stateService.mempoolRemovedTransactions$ .subscribe(tx => { this.removeTransaction(tx); + this.mempoolStats.removeTx(tx); }); this.blockTxSubscription = this.stateService.blockTransactions$ @@ -209,12 +283,14 @@ export class AddressComponent implements OnInit, OnDestroy { if (tx) { tx.status = transaction.status; this.transactions = this.transactions.slice(); + this.mempoolStats.removeTx(transaction); this.audioService.playSound('magic'); } else { if (this.addTransaction(transaction, false)) { this.audioService.playSound('magic'); } } + this.chainStats.addTx(transaction); }); } @@ -225,7 +301,6 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions.unshift(transaction); this.transactions = this.transactions.slice(); - this.txCount++; if (playSound) { if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) { @@ -235,17 +310,6 @@ export class AddressComponent implements OnInit, OnDestroy { } } - transaction.vin.forEach((vin) => { - if (vin?.prevout?.scriptpubkey_address === this.address.address) { - this.sent += vin.prevout.value; - } - }); - transaction.vout.forEach((vout) => { - if (vout?.scriptpubkey_address === this.address.address) { - this.received += vout.value; - } - }); - return true; } @@ -257,23 +321,11 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions.splice(index, 1); this.transactions = this.transactions.slice(); - this.txCount--; - - transaction.vin.forEach((vin) => { - if (vin?.prevout?.scriptpubkey_address === this.address.address) { - this.sent -= vin.prevout.value; - } - }); - transaction.vout.forEach((vout) => { - if (vout?.scriptpubkey_address === this.address.address) { - this.received -= vout.value; - } - }); return true; } - loadMore() { + loadMore(): void { if (this.isLoadingTransactions || this.fullyLoaded) { return; } @@ -301,10 +353,9 @@ export class AddressComponent implements OnInit, OnDestroy { }); } - updateChainStats() { - this.received = this.address.chain_stats.funded_txo_sum + this.address.mempool_stats.funded_txo_sum; - this.sent = this.address.chain_stats.spent_txo_sum + this.address.mempool_stats.spent_txo_sum; - this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count; + updateChainStats(): void { + this.chainStats = new AddressStats(this.address.chain_stats, this.address.address); + this.mempoolStats = new AddressStats(this.address.mempool_stats, this.address.address); } setBalancePeriod(period: 'all' | '1m'): boolean { @@ -319,7 +370,7 @@ export class AddressComponent implements OnInit, OnDestroy { ); } - ngOnDestroy() { + ngOnDestroy(): void { this.mainSubscription.unsubscribe(); this.mempoolTxSubscription.unsubscribe(); this.mempoolRemovedTxSubscription.unsubscribe(); From 9514bb703b888d5c252eccd321dc7537d59afa16 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 10 Jun 2024 23:04:37 +0000 Subject: [PATCH 2/8] Redesign top of address page --- .../address-labels.component.html | 4 +- .../address-labels.component.ts | 1 + .../components/address/address.component.html | 119 +++++++++++++----- .../components/address/address.component.scss | 2 +- .../components/address/address.component.ts | 34 ++++- .../address-type/address-type.component.html | 29 +++++ .../address-type/address-type.component.ts | 11 ++ frontend/src/app/shared/shared.module.ts | 3 + 8 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 frontend/src/app/shared/components/address-type/address-type.component.html create mode 100644 frontend/src/app/shared/components/address-type/address-type.component.ts diff --git a/frontend/src/app/components/address-labels/address-labels.component.html b/frontend/src/app/components/address-labels/address-labels.component.html index dfc6647f4..b055cf606 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.html +++ b/frontend/src/app/components/address-labels/address-labels.component.html @@ -4,7 +4,7 @@ {{ label }} @@ -15,6 +15,6 @@ {{ label }} \ No newline at end of file diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index 2365c167f..867fc9970 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -15,6 +15,7 @@ export class AddressLabelsComponent implements OnChanges { @Input() vin: Vin; @Input() vout: Vout; @Input() channel: any; + @Input() class: string = ''; label?: string; diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 98b432521..ba4884219 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -14,40 +14,39 @@
-
- - - - - - - - - - - - - - - - - - - - - -
Unconfidential - - - -
Total received
Total sent
Balance
-
-
-
-
- + @if (isMobile) { +
+ + + + + + + + + +
-
+ } @else { +
+ + + + + + +
+
+
+ + + + + + +
+
+ }
@@ -182,3 +181,57 @@
+ + + + + Balance + + + + + + + pending + + + + + + + Unspent TXOs + {{ chainStats.utxos }} + + + + + + pending + {{ mempoolStats.utxos > 0 ? '+' : ''}}{{ mempoolStats.utxos }} + + + + + + Volume + + + + + + + Type + + + + + + + Unconfidential + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/components/address/address.component.scss b/frontend/src/app/components/address/address.component.scss index da615376c..45b684a49 100644 --- a/frontend/src/app/components/address/address.component.scss +++ b/frontend/src/app/components/address/address.component.scss @@ -25,7 +25,7 @@ tr td { &:last-child { text-align: right; - @media (min-width: 576px) { + @media (min-width: 768px) { text-align: left; } } diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index 4ef67ed5f..b485167cf 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -1,8 +1,8 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { ElectrsApiService } from '../../services/electrs-api.service'; import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; -import { Address, ChainStats, Transaction } from '../../interfaces/electrs.interface'; +import { Address, ChainStats, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; import { WebsocketService } from '../../services/websocket.service'; import { StateService } from '../../services/state.service'; import { AudioService } from '../../services/audio.service'; @@ -79,6 +79,10 @@ class AddressStats implements ChainStats { return this.funded_txo_sum - this.spent_txo_sum; } + get volume(): number { + return this.funded_txo_sum + this.spent_txo_sum; + } + get utxos(): number { return this.funded_txo_count - this.spent_txo_count; } @@ -92,6 +96,8 @@ class AddressStats implements ChainStats { export class AddressComponent implements OnInit, OnDestroy { network = ''; + isMobile: boolean; + address: Address; addressString: string; isLoadingAddress = true; @@ -110,6 +116,10 @@ export class AddressComponent implements OnInit, OnDestroy { chainStats: AddressStats; mempoolStats: AddressStats; + exampleChannel?: any; + exampleVin?: Vin; + exampleVout?: Vout; + now = Date.now() / 1000; balancePeriod: 'all' | '1m' = 'all'; @@ -147,6 +157,9 @@ export class AddressComponent implements OnInit, OnDestroy { this.isLoadingTransactions = true; this.transactions = null; this.addressInfo = null; + this.exampleChannel = null; + this.exampleVin = null; + this.exampleVout = null; document.body.scrollTo(0, 0); this.addressString = params.get('id') || ''; if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) { @@ -252,6 +265,18 @@ export class AddressComponent implements OnInit, OnDestroy { } this.isLoadingTransactions = false; + for (const tx of this.transactions) { + if (!this.exampleVin) { + this.exampleVin = tx.vin.find(v => v.prevout?.scriptpubkey_address === this.address.address); + } + if (!this.exampleVout) { + this.exampleVout = tx.vout.find(v => v.scriptpubkey_address === this.address.address); + } + if (this.exampleVin && this.exampleVout) { + break; + } + } + if (!this.showBalancePeriod()) { this.setBalancePeriod('all'); } else { @@ -370,6 +395,11 @@ export class AddressComponent implements OnInit, OnDestroy { ); } + @HostListener('window:resize', ['$event']) + onResize(): void { + this.isMobile = window.innerWidth < 768; + } + ngOnDestroy(): void { this.mainSubscription.unsubscribe(); this.mempoolTxSubscription.unsubscribe(); diff --git a/frontend/src/app/shared/components/address-type/address-type.component.html b/frontend/src/app/shared/components/address-type/address-type.component.html new file mode 100644 index 000000000..fbe041cb5 --- /dev/null +++ b/frontend/src/app/shared/components/address-type/address-type.component.html @@ -0,0 +1,29 @@ +@switch (vout?.scriptpubkey_type || null) { + @case ('fee') { + fee + } + @case ('empty') { + empty + } + @case ('v0_p2wpkh') { + P2WPKH + } + @case ('v0_p2wsh') { + P2WSH + } + @case ('v1_p2tr') { + P2TR + } + @case ('provably_unspendable') { + provably unspendable + } + @case ('multisig') { + bare multisig + } + @case (null) { + unknown + } + @default { + {{ vout.scriptpubkey_type.toUpperCase() }} + } +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/address-type/address-type.component.ts b/frontend/src/app/shared/components/address-type/address-type.component.ts new file mode 100644 index 000000000..34077330a --- /dev/null +++ b/frontend/src/app/shared/components/address-type/address-type.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; +import { Vout } from '../../../interfaces/electrs.interface'; + +@Component({ + selector: 'app-address-type', + templateUrl: './address-type.component.html', + styleUrls: [] +}) +export class AddressTypeComponent { + @Input() vout: Vout; +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 3b56d3510..ead9060ae 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -87,6 +87,7 @@ import { ChangeComponent } from '../components/change/change.component'; import { SatsComponent } from './components/sats/sats.component'; import { BtcComponent } from './components/btc/btc.component'; import { FeeRateComponent } from './components/fee-rate/fee-rate.component'; +import { AddressTypeComponent } from './components/address-type/address-type.component'; import { TruncateComponent } from './components/truncate/truncate.component'; import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component'; import { TimestampComponent } from './components/timestamp/timestamp.component'; @@ -202,6 +203,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir SatsComponent, BtcComponent, FeeRateComponent, + AddressTypeComponent, TruncateComponent, SearchResultsComponent, TimestampComponent, @@ -343,6 +345,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir SatsComponent, BtcComponent, FeeRateComponent, + AddressTypeComponent, TruncateComponent, SearchResultsComponent, TimestampComponent, From 331b54fe89acb535c0e6c39f5cfab5c04a29ff43 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 10 Jun 2024 23:12:41 +0000 Subject: [PATCH 3/8] Address mouseover QR code --- .../components/address/address.component.html | 6 ++++++ .../components/address/address.component.scss | 20 +++++++------------ .../components/address/address.component.ts | 3 +++ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index ba4884219..6ab0766ee 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -4,6 +4,12 @@ diff --git a/frontend/src/app/components/address/address.component.scss b/frontend/src/app/components/address/address.component.scss index 45b684a49..fb1789701 100644 --- a/frontend/src/app/components/address/address.component.scss +++ b/frontend/src/app/components/address/address.component.scss @@ -1,16 +1,14 @@ .qr-wrapper { + position: absolute; + top: 30px; + right: 0px; + border: solid 10px var(--active-bg); + border-radius: 5px; background-color: #fff; padding: 10px; padding-bottom: 5px; - display: inline-block; -} - -.qrcode-col { - margin: 20px auto 10px; - text-align: center; - @media (min-width: 992px){ - margin: 0px auto 0px; - } + display: block; + z-index: 99; } .fiat { @@ -104,10 +102,6 @@ h1 { width: 80%; } } - - .qrcode-col { - flex-grow: 0.5; - } } .widget-toggler { diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index b485167cf..10054727a 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -97,6 +97,7 @@ export class AddressComponent implements OnInit, OnDestroy { network = ''; isMobile: boolean; + showQR: boolean = false; address: Address; addressString: string; @@ -141,6 +142,8 @@ export class AddressComponent implements OnInit, OnDestroy { this.stateService.networkChanged$.subscribe((network) => this.network = network); this.websocketService.want(['blocks']); + this.onResize(); + this.addressLoadingStatus$ = this.route.paramMap .pipe( switchMap(() => this.stateService.loadingIndicators$), From 3b419be341eca1dbda4766d56dfa763dc1ced3bd Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 11 Jun 2024 20:51:17 +0000 Subject: [PATCH 4/8] Address details pending -> unconfirmed --- frontend/src/app/components/address/address.component.html | 6 +++--- frontend/src/app/components/address/address.component.scss | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 6ab0766ee..6652047d0 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -198,21 +198,21 @@ - pending + unconfirmed balance - Unspent TXOs + UTXOs {{ chainStats.utxos }} - pending + unconfirmed UTXOs {{ mempoolStats.utxos > 0 ? '+' : ''}}{{ mempoolStats.utxos }} diff --git a/frontend/src/app/components/address/address.component.scss b/frontend/src/app/components/address/address.component.scss index fb1789701..b4ab5ac8a 100644 --- a/frontend/src/app/components/address/address.component.scss +++ b/frontend/src/app/components/address/address.component.scss @@ -76,10 +76,10 @@ h1 { top: 9px; position: relative; @media (min-width: 576px) { + max-width: calc(100% - 180px); top: 11px; } @media (min-width: 768px) { - max-width: calc(100% - 180px); top: 17px; } } From 7dfdb5553e5b06bf9c65add5cd8cada6c0d61790 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 12 Jun 2024 04:16:58 +0000 Subject: [PATCH 5/8] Address & script parsing refactor --- .../address-labels.component.ts | 87 ++------ .../components/address/address.component.html | 4 +- .../components/address/address.component.ts | 24 +-- .../transactions-list.component.html | 2 +- frontend/src/app/shared/address-utils.ts | 193 ++++++++++++++++++ .../address-type/address-type.component.html | 4 +- .../address-type/address-type.component.ts | 4 +- frontend/src/app/shared/regex.utils.ts | 6 +- frontend/src/app/shared/script.utils.ts | 110 +++++++++- 9 files changed, 340 insertions(+), 94 deletions(-) create mode 100644 frontend/src/app/shared/address-utils.ts diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index 867fc9970..72a58bfca 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -1,7 +1,7 @@ import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; import { Vin, Vout } from '../../interfaces/electrs.interface'; import { StateService } from '../../services/state.service'; -import { parseMultisigScript } from '../../bitcoin.utils'; +import { AddressType, AddressTypeInfo } from '../../shared/address-utils'; @Component({ selector: 'app-address-labels', @@ -12,6 +12,7 @@ import { parseMultisigScript } from '../../bitcoin.utils'; export class AddressLabelsComponent implements OnChanges { network = ''; + @Input() address: AddressTypeInfo; @Input() vin: Vin; @Input() vout: Vout; @Input() channel: any; @@ -28,10 +29,10 @@ export class AddressLabelsComponent implements OnChanges { ngOnChanges() { if (this.channel) { this.handleChannel(); + } else if (this.address) { + this.handleAddress(); } else if (this.vin) { this.handleVin(); - } else if (this.vout) { - this.handleVout(); } } @@ -42,74 +43,22 @@ export class AddressLabelsComponent implements OnChanges { this.label = `Channel ${type}: ${leftNodeName} <> ${rightNodeName}`; } + handleAddress() { + if (this.address?.scripts.size) { + const script = this.address?.scripts.values().next().value; + if (script.template?.label) { + this.label = script.template.label; + } + } + } + handleVin() { - if (this.vin.inner_witnessscript_asm) { - if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0 || this.vin.inner_witnessscript_asm.indexOf('OP_PUSHNUM_15 OP_CHECKMULTISIG OP_IFDUP OP_NOTIF OP_PUSHBYTES_2') === 1259) { - if (this.vin.witness.length > 11) { - this.label = 'Liquid Peg Out'; - } else { - this.label = 'Emergency Liquid Peg Out'; - } - return; + const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]) + if (address?.scripts.size) { + const script = address?.scripts.values().next().value; + if (script.template?.label) { + this.label = script.template.label; } - - const topElement = this.vin.witness[this.vin.witness.length - 2]; - if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(this.vin.inner_witnessscript_asm)) { - // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs - if (topElement === '01') { - // top element is '01' to get in the revocation path - this.label = 'Revoked Lightning Force Close'; - } else { - // top element is '', this is a delayed to_local output - this.label = 'Lightning Force Close'; - } - return; - } else if ( - /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm) || - /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm) - ) { - // https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs - // https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs - if (topElement.length === 66) { - // top element is a public key - this.label = 'Revoked Lightning HTLC'; - } else if (topElement) { - // top element is a preimage - this.label = 'Lightning HTLC'; - } else { - // top element is '' to get in the expiry of the script - this.label = 'Expired Lightning HTLC'; - } - return; - } else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) { - // https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors - if (topElement) { - // top element is a signature - this.label = 'Lightning Anchor'; - } else { - // top element is '', it has been swept after 16 blocks - this.label = 'Swept Lightning Anchor'; - } - return; - } - - this.detectMultisig(this.vin.inner_witnessscript_asm); } - - this.detectMultisig(this.vin.inner_redeemscript_asm); - - this.detectMultisig(this.vin.prevout.scriptpubkey_asm); - } - - detectMultisig(script: string) { - const ms = parseMultisigScript(script); - - if (ms) { - this.label = $localize`:@@address-label.multisig:Multisig ${ms.m}:multisigM: of ${ms.n}:multisigN:`; - } - } - - handleVout() { - this.detectMultisig(this.vout.scriptpubkey_asm); } } diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 6652047d0..de10948f7 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -220,14 +220,14 @@ Volume - + Type - + diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index 10054727a..477954805 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { ElectrsApiService } from '../../services/electrs-api.service'; import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; -import { Address, ChainStats, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; +import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface'; import { WebsocketService } from '../../services/websocket.service'; import { StateService } from '../../services/state.service'; import { AudioService } from '../../services/audio.service'; @@ -11,6 +11,7 @@ import { of, merge, Subscription, Observable } from 'rxjs'; import { SeoService } from '../../services/seo.service'; import { seoDescriptionNetwork } from '../../shared/common.utils'; import { AddressInformation } from '../../interfaces/node-api.interface'; +import { AddressTypeInfo } from '../../shared/address-utils'; class AddressStats implements ChainStats { address: string; @@ -112,14 +113,13 @@ export class AddressComponent implements OnInit, OnDestroy { blockTxSubscription: Subscription; addressLoadingStatus$: Observable; addressInfo: null | AddressInformation = null; + addressTypeInfo: null | AddressTypeInfo; fullyLoaded = false; chainStats: AddressStats; mempoolStats: AddressStats; exampleChannel?: any; - exampleVin?: Vin; - exampleVout?: Vout; now = Date.now() / 1000; balancePeriod: 'all' | '1m' = 'all'; @@ -161,8 +161,6 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions = null; this.addressInfo = null; this.exampleChannel = null; - this.exampleVin = null; - this.exampleVout = null; document.body.scrollTo(0, 0); this.addressString = params.get('id') || ''; if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) { @@ -171,6 +169,8 @@ export class AddressComponent implements OnInit, OnDestroy { this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`); + this.addressTypeInfo = new AddressTypeInfo(this.stateService.network || 'mainnet', this.addressString); + return merge( of(true), this.stateService.connectionState$ @@ -268,17 +268,13 @@ export class AddressComponent implements OnInit, OnDestroy { } this.isLoadingTransactions = false; + let addressVin: Vin[] = []; for (const tx of this.transactions) { - if (!this.exampleVin) { - this.exampleVin = tx.vin.find(v => v.prevout?.scriptpubkey_address === this.address.address); - } - if (!this.exampleVout) { - this.exampleVout = tx.vout.find(v => v.scriptpubkey_address === this.address.address); - } - if (this.exampleVin && this.exampleVout) { - break; - } + addressVin = addressVin.concat(tx.vin.filter(v => v.prevout?.scriptpubkey_address === this.address.address)); } + this.addressTypeInfo.processInputs(addressVin); + // hack to trigger change detection + this.addressTypeInfo = this.addressTypeInfo.clone(); if (!this.showBalancePeriod()) { this.setBalancePeriod('all'); diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index ee7ac52f5..88a984942 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -75,7 +75,7 @@ {{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
- +
diff --git a/frontend/src/app/shared/address-utils.ts b/frontend/src/app/shared/address-utils.ts new file mode 100644 index 000000000..c5e1fcf3d --- /dev/null +++ b/frontend/src/app/shared/address-utils.ts @@ -0,0 +1,193 @@ +import '@angular/localize/init'; +import { ScriptInfo } from './script.utils'; +import { Vin } from '../interfaces/electrs.interface'; +import { BECH32_CHARS_LW, BASE58_CHARS, HEX_CHARS } from './regex.utils'; + +export type AddressType = 'fee' + | 'empty' + | 'provably_unspendable' + | 'op_return' + | 'multisig' + | 'p2pk' + | 'p2pkh' + | 'p2sh' + | 'p2sh-p2wpkh' + | 'p2sh-p2wsh' + | 'v0_p2wpkh' + | 'v0_p2wsh' + | 'v1_p2tr' + | 'confidential' + | 'unknown' + +const ADDRESS_PREFIXES = { + mainnet: { + base58: { + pubkey: ['1'], + script: ['3'], + }, + bech32: 'bc1', + }, + testnet: { + base58: { + pubkey: ['m', 'n'], + script: '2', + }, + bech32: 'tb1', + }, + testnet4: { + base58: { + pubkey: ['m', 'n'], + script: '2', + }, + bech32: 'tb1', + }, + signet: { + base58: { + pubkey: ['m', 'n'], + script: '2', + }, + bech32: 'tb1', + }, + liquid: { + base58: { + pubkey: ['P','Q'], + script: ['G','H'], + confidential: ['V'], + }, + bech32: 'ex1', + confidential: 'lq1', + }, + liquidtestnet: { + base58: { + pubkey: ['F'], + script: ['8','9'], + confidential: ['V'], // TODO: check if this is actually correct + }, + bech32: 'tex1', + confidential: 'tlq1', + }, +}; + +// precompiled regexes for common address types (excluding prefixes) +const base58Regex = RegExp('^' + BASE58_CHARS + '{26,34}$'); +const confidentialb58Regex = RegExp('^[TJ]' + BASE58_CHARS + '{78}$'); +const p2wpkhRegex = RegExp('^q' + BECH32_CHARS_LW + '{38}$'); +const p2wshRegex = RegExp('^q' + BECH32_CHARS_LW + '{58}$'); +const p2trRegex = RegExp('^p' + BECH32_CHARS_LW + '{58}$'); +const pubkeyRegex = RegExp('^' + `(04${HEX_CHARS}{128})|(0[23]${HEX_CHARS}{64})$`); + +export function detectAddressType(address: string, network: string): AddressType { + // normal address types + const firstChar = address.substring(0, 1); + if (ADDRESS_PREFIXES[network].base58.pubkey.includes(firstChar) && base58Regex.test(address.slice(1))) { + return 'p2pkh'; + } else if (ADDRESS_PREFIXES[network].base58.script.includes(firstChar) && base58Regex.test(address.slice(1))) { + return 'p2sh'; + } else if (address.startsWith(ADDRESS_PREFIXES[network].bech32)) { + const suffix = address.slice(ADDRESS_PREFIXES[network].bech32.length); + if (p2wpkhRegex.test(suffix)) { + return 'v0_p2wpkh'; + } else if (p2wshRegex.test(suffix)) { + return 'v0_p2wsh'; + } else if (p2trRegex.test(suffix)) { + return 'v1_p2tr'; + } + } + + // p2pk + if (pubkeyRegex.test(address)) { + return 'p2pk'; + } + + // liquid-specific types + if (network.startsWith('liquid')) { + if (ADDRESS_PREFIXES[network].base58.confidential.includes(firstChar) && confidentialb58Regex.test(address.slice(1))) { + return 'confidential'; + } else if (address.startsWith(ADDRESS_PREFIXES[network].confidential)) { + return 'confidential'; + } + } + + return 'unknown'; +} + +/** + * Parses & classifies address types + properties from address strings + * + * can optionally augment this data with examples of spends from the address, + * e.g. to classify revealed scripts for scripthash-type addresses. + */ +export class AddressTypeInfo { + network: string; + address: string; + type: AddressType; + // script data + scripts: Map; // raw script + // flags + isMultisig?: { m: number, n: number }; + tapscript?: boolean; + + constructor (network: string, address: string, type?: AddressType, vin?: Vin[]) { + this.network = network; + this.address = address; + this.scripts = new Map(); + if (type) { + this.type = type; + } else { + this.type = detectAddressType(address, network); + } + this.processInputs(vin); + } + + public clone(): AddressTypeInfo { + const cloned = new AddressTypeInfo(this.network, this.address, this.type); + cloned.scripts = new Map(Array.from(this.scripts, ([key, value]) => [key, value?.clone()])); + cloned.isMultisig = this.isMultisig; + cloned.tapscript = this.tapscript; + return cloned; + } + + public processInputs(vin: Vin[] = []): void { + // taproot can have multiple script paths + if (this.type === 'v1_p2tr') { + for (const v of vin) { + if (v.inner_witnessscript_asm) { + this.tapscript = true; + const controlBlock = v.witness[v.witness.length - 1].startsWith('50') ? v.witness[v.witness.length - 2] : v.witness[v.witness.length - 1]; + this.processScript(new ScriptInfo('inner_witnessscript', undefined, v.inner_witnessscript_asm, v.witness, controlBlock)); + } + } + // for single-script types, if we've seen one input we've seen them all + } else if (['p2sh', 'v0_p2wsh'].includes(this.type)) { + if (!this.scripts.size && vin.length) { + const v = vin[0]; + // wrapped segwit + if (this.type === 'p2sh' && v.witness?.length) { + if (v.scriptsig.startsWith('160014')) { + this.type = 'p2sh-p2wpkh'; + } else if (v.scriptsig.startsWith('220020')) { + this.type = 'p2sh-p2wsh'; + } + } + // real script + if (this.type !== 'p2sh-p2wpkh') { + if (v.inner_witnessscript_asm) { + this.processScript(new ScriptInfo('inner_witnessscript', undefined, v.inner_witnessscript_asm, v.witness)); + } else if (v.inner_redeemscript_asm) { + this.processScript(new ScriptInfo('inner_redeemscript', undefined, v.inner_redeemscript_asm, v.witness)); + } else if (v.scriptsig || v.scriptsig_asm) { + this.processScript(new ScriptInfo('scriptsig', v.scriptsig, v.scriptsig_asm, v.witness)); + } + } + } + } + // and there's nothing more to learn from processing inputs for non-scripthash types + } + + private processScript(script: ScriptInfo): void { + this.scripts.set(script.key, script); + if (script.template?.type === 'multisig') { + this.isMultisig = { m: script.template['m'], n: script.template['n'] }; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/address-type/address-type.component.html b/frontend/src/app/shared/components/address-type/address-type.component.html index fbe041cb5..fe4286689 100644 --- a/frontend/src/app/shared/components/address-type/address-type.component.html +++ b/frontend/src/app/shared/components/address-type/address-type.component.html @@ -1,4 +1,4 @@ -@switch (vout?.scriptpubkey_type || null) { +@switch (address.type || null) { @case ('fee') { fee } @@ -24,6 +24,6 @@ unknown } @default { - {{ vout.scriptpubkey_type.toUpperCase() }} + {{ address.type.toUpperCase() }} } } \ No newline at end of file diff --git a/frontend/src/app/shared/components/address-type/address-type.component.ts b/frontend/src/app/shared/components/address-type/address-type.component.ts index 34077330a..1a2456c07 100644 --- a/frontend/src/app/shared/components/address-type/address-type.component.ts +++ b/frontend/src/app/shared/components/address-type/address-type.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { Vout } from '../../../interfaces/electrs.interface'; +import { AddressTypeInfo } from '../../address-utils'; @Component({ selector: 'app-address-type', @@ -7,5 +7,5 @@ import { Vout } from '../../../interfaces/electrs.interface'; styleUrls: [] }) export class AddressTypeComponent { - @Input() vout: Vout; + @Input() address: AddressTypeInfo; } diff --git a/frontend/src/app/shared/regex.utils.ts b/frontend/src/app/shared/regex.utils.ts index 187111a59..cdc2963e8 100644 --- a/frontend/src/app/shared/regex.utils.ts +++ b/frontend/src/app/shared/regex.utils.ts @@ -1,14 +1,14 @@ import { Env } from '../services/state.service'; // all base58 characters -const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`; +export const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`; // all bech32 characters (after the separator) -const BECH32_CHARS_LW = `[ac-hj-np-z02-9]`; +export const BECH32_CHARS_LW = `[ac-hj-np-z02-9]`; const BECH32_CHARS_UP = `[AC-HJ-NP-Z02-9]`; // Hex characters -const HEX_CHARS = `[a-fA-F0-9]`; +export const HEX_CHARS = `[a-fA-F0-9]`; // A regex to say "A single 0 OR any number with no leading zeroes" // Capped at 9 digits so as to not be confused with lightning channel IDs (which are around 17 digits) diff --git a/frontend/src/app/shared/script.utils.ts b/frontend/src/app/shared/script.utils.ts index 556cd40f2..171112dcc 100644 --- a/frontend/src/app/shared/script.utils.ts +++ b/frontend/src/app/shared/script.utils.ts @@ -145,8 +145,116 @@ for (let i = 187; i <= 255; i++) { export { opcodes }; +export type ScriptType = 'scriptpubkey' + | 'scriptsig' + | 'inner_witnessscript' + | 'inner_redeemscript' + +export interface ScriptTemplate { + type: string; + label: string; +} + +export const ScriptTemplates: { [type: string]: (...args: any) => ScriptTemplate } = { + liquid_peg_out: () => ({ type: 'liquid_peg_out', label: 'Liquid Peg Out' }), + liquid_peg_out_emergency: () => ({ type: 'liquid_peg_out_emergency', label: 'Emergency Liquid Peg Out' }), + ln_force_close: () => ({ type: 'ln_force_close', label: 'Lightning Force Close' }), + ln_force_close_revoked: () => ({ type: 'ln_force_close_revoked', label: 'Revoked Lightning Force Close' }), + ln_htlc: () => ({ type: 'ln_htlc', label: 'Lightning HTLC' }), + ln_htlc_revoked: () => ({ type: 'ln_htlc_revoked', label: 'Revoked Lightning HTLC' }), + ln_htlc_expired: () => ({ type: 'ln_htlc_expired', label: 'Expired Lightning HTLC' }), + ln_anchor: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }), + ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept Lightning Anchor' }), + multisig: (m: number, n: number) => ({ type: 'multisig', m, n, label: $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:` }), +}; + +export class ScriptInfo { + type: ScriptType; + scriptPath?: string; + hex?: string; + asm?: string; + template: ScriptTemplate; + + constructor(type: ScriptType, hex?: string, asm?: string, witness?: string[], scriptPath?: string) { + this.type = type; + this.hex = hex; + this.asm = asm; + if (scriptPath) { + this.scriptPath = scriptPath; + } + if (this.asm) { + this.template = detectScriptTemplate(this.type, this.asm, witness); + } + } + + public clone(): ScriptInfo { + return { ...this }; + } + + get key(): string { + return this.type + (this.scriptPath || ''); + } +} + +/** parses an inner_witnessscript + witness stack, and detects named script types */ +export function detectScriptTemplate(type: ScriptType, script_asm: string, witness?: string[]): ScriptTemplate | undefined { + if (type === 'inner_witnessscript' && witness?.length) { + if (script_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0 || script_asm.indexOf('OP_PUSHNUM_15 OP_CHECKMULTISIG OP_IFDUP OP_NOTIF OP_PUSHBYTES_2') === 1259) { + if (witness.length > 11) { + return ScriptTemplates.liquid_peg_out(); + } else { + return ScriptTemplates.liquid_peg_out_emergency(); + } + } + + const topElement = witness[witness.length - 2]; + if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(script_asm)) { + // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs + if (topElement === '01') { + // top element is '01' to get in the revocation path + return ScriptTemplates.ln_force_close_revoked(); + } else { + // top element is '', this is a delayed to_local output + return ScriptTemplates.ln_force_close(); + } + } else if ( + /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(script_asm) || + /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(script_asm) + ) { + // https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs + // https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs + if (topElement.length === 66) { + // top element is a public key + return ScriptTemplates.ln_htlc_revoked(); + } else if (topElement) { + // top element is a preimage + return ScriptTemplates.ln_htlc(); + } else { + // top element is '' to get in the expiry of the script + return ScriptTemplates.ln_htlc_expired(); + } + } else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(script_asm)) { + // https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors + if (topElement) { + // top element is a signature + return ScriptTemplates.ln_anchor(); + } else { + // top element is '', it has been swept after 16 blocks + return ScriptTemplates.ln_anchor_swept(); + } + } + } + + const multisig = parseMultisigScript(script_asm); + if (multisig) { + return ScriptTemplates.multisig(multisig.m, multisig.n); + } + + return; +} + /** extracts m and n from a multisig script (asm), returns nothing if it is not a multisig script */ -export function parseMultisigScript(script: string): void | { m: number, n: number } { +export function parseMultisigScript(script: string): undefined | { m: number, n: number } { if (!script) { return; } From fb621f98123778923a4787c2e213b3f55fcf23c4 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 14 Jun 2024 15:51:00 +0000 Subject: [PATCH 6/8] Address redesign liquid & layout fixes --- .../components/address/address.component.html | 20 +++++++++++++------ .../components/address/address.component.scss | 4 ++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index de10948f7..3d6f1b86d 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -28,23 +28,31 @@ - + @if (network === 'liquid' || network === 'liquidtestnet') { + + } @else { + + } } @else {
- +
- + @if (network === 'liquid' || network === 'liquidtestnet') { + + } @else { + + }
- +
@@ -227,7 +235,7 @@ - + @@ -235,7 +243,7 @@ diff --git a/frontend/src/app/components/address/address.component.scss b/frontend/src/app/components/address/address.component.scss index b4ab5ac8a..c7d37425e 100644 --- a/frontend/src/app/components/address/address.component.scss +++ b/frontend/src/app/components/address/address.component.scss @@ -27,6 +27,10 @@ text-align: left; } } + + &.wrap-cell { + white-space: normal; + } } } From 5cecd9f8a79cb757a1c64383013f9c1462462766 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 15 Jun 2024 20:52:46 +0000 Subject: [PATCH 7/8] Address page bigger QR button on mobile --- .../src/app/components/address/address.component.html | 6 +++--- .../src/app/components/clipboard/clipboard.component.html | 4 ++-- .../src/app/components/clipboard/clipboard.component.ts | 8 +++++++- .../shared/components/truncate/truncate.component.scss | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 3d6f1b86d..dcff7f6b3 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -3,9 +3,9 @@

Address

Type
Unconfidential - +
+
@@ -39,7 +39,7 @@ } @else {
-
+
@@ -52,7 +52,7 @@
- +
diff --git a/frontend/src/app/components/address/address.component.scss b/frontend/src/app/components/address/address.component.scss index c7d37425e..8e04ffe8b 100644 --- a/frontend/src/app/components/address/address.component.scss +++ b/frontend/src/app/components/address/address.component.scss @@ -98,13 +98,6 @@ h1 { .liquid-address { .address-table { table-layout: fixed; - - tr td:first-child { - width: 170px; - } - tr td:last-child { - width: 80%; - } } }