From 65dbafd2ec0b13873468671bda0bdc40f611efa1 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 22 Jul 2023 17:51:45 +0900 Subject: [PATCH 1/6] Support P2PK address types --- .../bitcoin/bitcoin-api-abstract-factory.ts | 2 + backend/src/api/bitcoin/bitcoin-api.ts | 8 +++ backend/src/api/bitcoin/bitcoin.routes.ts | 41 +++++++++++ backend/src/api/bitcoin/electrum-api.ts | 71 +++++++++++++++++++ .../src/api/bitcoin/esplora-api.interface.ts | 7 ++ backend/src/api/bitcoin/esplora-api.ts | 8 +++ frontend/src/app/bitcoin.utils.ts | 9 +++ .../address/address-preview.component.ts | 8 ++- .../components/address/address.component.scss | 1 + .../components/address/address.component.ts | 15 ++-- .../transactions-list.component.html | 15 +++- .../transactions-list.component.scss | 6 ++ .../src/app/interfaces/electrs.interface.ts | 16 +++++ .../src/app/services/electrs-api.service.ts | 33 ++++++++- 14 files changed, 227 insertions(+), 13 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index 7b2802d1b..c233ed5d7 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -14,6 +14,8 @@ export interface AbstractBitcoinApi { $getAddress(address: string): Promise; $getAddressTransactions(address: string, lastSeenTxId: string): Promise; $getAddressPrefix(prefix: string): string[]; + $getScriptHash(scripthash: string): Promise; + $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise; $sendRawTransaction(rawTransaction: string): Promise; $getOutspend(txId: string, vout: number): Promise; $getOutspends(txId: string): Promise; diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index cbcb2c571..c045d8664 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -108,6 +108,14 @@ class BitcoinApi implements AbstractBitcoinApi { throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.'); } + $getScriptHash(scripthash: string): Promise { + throw new Error('Method getScriptHash not supported by the Bitcoin RPC API.'); + } + + $getScriptHashTransactions(scripthash: string, lastSeenTxId: string): Promise { + throw new Error('Method getScriptHashTransactions not supported by the Bitcoin RPC API.'); + } + $getRawMempool(): Promise { return this.bitcoindClient.getRawMemPool(); } diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index babc0aa53..ffdb2e629 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -121,6 +121,8 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress) .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions) + .get(config.MEMPOOL.API_URL_PREFIX + 'scripthash/:scripthash', this.getScriptHash) + .get(config.MEMPOOL.API_URL_PREFIX + 'scripthash/:scripthash/txs', this.getScriptHashTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix) ; } @@ -567,6 +569,45 @@ class BitcoinRoutes { } } + private async getScriptHash(req: Request, res: Response) { + if (config.MEMPOOL.BACKEND === 'none') { + res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + return; + } + + try { + const addressData = await bitcoinApi.$getScriptHash(req.params.address); + res.json(addressData); + } catch (e) { + if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { + return res.status(413).send(e instanceof Error ? e.message : e); + } + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async getScriptHashTransactions(req: Request, res: Response): Promise { + if (config.MEMPOOL.BACKEND === 'none') { + res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + return; + } + + try { + let lastTxId: string = ''; + if (req.query.after_txid && typeof req.query.after_txid === 'string') { + lastTxId = req.query.after_txid; + } + const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.address, lastTxId); + res.json(transactions); + } catch (e) { + if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { + res.status(413).send(e instanceof Error ? e.message : e); + return; + } + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async getAddressPrefix(req: Request, res: Response) { try { const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); diff --git a/backend/src/api/bitcoin/electrum-api.ts b/backend/src/api/bitcoin/electrum-api.ts index 9d1ef46d3..07c58dbc9 100644 --- a/backend/src/api/bitcoin/electrum-api.ts +++ b/backend/src/api/bitcoin/electrum-api.ts @@ -126,6 +126,77 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi { } } + async $getScriptHash(scripthash: string): Promise { + try { + const balance = await this.electrumClient.blockchainScripthash_getBalance(scripthash); + let history = memoryCache.get('Scripthash_getHistory', scripthash); + if (!history) { + history = await this.electrumClient.blockchainScripthash_getHistory(scripthash); + memoryCache.set('Scripthash_getHistory', scripthash, history, 2); + } + + const unconfirmed = history ? history.filter((h) => h.fee).length : 0; + + return { + 'scripthash': scripthash, + 'chain_stats': { + 'funded_txo_count': 0, + 'funded_txo_sum': balance.confirmed ? balance.confirmed : 0, + 'spent_txo_count': 0, + 'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0, + 'tx_count': (history?.length || 0) - unconfirmed, + }, + 'mempool_stats': { + 'funded_txo_count': 0, + 'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0, + 'spent_txo_count': 0, + 'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0, + 'tx_count': unconfirmed, + }, + 'electrum': true, + }; + } catch (e: any) { + throw new Error(typeof e === 'string' ? e : e && e.message || e); + } + } + + async $getScriptHashTransactions(scripthash: string, lastSeenTxId?: string): Promise { + try { + loadingIndicators.setProgress('address-' + scripthash, 0); + + const transactions: IEsploraApi.Transaction[] = []; + let history = memoryCache.get('Scripthash_getHistory', scripthash); + if (!history) { + history = await this.electrumClient.blockchainScripthash_getHistory(scripthash); + memoryCache.set('Scripthash_getHistory', scripthash, history, 2); + } + if (!history) { + throw new Error('failed to get scripthash history'); + } + history.sort((a, b) => (b.height || 9999999) - (a.height || 9999999)); + + let startingIndex = 0; + if (lastSeenTxId) { + const pos = history.findIndex((historicalTx) => historicalTx.tx_hash === lastSeenTxId); + if (pos) { + startingIndex = pos + 1; + } + } + const endIndex = Math.min(startingIndex + 10, history.length); + + for (let i = startingIndex; i < endIndex; i++) { + const tx = await this.$getRawTransaction(history[i].tx_hash, false, true); + transactions.push(tx); + loadingIndicators.setProgress('address-' + scripthash, (i + 1) / endIndex * 100); + } + + return transactions; + } catch (e: any) { + loadingIndicators.setProgress('address-' + scripthash, 100); + throw new Error(typeof e === 'string' ? e : e && e.message || e); + } + } + private $getScriptHashBalance(scriptHash: string): Promise { return this.electrumClient.blockchainScripthash_getBalance(this.encodeScriptHash(scriptHash)); } diff --git a/backend/src/api/bitcoin/esplora-api.interface.ts b/backend/src/api/bitcoin/esplora-api.interface.ts index 5b86952b0..55abe1d34 100644 --- a/backend/src/api/bitcoin/esplora-api.interface.ts +++ b/backend/src/api/bitcoin/esplora-api.interface.ts @@ -99,6 +99,13 @@ export namespace IEsploraApi { electrum?: boolean; } + export interface ScriptHash { + scripthash: string; + chain_stats: ChainStats; + mempool_stats: MempoolStats; + electrum?: boolean; + } + export interface ChainStats { funded_txo_count: number; funded_txo_sum: number; diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index ee7fa4765..01294cc01 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -110,6 +110,14 @@ class ElectrsApi implements AbstractBitcoinApi { throw new Error('Method getAddressTransactions not implemented.'); } + $getScriptHash(scripthash: string): Promise { + throw new Error('Method getAddress not implemented.'); + } + + $getScriptHashTransactions(scripthash: string, txId?: string): Promise { + throw new Error('Method getAddressTransactions not implemented.'); + } + $getAddressPrefix(prefix: string): string[] { throw new Error('Method not implemented.'); } diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index 5419464a9..7ff0d9570 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -281,3 +281,12 @@ export function isFeatureActive(network: string, height: number, feature: 'rbf' return false; } } + +export async function calcScriptHash$(script: string): Promise { + const buf = Uint8Array.from(script.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); + const hashBuffer = await crypto.subtle.digest('SHA-256', buf); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray + .map((bytes) => bytes.toString(16).padStart(2, '0')) + .join(''); +} \ No newline at end of file diff --git a/frontend/src/app/components/address/address-preview.component.ts b/frontend/src/app/components/address/address-preview.component.ts index 713f09f14..07ead8baa 100644 --- a/frontend/src/app/components/address/address-preview.component.ts +++ b/frontend/src/app/components/address/address-preview.component.ts @@ -64,13 +64,15 @@ export class AddressPreviewComponent implements OnInit, OnDestroy { this.address = null; this.addressInfo = null; this.addressString = params.get('id') || ''; - if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) { + if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) { this.addressString = this.addressString.toLowerCase(); } this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); - return this.electrsApiService.getAddress$(this.addressString) - .pipe( + return (this.addressString.match(/[a-f0-9]{130}/) + ? this.electrsApiService.getPubKeyAddress$(this.addressString) + : this.electrsApiService.getAddress$(this.addressString) + ).pipe( catchError((err) => { this.isLoadingAddress = false; this.error = err; diff --git a/frontend/src/app/components/address/address.component.scss b/frontend/src/app/components/address/address.component.scss index 37abcc49e..fe0729b94 100644 --- a/frontend/src/app/components/address/address.component.scss +++ b/frontend/src/app/components/address/address.component.scss @@ -81,6 +81,7 @@ h1 { top: 11px; } @media (min-width: 768px) { + max-width: calc(100% - 180px); top: 17px; } } diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index 57439f983..ae1f6dbbe 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, Transaction } from '../../interfaces/electrs.interface'; +import { Address, ScriptHash, Transaction } from '../../interfaces/electrs.interface'; import { WebsocketService } from '../../services/websocket.service'; import { StateService } from '../../services/state.service'; import { AudioService } from '../../services/audio.service'; @@ -72,7 +72,7 @@ export class AddressComponent implements OnInit, OnDestroy { this.addressInfo = null; document.body.scrollTo(0, 0); this.addressString = params.get('id') || ''; - if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) { + if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) { this.addressString = this.addressString.toLowerCase(); } this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); @@ -83,8 +83,11 @@ export class AddressComponent implements OnInit, OnDestroy { .pipe(filter((state) => state === 2 && this.transactions && this.transactions.length > 0)) ) .pipe( - switchMap(() => this.electrsApiService.getAddress$(this.addressString) - .pipe( + switchMap(() => ( + this.addressString.match(/[a-f0-9]{130}/) + ? this.electrsApiService.getPubKeyAddress$(this.addressString) + : this.electrsApiService.getAddress$(this.addressString) + ).pipe( catchError((err) => { this.isLoadingAddress = false; this.error = err; @@ -114,7 +117,9 @@ export class AddressComponent implements OnInit, OnDestroy { this.updateChainStats(); this.isLoadingAddress = false; this.isLoadingTransactions = true; - return this.electrsApiService.getAddressTransactions$(address.address); + return address.is_pubkey + ? this.electrsApiService.getScriptHashTransactions$('41' + address.address + 'ac') + : this.electrsApiService.getAddressTransactions$(address.address); }), switchMap((transactions) => { this.tempTransactions = transactions; 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 b32afbfb3..3f88c61b0 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -56,7 +56,9 @@ Peg-in - P2PK + P2PK + + @@ -182,12 +184,19 @@ - + + + + P2PK + + + +
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.scss b/frontend/src/app/components/transactions-list/transactions-list.component.scss index 08d7d7486..7356bad0b 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.scss +++ b/frontend/src/app/components/transactions-list/transactions-list.component.scss @@ -140,6 +140,12 @@ h2 { font-family: monospace; } +.p2pk-address { + display: inline-block; + margin-left: 1em; + max-width: 140px; +} + .grey-info-text { color:#6c757d; font-style: italic; diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index 2739d2b06..df19f7491 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -129,6 +129,22 @@ export interface Address { address: string; chain_stats: ChainStats; mempool_stats: MempoolStats; + is_pubkey?: boolean; +} + +export interface ScriptHash { + electrum?: boolean; + scripthash: string; + chain_stats: ChainStats; + mempool_stats: MempoolStats; +} + +export interface AddressOrScriptHash { + electrum?: boolean; + address?: string; + scripthash?: string; + chain_stats: ChainStats; + mempool_stats: MempoolStats; } export interface ChainStats { diff --git a/frontend/src/app/services/electrs-api.service.ts b/frontend/src/app/services/electrs-api.service.ts index c87018741..f866eb23d 100644 --- a/frontend/src/app/services/electrs-api.service.ts +++ b/frontend/src/app/services/electrs-api.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface'; +import { Observable, from, of, switchMap } from 'rxjs'; +import { Transaction, Address, Outspend, Recent, Asset, ScriptHash } from '../interfaces/electrs.interface'; import { StateService } from './state.service'; import { BlockExtended } from '../interfaces/node-api.interface'; +import { calcScriptHash$ } from '../bitcoin.utils'; @Injectable({ providedIn: 'root' @@ -65,6 +66,24 @@ export class ElectrsApiService { return this.httpClient.get
(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address); } + getPubKeyAddress$(pubkey: string): Observable
{ + return this.getScriptHash$('41' + pubkey + 'ac').pipe( + switchMap((scripthash: ScriptHash) => { + return of({ + ...scripthash, + address: pubkey, + is_pubkey: true, + }); + }) + ); + } + + getScriptHash$(script: string): Observable { + return from(calcScriptHash$(script)).pipe( + switchMap(scriptHash => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash)) + ); + } + getAddressTransactions$(address: string, txid?: string): Observable { let params = new HttpParams(); if (txid) { @@ -73,6 +92,16 @@ export class ElectrsApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params }); } + getScriptHashTransactions$(script: string, txid?: string): Observable { + let params = new HttpParams(); + if (txid) { + params = params.append('after_txid', txid); + } + return from(calcScriptHash$(script)).pipe( + switchMap(scriptHash => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/txs', { params })), + ); + } + getAsset$(assetId: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId); } From 0ce043cca9f63c51e3b9bf4685ce5fffc33f200d Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 23 Jul 2023 13:55:27 +0900 Subject: [PATCH 2/6] Fix esplora error messages --- backend/src/api/bitcoin/esplora-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 01294cc01..5bfff5730 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -111,11 +111,11 @@ class ElectrsApi implements AbstractBitcoinApi { } $getScriptHash(scripthash: string): Promise { - throw new Error('Method getAddress not implemented.'); + throw new Error('Method getScriptHash not implemented.'); } $getScriptHashTransactions(scripthash: string, txId?: string): Promise { - throw new Error('Method getAddressTransactions not implemented.'); + throw new Error('Method getScriptHashTransactions not implemented.'); } $getAddressPrefix(prefix: string): string[] { From 48b55eed468d9515e82518ebb31d5c41fc1080a4 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 23 Jul 2023 13:55:52 +0900 Subject: [PATCH 3/6] improve script hex parsing validation --- frontend/src/app/bitcoin.utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index 7ff0d9570..c4af730f6 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -283,7 +283,10 @@ export function isFeatureActive(network: string, height: number, feature: 'rbf' } export async function calcScriptHash$(script: string): Promise { - const buf = Uint8Array.from(script.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); + if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) { + throw new Error('script is not a valid hex string'); + } + const buf = Uint8Array.from(script.match(/.{2}/g).map((byte) => parseInt(byte, 16))); const hashBuffer = await crypto.subtle.digest('SHA-256', buf); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray From 0376467e6c9c211b7357cf61f0e1903cc631dbaa Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 23 Jul 2023 14:00:39 +0900 Subject: [PATCH 4/6] highlight matching P2PK inputs --- .../transactions-list/transactions-list.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3f88c61b0..d1d0673fe 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -23,7 +23,7 @@ From 56127dce6a19039c126858953a28fcb2eb475ae3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 23 Jul 2023 14:05:04 +0900 Subject: [PATCH 5/6] Add P2PK support to search bar --- .../src/app/components/search-form/search-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index ab42fe1f7..2361f8873 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit { } } - regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/; + regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|[0-9a-fA-F]{130})$/; regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/; regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/; regexBlockheight = /^[0-9]{1,9}$/; From ae183210e036b0c9455d21abb5d922cd5e70f083 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 23 Jul 2023 14:43:43 +0900 Subject: [PATCH 6/6] Updating pubkey width on mobile and desktop --- .../transactions-list/transactions-list.component.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.scss b/frontend/src/app/components/transactions-list/transactions-list.component.scss index 7356bad0b..14559089a 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.scss +++ b/frontend/src/app/components/transactions-list/transactions-list.component.scss @@ -143,7 +143,10 @@ h2 { .p2pk-address { display: inline-block; margin-left: 1em; - max-width: 140px; + max-width: 100px; + @media (min-width: 576px) { + max-width: 200px + } } .grey-info-text {