Merge branch 'master' into mononaut/fix-mobile-bottom-nav

This commit is contained in:
wiz 2023-01-27 03:49:07 +09:00 committed by GitHub
commit 2e7a701ca7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 37829 additions and 16586 deletions

View File

@ -18,6 +18,7 @@ import blocks from '../blocks';
import bitcoinClient from './bitcoin-client';
import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache';
class BitcoinRoutes {
public initRoutes(app: Application) {
@ -31,6 +32,8 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData)
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/replaces', this.getRbfHistory)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
@ -589,6 +592,28 @@ class BitcoinRoutes {
}
}
private async getRbfHistory(req: Request, res: Response) {
try {
const result = rbfCache.getReplaces(req.params.txId);
res.json(result || []);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getCachedTx(req: Request, res: Response) {
try {
const result = rbfCache.getTx(req.params.txId);
if (result) {
res.json(result);
} else {
res.status(404).send('not found');
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getTransactionOutspends(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getOutspends(req.params.txId);

View File

@ -35,24 +35,31 @@ export class Common {
}
static getFeesInRange(transactions: TransactionExtended[], rangeLength: number) {
const arr = [transactions[transactions.length - 1].effectiveFeePerVsize];
const filtered: TransactionExtended[] = [];
let lastValidRate = Infinity;
// filter out anomalous fee rates to ensure monotonic range
for (const tx of transactions) {
if (tx.effectiveFeePerVsize <= lastValidRate) {
filtered.push(tx);
lastValidRate = tx.effectiveFeePerVsize;
}
}
const arr = [filtered[filtered.length - 1].effectiveFeePerVsize];
const chunk = 1 / (rangeLength - 1);
let itemsToAdd = rangeLength - 2;
while (itemsToAdd > 0) {
arr.push(transactions[Math.floor(transactions.length * chunk * itemsToAdd)].effectiveFeePerVsize);
arr.push(filtered[Math.floor(filtered.length * chunk * itemsToAdd)].effectiveFeePerVsize);
itemsToAdd--;
}
arr.push(transactions[0].effectiveFeePerVsize);
arr.push(filtered[0].effectiveFeePerVsize);
return arr;
}
static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended } {
const matches: { [txid: string]: TransactionExtended } = {};
deleted
// The replaced tx must have at least one input with nSequence < maxint-1 (Thats the opt-in)
.filter((tx) => tx.vin.some((vin) => vin.sequence < 0xfffffffe))
.forEach((deletedTx) => {
const foundMatches = added.find((addedTx) => {
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
@ -61,7 +68,7 @@ export class Common {
&& addedTx.feePerVsize > deletedTx.feePerVsize
// Spends one or more of the same inputs
&& deletedTx.vin.some((deletedVin) =>
addedTx.vin.some((vin) => vin.txid === deletedVin.txid));
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
});
if (foundMatches) {
matches[deletedTx.txid] = foundMatches;

View File

@ -461,8 +461,8 @@ class DatabaseMigration {
await this.$executeQuery(this.getCreateCompactTransactionsTableQuery(), await this.$checkIfTableExists('compact_transactions'));
try {
await this.$convertCompactCpfpTables();
await this.$executeQuery('DROP TABLE IF EXISTS `cpfp_clusters`');
await this.$executeQuery('DROP TABLE IF EXISTS `transactions`');
await this.$executeQuery('DROP TABLE IF EXISTS `cpfp_clusters`');
await this.updateToSchemaVersion(52);
} catch(e) {
logger.warn('' + (e instanceof Error ? e.message : e));

View File

@ -55,11 +55,12 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
clChannelsDict[clChannel.short_channel_id] = clChannel;
clChannelsDictCount[clChannel.short_channel_id] = 1;
} else {
consolidatedChannelList.push(
await buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id])
);
delete clChannelsDict[clChannel.short_channel_id];
clChannelsDictCount[clChannel.short_channel_id]++;
const fullChannel = await buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id]);
if (fullChannel !== null) {
consolidatedChannelList.push(fullChannel);
delete clChannelsDict[clChannel.short_channel_id];
clChannelsDictCount[clChannel.short_channel_id]++;
}
}
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
@ -74,7 +75,10 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
channelProcessed = 0;
const keys = Object.keys(clChannelsDict);
for (const short_channel_id of keys) {
consolidatedChannelList.push(await buildIncompleteChannel(clChannelsDict[short_channel_id]));
const incompleteChannel = await buildIncompleteChannel(clChannelsDict[short_channel_id]);
if (incompleteChannel !== null) {
consolidatedChannelList.push(incompleteChannel);
}
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
@ -92,10 +96,13 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
* Convert two clightning "getchannels" entries into a full a lnd "describegraph.edges" format
* In this case, clightning knows the channel policy for both nodes
*/
async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILightningApi.Channel> {
async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILightningApi.Channel | null> {
const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0);
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannelA.short_channel_id);
if (!tx) {
return null;
}
const parts = clChannelA.short_channel_id.split('x');
const outputIdx = parts[2];
@ -115,8 +122,11 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILigh
* Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format
* In this case, clightning knows the channel policy of only one node
*/
async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel> {
async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel | null> {
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id);
if (!tx) {
return null;
}
const parts = clChannel.short_channel_id.split('x');
const outputIdx = parts[2];

View File

@ -210,7 +210,7 @@ class Mempool {
for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction]) {
// Store replaced transactions
rbfCache.add(rbfTransaction, rbfTransactions[rbfTransaction].txid);
rbfCache.add(this.mempoolCache[rbfTransaction], rbfTransactions[rbfTransaction].txid);
// Erase the replaced transactions from the local mempool
delete this.mempoolCache[rbfTransaction];
}
@ -236,6 +236,7 @@ class Mempool {
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;
if (lazyDeleteAt && lazyDeleteAt < now) {
delete this.mempoolCache[tx];
rbfCache.evict(tx);
}
}
}

View File

@ -1,31 +1,62 @@
export interface CachedRbf {
txid: string;
expires: Date;
}
import { TransactionExtended } from "../mempool.interfaces";
class RbfCache {
private cache: { [txid: string]: CachedRbf; } = {};
private replacedBy: { [txid: string]: string; } = {};
private replaces: { [txid: string]: string[] } = {};
private txs: { [txid: string]: TransactionExtended } = {};
private expiring: { [txid: string]: Date } = {};
constructor() {
setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
}
public add(replacedTxId: string, newTxId: string): void {
this.cache[replacedTxId] = {
expires: new Date(Date.now() + 1000 * 604800), // 1 week
txid: newTxId,
};
public add(replacedTx: TransactionExtended, newTxId: string): void {
this.replacedBy[replacedTx.txid] = newTxId;
this.txs[replacedTx.txid] = replacedTx;
if (!this.replaces[newTxId]) {
this.replaces[newTxId] = [];
}
this.replaces[newTxId].push(replacedTx.txid);
}
public get(txId: string): CachedRbf | undefined {
return this.cache[txId];
public getReplacedBy(txId: string): string | undefined {
return this.replacedBy[txId];
}
public getReplaces(txId: string): string[] | undefined {
return this.replaces[txId];
}
public getTx(txId: string): TransactionExtended | undefined {
return this.txs[txId];
}
// flag a transaction as removed from the mempool
public evict(txid): void {
this.expiring[txid] = new Date(Date.now() + 1000 * 86400); // 24 hours
}
private cleanup(): void {
const currentDate = new Date();
for (const c in this.cache) {
if (this.cache[c].expires < currentDate) {
delete this.cache[c];
for (const txid in this.expiring) {
if (this.expiring[txid] < currentDate) {
delete this.expiring[txid];
this.remove(txid);
}
}
}
// remove a transaction & all previous versions from the cache
private remove(txid): void {
// don't remove a transaction while a newer version remains in the mempool
if (this.replaces[txid] && !this.replacedBy[txid]) {
const replaces = this.replaces[txid];
delete this.replaces[txid];
for (const tx of replaces) {
// recursively remove prior versions from the cache
delete this.replacedBy[tx];
delete this.txs[tx];
this.remove(tx);
}
}
}

View File

@ -58,10 +58,10 @@ class WebsocketHandler {
client['track-tx'] = parsedMessage['track-tx'];
// Client is telling the transaction wasn't found
if (parsedMessage['watch-mempool']) {
const rbfCacheTx = rbfCache.get(client['track-tx']);
if (rbfCacheTx) {
const rbfCacheTxid = rbfCache.getReplacedBy(client['track-tx']);
if (rbfCacheTxid) {
response['txReplaced'] = {
txid: rbfCacheTx.txid,
txid: rbfCacheTxid,
};
client['track-tx'] = null;
} else {
@ -467,6 +467,7 @@ class WebsocketHandler {
for (const txId of txIds) {
delete _memPool[txId];
removed.push(txId);
rbfCache.evict(txId);
}
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {

View File

@ -210,6 +210,9 @@ class NetworkSyncService {
const channels = await channelsApi.$getChannelsWithoutCreatedDate();
for (const channel of channels) {
const transaction = await fundingTxFetcher.$fetchChannelOpenTx(channel.short_id);
if (!transaction) {
continue;
}
await DB.query(`
UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`,
[transaction.timestamp, channel.id]

View File

@ -71,7 +71,7 @@ class FundingTxFetcher {
this.running = false;
}
public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number}> {
public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number} | null> {
channelId = Common.channelIntegerIdToShortId(channelId);
if (this.fundingTxCache[channelId]) {
@ -102,6 +102,11 @@ class FundingTxFetcher {
const rawTx = await bitcoinClient.getRawTransaction(txid);
const tx = await bitcoinClient.decodeRawTransaction(rawTx);
if (!tx || !tx.vout || tx.vout.length < parseInt(outputIdx, 10) + 1 || tx.vout[outputIdx].value === undefined) {
logger.err(`Cannot find blockchain funding tx for channel id ${channelId}. Possible reasons are: bitcoin backend timeout or the channel shortId is not valid`);
return null;
}
this.fundingTxCache[channelId] = {
timestamp: block.time,
txid: txid,

View File

@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of December 17, 2022.
Signed: piterden

View File

@ -17,7 +17,7 @@ _Note: address lookups require an Electrum Server and will not work with this co
The default Docker configuration assumes you have the following configuration in your `bitcoin.conf` file:
```
```ini
txindex=1
server=1
rpcuser=mempool
@ -26,7 +26,7 @@ rpcpassword=mempool
If you want to use different credentials, specify them in the `docker-compose.yml` file:
```
```yaml
api:
environment:
MEMPOOL_BACKEND: "none"
@ -54,7 +54,7 @@ First, configure `bitcoind` as specified above, and make sure your Electrum Serv
Then, set the following variables in `docker-compose.yml` so Mempool can connect to your Electrum Server:
```
```yaml
api:
environment:
MEMPOOL_BACKEND: "electrum"
@ -85,7 +85,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
<br/>
`mempool-config.json`:
```
```json
"MEMPOOL": {
"NETWORK": "mainnet",
"BACKEND": "electrum",
@ -116,7 +116,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
MEMPOOL_NETWORK: ""
@ -153,8 +153,8 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
"CORE_RPC": {
```json
"CORE_RPC": {
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
@ -163,7 +163,7 @@ Corresponding `docker-compose.yml` overrides:
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
CORE_RPC_HOST: ""
@ -176,7 +176,7 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
```json
"ELECTRUM": {
"HOST": "127.0.0.1",
"PORT": 50002,
@ -185,7 +185,7 @@ Corresponding `docker-compose.yml` overrides:
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
ELECTRUM_HOST: ""
@ -197,14 +197,14 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
```json
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000"
},
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
ESPLORA_REST_API_URL: ""
@ -214,7 +214,7 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
```json
"SECOND_CORE_RPC": {
"HOST": "127.0.0.1",
"PORT": 8332,
@ -224,7 +224,7 @@ Corresponding `docker-compose.yml` overrides:
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
SECOND_CORE_RPC_HOST: ""
@ -237,7 +237,7 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
```json
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
@ -249,7 +249,7 @@ Corresponding `docker-compose.yml` overrides:
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
DATABASE_ENABLED: ""
@ -264,7 +264,7 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
```json
"SYSLOG": {
"ENABLED": true,
"HOST": "127.0.0.1",
@ -275,7 +275,7 @@ Corresponding `docker-compose.yml` overrides:
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
SYSLOG_ENABLED: ""
@ -289,7 +289,7 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
```json
"STATISTICS": {
"ENABLED": true,
"TX_PER_SECOND_SAMPLE_PERIOD": 150
@ -297,7 +297,7 @@ Corresponding `docker-compose.yml` overrides:
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
STATISTICS_ENABLED: ""
@ -308,7 +308,7 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
```json
"BISQ": {
"ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
@ -316,7 +316,7 @@ Corresponding `docker-compose.yml` overrides:
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
BISQ_ENABLED: ""
@ -327,7 +327,7 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
```json
"SOCKS5PROXY": {
"ENABLED": false,
"HOST": "127.0.0.1",
@ -338,7 +338,7 @@ Corresponding `docker-compose.yml` overrides:
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
SOCKS5PROXY_ENABLED: ""
@ -352,7 +352,7 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
```json
"PRICE_DATA_SERVER": {
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
@ -360,7 +360,7 @@ Corresponding `docker-compose.yml` overrides:
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
PRICE_DATA_SERVER_TOR_URL: ""
@ -371,7 +371,7 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
```json
"LIGHTNING": {
"ENABLED": false
"BACKEND": "lnd"
@ -383,7 +383,7 @@ Corresponding `docker-compose.yml` overrides:
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
LIGHTNING_ENABLED: false
@ -398,7 +398,7 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
```json
"LND": {
"TLS_CERT_PATH": ""
"MACAROON_PATH": ""
@ -407,7 +407,7 @@ Corresponding `docker-compose.yml` overrides:
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
LND_TLS_CERT_PATH: ""
@ -419,14 +419,14 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```
```json
"CLIGHTNING": {
"SOCKET": ""
}
```
Corresponding `docker-compose.yml` overrides:
```
```yaml
api:
environment:
CLIGHTNING_SOCKET: ""

View File

@ -138,6 +138,10 @@
"translation": "src/locale/messages.hi.xlf",
"baseHref": "/hi/"
},
"ne": {
"translation": "src/locale/messages.ne.xlf",
"baseHref": "/ne/"
},
"lt": {
"translation": "src/locale/messages.lt.xlf",
"baseHref": "/lt/"

View File

@ -116,6 +116,7 @@ export const languages: Language[] = [
// { code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
{ code: 'hi', name: 'हिन्दी' }, // Hindi
{ code: 'ne', name: 'नेपाली' }, // Nepalese
{ code: 'it', name: 'Italiano' }, // Italian
{ code: 'he', name: 'עברית' }, // Hebrew
{ code: 'ka', name: 'ქართული' }, // Georgian

View File

@ -1,11 +1,9 @@
<div class="container-xl">
<h1 i18n="shared.address">Address</h1>
<span class="address-link">
<a [routerLink]="['/address/' | relativeUrl, addressString]">
<span class="d-inline d-lg-none">{{ addressString | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ addressString }}</span>
</a>
<app-clipboard [text]="addressString"></app-clipboard>
<app-truncate [text]="addressString" [lastChars]="8" [link]="['/address/' | relativeUrl, addressString]">
<app-clipboard [text]="addressString"></app-clipboard>
</app-truncate>
</span>
<br>

View File

@ -6,12 +6,12 @@
<h1 i18n="shared.transaction">Transaction</h1>
</div>
<span class="tx-link float-left">
<a [routerLink]="['/tx' | relativeUrl, bisqTx.id]">
<span class="d-inline d-lg-none">{{ bisqTx.id | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ bisqTx.id }}</span>
</a>
<app-clipboard [text]="bisqTx.id"></app-clipboard>
<span class="tx-link">
<span class="txid">
<app-truncate [text]="bisqTx.id" [lastChars]="12" [link]="['/tx/' | relativeUrl, bisqTx.id]">
<app-clipboard [text]="bisqTx.id"></app-clipboard>
</app-truncate>
</span>
</span>
<span class="grow"></span>
<div class="container-buttons">

View File

@ -6,17 +6,16 @@
<div class="col-md">
<div class="row d-flex justify-content-between">
<div class="title-wrapper">
<h1 class="title truncated"><span class="first">{{addressString.slice(0,-4)}}</span><span class="last-four">{{addressString.slice(-4)}}</span></h1>
<h1 class="title"><app-truncate [text]="addressString"></app-truncate></h1>
</div>
</div>
<table class="table table-borderless table-striped">
<tbody>
<tr *ngIf="addressInfo && addressInfo.unconfidential">
<td i18n="address.unconfidential">Unconfidential</td>
<td><a [routerLink]="['/address/' | relativeUrl, addressInfo.unconfidential]">
<span class="d-inline d-lg-none">{{ addressInfo.unconfidential | shortenString : 14 }}</span>
<span class="d-none d-lg-inline">{{ addressInfo.unconfidential }}</span>
</a> <app-clipboard [text]="addressInfo.unconfidential"></app-clipboard></td>
<td>
<app-truncate [text]="addressInfo.unconfidential" [lastChars]="7" [link]="['/address/' | relativeUrl, addressInfo.unconfidential]"></app-truncate>
</td>
</tr>
<ng-template [ngIf]="!address.electrum">
<tr>

View File

@ -2,11 +2,9 @@
<div class="title-address">
<h1 i18n="shared.address">Address</h1>
<div class="tx-link">
<a [routerLink]="['/address/' | relativeUrl, addressString]" >
<span class="d-inline d-lg-none">{{ addressString | shortenString : 18 }}</span>
<span class="d-none d-lg-inline">{{ addressString }}</span>
</a>
<app-clipboard [text]="addressString"></app-clipboard>
<app-truncate [text]="addressString" [lastChars]="8" [link]="['/address/' | relativeUrl, addressString]">
<app-clipboard [text]="addressString"></app-clipboard>
</app-truncate>
</div>
</div>
@ -21,10 +19,11 @@
<tbody>
<tr *ngIf="addressInfo && addressInfo.unconfidential">
<td i18n="address.unconfidential">Unconfidential</td>
<td><a [routerLink]="['/address/' | relativeUrl, addressInfo.unconfidential]">
<span class="d-inline d-lg-none">{{ addressInfo.unconfidential | shortenString : 14 }}</span>
<span class="d-none d-lg-inline">{{ addressInfo.unconfidential }}</span>
</a> <app-clipboard [text]="addressInfo.unconfidential"></app-clipboard></td>
<td>
<app-truncate [text]="addressInfo.unconfidential" [lastChars]="8" [link]="['/address/' | relativeUrl, addressInfo.unconfidential]">
<app-clipboard [text]="addressInfo.unconfidential"></app-clipboard>
</app-truncate>
</td>
</tr>
<ng-template [ngIf]="!address.electrum">
<tr>

View File

@ -2,11 +2,9 @@
<div class="title-asset">
<h1 i18n="asset|Liquid Asset page title">Asset</h1>
<div class="tx-link">
<a [routerLink]="['/assets/asset/' | relativeUrl, assetString]">
<span class="d-inline d-lg-none">{{ assetString | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ assetString }}</span>
</a>
<app-clipboard [text]="assetString"></app-clipboard>
<app-truncate [text]="assetString" [lastChars]="8" [link]="['/assets/asset/' | relativeUrl, assetString]">
<app-clipboard [text]="assetString"></app-clipboard>
</app-truncate>
</div>
</div>

View File

@ -213,6 +213,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
},
},
legend: (data.series.length === 0) ? undefined : {
padding: [10, 75],
data: data.legends,
selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? {
'Min': true,

View File

@ -10,12 +10,13 @@ const defaultHoverColor = hexToColor('1bd8f4');
const feeColors = mempoolFeeColors.map(hexToColor);
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1));
const auditColors = {
censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('0099ff'),
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
}
};
// convert from this class's update format to TxSprite's update format
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
@ -161,13 +162,13 @@ export default class TxView implements TransactionStripped {
case 'censored':
return auditColors.censored;
case 'missing':
return auditColors.missing;
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'fresh':
return auditColors.missing;
case 'added':
return auditColors.added;
case 'selected':
return auditColors.selected;
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'found':
if (this.context === 'projected') {
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];

View File

@ -35,12 +35,12 @@
<tr *ngIf="tx && tx.status && tx.status.length">
<td class="td-width" i18n="transaction.audit-status">Audit status</td>
<ng-container [ngSwitch]="tx?.status">
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
<td *ngSwitchCase="'missing'" i18n="transaction.audit.marginal">marginal fee rate</td>
<td *ngSwitchCase="'fresh'" i18n="transaction.audit.recently-broadcast">recently broadcast</td>
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
<td *ngSwitchCase="'selected'" i18n="transaction.audit.marginal">marginal fee rate</td>
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">Match</td>
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">Removed</td>
<td *ngSwitchCase="'missing'" i18n="transaction.audit.marginal">Marginal fee rate</td>
<td *ngSwitchCase="'fresh'" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</td>
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">Added</td>
<td *ngSwitchCase="'selected'" i18n="transaction.audit.marginal">Marginal fee rate</td>
</ng-container>
</tr>
</tbody>

View File

@ -4,8 +4,7 @@ import { StateService } from '../../services/state.service';
import { specialBlocks } from '../../app.constants';
import { BlockExtended } from '../../interfaces/node-api.interface';
import { Location } from '@angular/common';
import { config } from 'process';
import { CacheService } from 'src/app/services/cache.service';
import { CacheService } from '../../services/cache.service';
interface BlockchainBlock extends BlockExtended {
placeholder?: boolean;

View File

@ -64,26 +64,6 @@
white-space: nowrap;
margin: 0;
display: inline-block;
&.truncated {
text-overflow: unset;
display: flex;
flex-direction: row;
align-items: baseline;
.first {
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
margin-right: -2px;
}
.last-four {
flex-shrink: 0;
flex-grow: 0;
}
}
}
::ng-deep .title-wrapper {

View File

@ -13,21 +13,21 @@
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.rewards-per-tx" i18n-ngbTooltip="mining.rewards-per-tx"
ngbTooltip="Reward Per Tx" placement="bottom" #rewardspertx [disableTooltip]="!isEllipsisActive(rewardspertx)">Reward Per Tx</h5>
<div class="card-text" i18n-ngbTooltip="mining.rewards-per-tx-desc" ngbTooltip="Average miners' reward per transaction in the past 144 blocks" placement="bottom">
<h5 class="card-title" i18n="mining.fees-per-block" i18n-ngbTooltip="mining.fees-per-block"
ngbTooltip="Avg Block Fees" placement="bottom" #rewardsperblock [disableTooltip]="!isEllipsisActive(rewardsperblock)">Avg Block Fees</h5>
<div class="card-text" i18n-ngbTooltip="mining.fees-per-block-desc" ngbTooltip="Average fees per block in the past 144 blocks" placement="bottom">
<div class="fee-text">
{{ rewardStats.rewardPerTx | amountShortener: 2 }}
<span i18n="shared.sat-vbyte|sat/vB">sats/tx</span>
{{ (rewardStats.feePerBlock / 100000000) | amountShortener: 4 }}
<span i18n="shared.btc-block|BTC/block">BTC/block</span>
</div>
<span class="fiat">
<app-fiat [value]="rewardStats.rewardPerTx"></app-fiat>
<app-fiat [value]="rewardStats.feePerBlock"></app-fiat>
</span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.average-fee" i18n-ngbTooltip="mining.average-fee"
ngbTooltip="Average Fee" placement="bottom" #averagefee [disableTooltip]="!isEllipsisActive(averagefee)">Average Fee</h5>
ngbTooltip="Avg Tx Fee" placement="bottom" #averagefee [disableTooltip]="!isEllipsisActive(averagefee)">Avg Tx Fee</h5>
<div class="card-text" i18n-ngbTooltip="mining.average-fee" ngbTooltip="Fee paid on average for each transaction in the past 144 blocks" placement="bottom">
<div class="fee-text">{{ rewardStats.feePerTx | amountShortener: 2 }}
<span i18n="shared.sat-vbyte|sat/vB">sats/tx</span>

View File

@ -42,8 +42,8 @@ export class RewardStatsComponent implements OnInit {
map((stats) => {
return {
totalReward: stats.totalReward,
rewardPerTx: stats.totalReward / stats.totalTx,
feePerTx: stats.totalFee / stats.totalTx,
feePerBlock: stats.totalFee / 144,
};
})
);

View File

@ -4,7 +4,7 @@
</app-preview-title>
<div class="row d-flex justify-content-between full-width-row">
<div class="title-wrapper">
<h1 class="title truncated"><span class="first">{{txId.slice(0,-4)}}</span><span class="last-four">{{txId.slice(-4)}}</span></h1>
<h1 class="title truncated"><app-truncate [text]="txId"></app-truncate></h1>
</div>
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
<app-tx-features [tx]="tx"></app-tx-features>

View File

@ -3,21 +3,25 @@
<div class="title-block">
<div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
<a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]">
<span class="d-inline d-lg-none">{{ rbfTransaction.txid | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span>
</a>
<app-truncate [text]="rbfTransaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, rbfTransaction.txid]"></app-truncate>
</div>
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size">
<div *ngIf="rbfReplaces?.length" class="alert alert-mempool" role="alert">
<span i18n="transaction.rbf.replaced|RBF replaced">This transaction replaced:</span>
<div class="tx-list">
<app-truncate [text]="replaced" [lastChars]="12" *ngFor="let replaced of rbfReplaces" [link]="['/tx/' | relativeUrl, replaced]"></app-truncate>
</div>
</div>
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size || tx">
<h1 i18n="shared.transaction">Transaction</h1>
<span class="tx-link float-left">
<a [routerLink]="['/tx/' | relativeUrl, txId]">
<span class="d-inline d-lg-none">{{ txId | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ txId }}</span>
</a>
<app-clipboard [text]="txId"></app-clipboard>
<span class="tx-link">
<span class="txid">
<app-truncate [text]="txId" [lastChars]="12" [link]="['/tx/' | relativeUrl, txId]">
<app-clipboard [text]="txId"></app-clipboard>
</app-truncate>
</span>
</span>
<div class="container-buttons">
@ -28,7 +32,10 @@
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
</button>
</ng-template>
<ng-template [ngIf]="tx && !tx?.status.confirmed">
<ng-template [ngIf]="tx && !tx?.status?.confirmed && replaced">
<button type="button" class="btn btn-sm btn-danger" i18n="transaction.unconfirmed|Transaction unconfirmed state">Replaced</button>
</ng-template>
<ng-template [ngIf]="tx && !tx?.status?.confirmed && !replaced">
<button type="button" class="btn btn-sm btn-danger" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
</ng-template>
</div>
@ -91,7 +98,7 @@
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<ng-template [ngIf]="transactionTime !== 0">
<ng-template [ngIf]="transactionTime !== 0 && !replaced">
<tr *ngIf="transactionTime === -1; else firstSeenTmpl">
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
@ -103,7 +110,7 @@
</tr>
</ng-template>
</ng-template>
<tr>
<tr *ngIf="!replaced">
<td class="td-width" i18n="transaction.eta|Transaction ETA">ETA</td>
<td>
<ng-template [ngIf]="txInBlockIndex === undefined" [ngIfElse]="estimationTmpl">
@ -144,12 +151,12 @@
<br>
<h2 class="text-left">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="xs"></fa-icon></h2>
<div class="box">
<table class="table table-borderless table-striped">
<div class="box cpfp-details">
<table class="table table-fixed table-borderless table-striped">
<thead>
<tr>
<th i18n="transactions-list.vout.scriptpubkey-type">Type</th>
<th i18n="dashboard.latest-transactions.txid">TXID</th>
<th class="txids" i18n="dashboard.latest-transactions.txid">TXID</th>
<th class="d-none d-lg-table-cell" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</th>
<th i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
<th class="d-none d-lg-table-cell"></th>
@ -159,10 +166,8 @@
<ng-template [ngIf]="cpfpInfo?.descendants?.length">
<tr *ngFor="let cpfpTx of cpfpInfo.descendants">
<td><span class="badge badge-primary" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
<span class="d-inline d-lg-none">{{ cpfpTx.txid | shortenString : 8 }}</span>
<span class="d-none d-lg-inline">{{ cpfpTx.txid }}</span>
</a>
<td>
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
</td>
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
<td>{{ cpfpTx.fee / (cpfpTx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
@ -172,11 +177,8 @@
<ng-template [ngIf]="cpfpInfo?.bestDescendant">
<tr>
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td>
<a [routerLink]="['/tx' | relativeUrl, cpfpInfo.bestDescendant.txid]">
<span class="d-inline d-lg-none">{{ cpfpInfo.bestDescendant.txid | shortenString : 8 }}</span>
<span class="d-none d-lg-inline">{{ cpfpInfo.bestDescendant.txid }}</span>
</a>
<td class="txids">
<app-truncate [text]="cpfpInfo.bestDescendant.txid" [link]="['/tx' | relativeUrl, cpfpInfo.bestDescendant.txid]"></app-truncate>
</td>
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight / 4 | vbytes: 2"></td>
<td>{{ cpfpInfo.bestDescendant.fee / (cpfpInfo.bestDescendant.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
@ -186,10 +188,8 @@
<ng-template [ngIf]="cpfpInfo?.ancestors?.length">
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
<span class="d-inline d-lg-none">{{ cpfpTx.txid | shortenString : 8 }}</span>
<span class="d-none d-lg-inline">{{ cpfpTx.txid }}</span>
</a>
<td class="txids">
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
</td>
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
<td>{{ cpfpTx.fee / (cpfpTx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>

View File

@ -19,22 +19,32 @@
}
}
.tx-link {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: baseline;
width: 0;
max-width: 100%;
margin-right: 0px;
margin-bottom: 0px;
margin-top: 8px;
display: inline-block;
width: 100%;
flex-shrink: 0;
@media (min-width: 651px) {
display: flex;
width: auto;
flex-grow: 1;
margin-bottom: 0px;
margin-right: 1em;
top: 1px;
position: relative;
}
@media (max-width: 650px) {
width: 100%;
order: 3;
}
.txid {
width: 200px;
min-width: 200px;
flex-grow: 1;
}
}
.td-width {
@ -188,4 +198,16 @@
margin-left: 8px;
}
}
}
.cpfp-details {
.txids {
width: 60%;
}
}
.tx-list {
.alert-link {
display: block;
}
}

View File

@ -40,15 +40,21 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
transactionTime = -1;
subscription: Subscription;
fetchCpfpSubscription: Subscription;
fetchRbfSubscription: Subscription;
fetchCachedTxSubscription: Subscription;
txReplacedSubscription: Subscription;
blocksSubscription: Subscription;
queryParamsSubscription: Subscription;
urlFragmentSubscription: Subscription;
fragmentParams: URLSearchParams;
rbfTransaction: undefined | Transaction;
replaced: boolean = false;
rbfReplaces: string[];
cpfpInfo: CpfpInfo | null;
showCpfpDetails = false;
fetchCpfp$ = new Subject<string>();
fetchRbfHistory$ = new Subject<string>();
fetchCachedTx$ = new Subject<string>();
now = new Date().getTime();
timeAvg$: Observable<number>;
liquidUnblinding = new LiquidUnblinding();
@ -122,7 +128,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
}),
delay(2000)
)))
)),
catchError(() => {
return of(null);
})
)
),
catchError(() => {
return of(null);
@ -155,6 +165,49 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.cpfpInfo = cpfpInfo;
});
this.fetchRbfSubscription = this.fetchRbfHistory$
.pipe(
switchMap((txId) =>
this.apiService
.getRbfHistory$(txId)
),
catchError(() => {
return of([]);
})
).subscribe((replaces) => {
this.rbfReplaces = replaces;
});
this.fetchCachedTxSubscription = this.fetchCachedTx$
.pipe(
switchMap((txId) =>
this.apiService
.getRbfCachedTx$(txId)
),
catchError(() => {
return of(null);
})
).subscribe((tx) => {
if (!tx) {
return;
}
this.tx = tx;
if (tx.fee === undefined) {
this.tx.fee = 0;
}
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.isLoadingTx = false;
this.error = undefined;
this.waitingForTransaction = false;
this.graphExpanded = false;
this.setupGraph();
if (!this.tx?.status?.confirmed) {
this.fetchRbfHistory$.next(this.tx.txid);
}
});
this.subscription = this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
@ -268,6 +321,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} else {
this.fetchCpfp$.next(this.tx.txid);
}
this.fetchRbfHistory$.next(this.tx.txid);
}
setTimeout(() => { this.applyFragment(); }, 0);
},
@ -299,6 +353,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
this.rbfTransaction = rbfTransaction;
this.cacheService.setTxCache([this.rbfTransaction]);
this.replaced = true;
if (rbfTransaction && !this.tx) {
this.fetchCachedTx$.next(this.txId);
}
});
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
@ -364,8 +422,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.waitingForTransaction = false;
this.isLoadingTx = true;
this.rbfTransaction = undefined;
this.replaced = false;
this.transactionTime = -1;
this.cpfpInfo = null;
this.rbfReplaces = [];
this.showCpfpDetails = false;
document.body.scrollTo(0, 0);
this.leaveTransaction();
@ -431,6 +491,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
ngOnDestroy() {
this.subscription.unsubscribe();
this.fetchCpfpSubscription.unsubscribe();
this.fetchRbfSubscription.unsubscribe();
this.fetchCachedTxSubscription.unsubscribe();
this.txReplacedSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
this.queryParamsSubscription.unsubscribe();

View File

@ -1,16 +1,14 @@
<ng-container *ngFor="let tx of transactions; let i = index; trackBy: trackByFn">
<div *ngIf="!transactionPage" class="header-bg box tx-page-container">
<a class="float-left" [routerLink]="['/tx/' | relativeUrl, tx.txid]">
<span style="float: left;" class="d-block d-md-none">{{ tx.txid | shortenString : 16 }}</span>
<span style="float: left;" class="d-none d-md-block">{{ tx.txid }}</span>
<a class="tx-link" [routerLink]="['/tx/' | relativeUrl, tx.txid]">
<app-truncate [text]="tx.txid"></app-truncate>
</a>
<div class="float-right">
<div>
<ng-template [ngIf]="tx.status.confirmed">&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-template>
<ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
<i><app-time-since [time]="tx.firstSeen" [fastRender]="true"></app-time-since></i>
</ng-template>
</div>
<div class="clearfix"></div>
</div>
<div class="header-bg box" infiniteScroll [alwaysCallback]="true" [infiniteScrollDistance]="2" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="onScroll()" [attr.data-cy]="'tx-' + i">
@ -18,7 +16,7 @@
<div *ngIf="errorUnblinded" class="error-unblinded">{{ errorUnblinded }}</div>
<div class="row">
<div class="col">
<table class="table table-borderless smaller-text table-sm table-tx-vin">
<table class="table table-fixed table-borderless smaller-text table-sm table-tx-vin">
<tbody>
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx.vin.slice(0, getVinLimit(tx))" [ngForTrackBy]="trackByIndexFn">
<tr [ngClass]="{
@ -49,7 +47,7 @@
</ng-template>
</ng-template>
</td>
<td>
<td class="address-cell">
<div [ngSwitch]="true">
<ng-container *ngSwitchCase="vin.is_coinbase"><span i18n="transactions-list.coinbase">Coinbase</span><ng-template [ngIf]="network !== 'liquid' && network !== 'liquidtestnet'">&nbsp;<span i18n="transactions-list.newly-generated-coins">(Newly Generated Coins)</span></ng-template><br /><a placement="bottom" [ngbTooltip]="vin.scriptsig | hex2ascii"><span class="badge badge-secondary scriptmessage longer">{{ vin.scriptsig | hex2ascii }}</span></a></ng-container>
<ng-container *ngSwitchCase="vin.is_pegin">
@ -66,12 +64,8 @@
</ng-template>
</ng-template>
<ng-template #defaultAddress>
<a class="shortable-address" *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vin.prevout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-inline-flex justify-content-start">
<span class="addr-left flex-grow-1" [style]="vin.prevout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vin.prevout.scriptpubkey_address }}</span>
<span *ngIf="vin.prevout.scriptpubkey_address.length > 40" class="addr-right">{{ vin.prevout.scriptpubkey_address | capAddress: 40: 10 }}</span>
</span>
<a class="address" *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<app-truncate [text]="vin.prevout.scriptpubkey_address" [lastChars]="8"></app-truncate>
</a>
<ng-template #vinScriptPubkeyType>
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
@ -100,7 +94,7 @@
</tr>
<tr *ngIf="(showDetails$ | async) === true">
<td colspan="3" class="details-container" >
<table class="table table-striped table-borderless details-table mb-3">
<table class="table table-striped table-fixed table-borderless details-table mb-3">
<tbody>
<ng-template [ngIf]="vin.scriptsig">
<tr>
@ -112,9 +106,23 @@
<td style="text-align: left;">{{ vin.scriptsig }}</td>
</tr>
</ng-template>
<tr *ngIf="vin.witness">
<tr *ngIf="vin.witness" class="vin-witness">
<td i18n="transactions-list.witness">Witness</td>
<td style="text-align: left;">{{ vin.witness.join(' ') }}</td>
<td style="text-align: left;">
<ng-container *ngFor="let witness of vin.witness; index as i">
<input type="checkbox" [id]="'tx' + vindex + 'witness' + i" style="display: none;">
<p class="witness-item" [class.accordioned]="witness.length > 1000">
{{ witness }}
</p>
<div class="witness-toggle" *ngIf="witness.length > 1000">
<span class="ellipsis">...</span>
<label [for]="'tx' + vindex + 'witness' + i" class="btn btn-sm btn-primary mt-2">
<span class="show-all" i18n="show-all">Show all</span>
<span class="show-less" i18n="show-less">Show less</span>
</label>
</div>
</ng-container>
</td>
</tr>
<tr *ngIf="vin.inner_redeemscript_asm">
<td i18n="transactions-list.p2sh-redeem-script">P2SH redeem script</td>
@ -153,7 +161,7 @@
<ng-template #showMoreInputsLabel>
<span i18n="show-more">Show more</span>
</ng-template>
({{ tx.vin.length - getVinLimit(tx) }} <span i18n="inputs-remaining">remaining</span>)
(<ng-container *ngTemplateOutlet="xRemaining; context: {$implicit: tx.vin.length - getVinLimit(tx)}"></ng-container>)
</button>
</td>
</tr>
@ -162,20 +170,16 @@
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col mobile-bottomcol">
<table class="table table-borderless smaller-text table-sm table-tx-vout">
<table class="table table-fixed table-borderless smaller-text table-sm table-tx-vout">
<tbody>
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx.vout.slice(0, getVoutLimit(tx))" [ngForTrackBy]="trackByIndexFn">
<tr [ngClass]="{
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
}">
<td>
<a class="shortable-address" *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-inline-flex justify-content-start">
<span class="addr-left flex-grow-1" [style]="vout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vout.scriptpubkey_address }}</span>
<span *ngIf="vout.scriptpubkey_address.length > 40" class="addr-right">{{ vout.scriptpubkey_address | capAddress: 40: 10 }}</span>
</span>
<td class="address-cell">
<a class="address" *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<app-truncate [text]="vout.scriptpubkey_address" [lastChars]="8"></app-truncate>
</a>
<div>
<app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
@ -185,13 +189,11 @@
<ng-container i18n="transactions-list.peg-out-to">Peg-out to <ng-container *ngTemplateOutlet="pegOutLink"></ng-container></ng-container>
<ng-template #pegOutLink>
<a *ngIf="stateService.env.BASE_MODULE === 'liquid'; else localPegoutLink" [attr.href]="'https://mempool.space/address/' + vout.pegout.scriptpubkey_address" title="{{ vout.pegout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vout.pegout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-block">{{ vout.pegout.scriptpubkey_address | shortenString : 35 }}</span>
<app-truncate [text]="vout.pegout.scriptpubkey_address"></app-truncate>
</a>
<ng-template #localPegoutLink>
<a [routerLink]="['/address/', vout.pegout.scriptpubkey_address]" title="{{ vout.pegout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vout.pegout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-block">{{ vout.pegout.scriptpubkey_address | shortenString : 35 }}</span>
<app-truncate [text]="vout.pegout.scriptpubkey_address"></app-truncate>
</a>
</ng-template>
</ng-template>
@ -270,7 +272,7 @@
<ng-template #showMoreOutputsLabel>
<span i18n="show-more">Show more</span>
</ng-template>
({{ tx.vout.length - getVoutLimit(tx) }} <span i18n="outputs-remaining">remaining</span>)
(<ng-container *ngTemplateOutlet="xRemaining; context: {$implicit: tx.vout.length - getVoutLimit(tx)}"></ng-container>)
</button>
</td>
</tr>
@ -324,3 +326,5 @@
<br />
<a [routerLink]="['/assets/asset/' | relativeUrl, item.asset]">{{ item.asset | shortenString : 13 }}</a>
</ng-template>
<ng-template #xRemaining let-x i18n="x-remaining">{{ x }} remaining</ng-template>

View File

@ -1,6 +1,6 @@
.arrow-td {
width: 20px;
width: 30px;
padding-top: 0;
padding-bottom: 0;
}
@ -45,6 +45,10 @@
}
}
td.amount {
width: 32.5%;
}
.extra-info {
display: none;
@media (min-width: 576px) {
@ -81,6 +85,10 @@
}
.tx-page-container {
display: flex;
flex-direction: row;
align-items: baseline;
white-space: nowrap;
padding: 10px;
margin-bottom: 10px;
margin-top: 10px;
@ -97,9 +105,7 @@
&:first-child {
color: #ffffff66;
white-space: pre-wrap;
@media (min-width: 476px) {
white-space: nowrap;
}
width: 150px;
}
&:nth-child(2) {
word-break: break-all;
@ -130,14 +136,7 @@ h2 {
margin-top: 10px;
}
.addr-left {
font-family: monospace;
overflow: hidden;
text-overflow: ellipsis;
margin-right: -7px
}
.addr-right {
.address {
font-family: monospace;
}
@ -146,3 +145,50 @@ h2 {
font-style: italic;
font-size: 12px;
}
.tx-link {
width: 0;
flex-grow: 1;
margin-inline-end: 2em;
}
.vin-witness {
.witness-item.accordioned {
max-height: 300px;
overflow: hidden;
}
input:checked + .witness-item.accordioned {
max-height: none;
}
.witness-toggle {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 1em;
.show-all {
display: inline;
}
.show-less {
display: none;
}
.ellipsis {
visibility: visible;
}
}
input:checked ~ .witness-toggle {
.show-all {
display: none;
}
.show-less {
display: inline;
}
.ellipsis {
visibility: hidden;
}
}
}

View File

@ -31,8 +31,7 @@
<p *ngIf="!isConnector">Peg Out</p>
<p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
<p class="address">
<span class="first">{{ line.pegout.slice(0, -4) }}</span>
<span class="last-four">{{ line.pegout.slice(-4) }}</span>
<app-truncate [text]="line.pegout"></app-truncate>
</p>
</ng-container>
</ng-template>
@ -49,8 +48,7 @@
<ng-container *ngIf="isConnector && line.txid">
<p>
<span i18n="transaction">Transaction</span>&nbsp;
<span class="first">{{ line.txid.slice(0, 8) }}</span>...
<span class="last-four">{{ line.txid.slice(-4) }}</span>
<app-truncate [text]="line.txid"></app-truncate>
</p>
<ng-container [ngSwitch]="line.type">
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span>&nbsp; #{{ line.vout + 1 }}</p>
@ -60,8 +58,7 @@
<p *ngIf="line.value == null && line.confidential" i18n="shared.confidential">Confidential</p>
<p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
<p *ngIf="line.type !== 'fee' && line.address" class="address">
<span class="first">{{ line.address.slice(0, -4) }}</span>
<span class="last-four">{{ line.address.slice(-4) }}</span>
<app-truncate [text]="line.address"></app-truncate>
</p>
</ng-template>
</div>

View File

@ -17,22 +17,5 @@
.address {
width: 100%;
max-width: 100%;
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: flex-start;
.first {
flex-grow: 0;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
margin-right: -2px;
}
.last-four {
flex-shrink: 0;
flex-grow: 0;
}
}
}

View File

@ -127,7 +127,11 @@
</thead>
<tbody>
<tr *ngFor="let transaction of transactions$ | async; let i = index;">
<td class="table-cell-txid"><a [routerLink]="['/tx' | relativeUrl, transaction.txid]">{{ transaction.txid | shortenString : 10 }}</a></td>
<td class="table-cell-txid">
<a [routerLink]="['/tx' | relativeUrl, transaction.txid]">
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
</a>
</td>
<td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td>
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
<td class="table-cell-fees">{{ transaction.fee / transaction.vsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>

View File

@ -12,7 +12,8 @@ if (browserWindowEnv.BASE_MODULE && (browserWindowEnv.BASE_MODULE === 'bisq' ||
routes = [
{
path: '',
redirectTo: 'api/rest'
redirectTo: 'api/rest',
pathMatch: 'full'
},
{
path: 'api/:type',
@ -20,11 +21,13 @@ if (browserWindowEnv.BASE_MODULE && (browserWindowEnv.BASE_MODULE === 'bisq' ||
},
{
path: 'api',
redirectTo: 'api/rest'
redirectTo: 'api/rest',
pathMatch: 'full'
},
{
path: '**',
redirectTo: 'api/rest'
redirectTo: 'api/rest',
pathMatch: 'full'
}
];
} else {

View File

@ -1,10 +1,9 @@
<div class="mb-2 box-top">
<div class="box-left text-truncate">
<h3 class="mb-0 text-truncate">{{ channel.alias || '?' }}</h3>
<a [routerLink]="['/lightning/node' | relativeUrl, channel.public_key]" >
{{ channel.public_key | shortenString : 12 }}
</a>
<app-clipboard [text]="channel.public_key"></app-clipboard>
<app-truncate [text]="channel.public_key" [lastChars]="6" [link]="['/lightning/node' | relativeUrl, channel.public_key]">
<app-clipboard [text]="channel.public_key"></app-clipboard>
</app-truncate>
</div>
<div class="box-right">
<div class="second-line"><ng-container *ngTemplateOutlet="xChannels; context: {$implicit: channel.channels }"></ng-container></div>

View File

@ -2,7 +2,7 @@
display: flex;
flex-direction: row;
@media (max-width: 768px) {
@media (max-width: 767.98px) {
flex-direction: column;
}
}
@ -10,16 +10,13 @@
.tx-link {
display: flex;
flex-grow: 1;
@media (min-width: 650px) {
@media (min-width: 768px) {
top: 1px;
position: relative;
align-self: end;
margin-left: 15px;
margin-top: 0px;
margin-bottom: -3px;
}
@media (min-width: 768px) {
margin-bottom: 4px;
top: 1px;
position: relative;
}
@media (max-width: 768px) {
order: 2;

View File

@ -49,10 +49,9 @@
<td class="alias text-left">
<div>{{ node.alias || '?' }}</div>
<div class="second-line">
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
<span>{{ node.public_key | shortenString : publicKeySize }}</span>
</a>
<app-clipboard [text]="node.public_key" size="small"></app-clipboard>
<app-truncate [text]="node.public_key" [maxWidth]="200" [lastChars]="6" [link]="['/lightning/node' | relativeUrl, node.public_key]">
<app-clipboard [text]="node.public_key" size="small"></app-clipboard>
</app-truncate>
</div>
</td>
<td class="alias text-left d-none d-md-table-cell">

View File

@ -56,7 +56,7 @@
<!-- Top nodes per capacity -->
<div class="col">
<div class="card" style="height: 409px">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/liquidity' | relativeUrl]">
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
@ -70,7 +70,7 @@
<!-- Top nodes per channels -->
<div class="col">
<div class="card" style="height: 409px">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/connectivity' | relativeUrl]">
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>

View File

@ -3,11 +3,11 @@
<div class="title-container mb-2" *ngIf="!error">
<h1 class="mb-0 text-truncate">{{ node.alias }}</h1>
<span class="tx-link">
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
<span class="d-inline d-lg-none">{{ node.public_key | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ node.public_key }}</span>
</a>
<app-clipboard [text]="node.public_key"></app-clipboard>
<span class="node-id">
<app-truncate [text]="node.public_key" [lastChars]="8" [link]="['/lightning/node' | relativeUrl, node.public_key]">
<app-clipboard [text]="node.public_key"></app-clipboard>
</app-truncate>
</span>
</span>
</div>
@ -215,7 +215,7 @@
</div>
<div *ngIf="hasDetails" class="text-right mt-3">
<button type="button" class="btn btn-outline-info btn-sm btn-details" (click)="toggleShowDetails()" i18n="node.details|Node Details">Details</button>
<button type="button" class="btn btn-outline-info btn-sm btn-details" (click)="toggleShowDetails()" i18n="transaction.details|Transaction Details">Details</button>
</div>
<div *ngIf="!error">

View File

@ -8,6 +8,11 @@
flex-wrap: wrap;
}
.node-id {
width: 0;
flex-grow: 1;
}
.qr-wrapper {
background-color: #FFF;
padding: 10px;

View File

@ -21,7 +21,7 @@
<div class="spinner-border text-light"></div>
</div>
<table class="table table-borderless text-center m-auto" style="max-width: 900px">
<table class="table table-borderless table-fixed text-center m-auto" style="max-width: 900px">
<thead>
<tr>
<th class="text-left rank" i18n="mining.rank">Rank</th>

View File

@ -42,14 +42,14 @@
}
.rank {
width: 20%;
width: 8%;
@media (max-width: 576px) {
display: none
}
}
.name {
width: 20%;
width: 36%;
@media (max-width: 576px) {
width: 80%;
max-width: 150px;
@ -59,21 +59,21 @@
}
.share {
width: 20%;
width: 15%;
@media (max-width: 576px) {
display: none
}
}
.nodes {
width: 20%;
width: 15%;
@media (max-width: 576px) {
width: 10%;
}
}
.capacity {
width: 20%;
width: 26%;
@media (max-width: 576px) {
width: 10%;
max-width: 100px;
@ -91,3 +91,8 @@ a:hover .link {
.flag {
font-size: 20px;
}
.text-truncate .link {
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -51,7 +51,7 @@
<app-toggle [textLeft]="'Sort by nodes'" [textRight]="'capacity'" [checked]="true" (toggleStatusChanged)="onGroupToggleStatusChanged($event)"></app-toggle>
</div>
<table class="table table-borderless text-center m-auto" style="max-width: 900px" *ngIf="!widget">
<table class="table table-borderless table-fixed text-center m-auto" style="max-width: 900px" *ngIf="!widget">
<thead>
<tr>
<th class="rank text-left pl-0" i18n="mining.rank">Rank</th>

View File

@ -4,7 +4,7 @@
</h1>
<div [class]="widget ? 'widget' : 'full'">
<table class="table table-borderless">
<table class="table table-borderless table-fixed">
<thead>
<th class="rank"></th>
<th class="alias text-left" i18n="nodes.alias">Alias</th>
@ -29,10 +29,10 @@
{{ node.channels | number }}
</td>
<td *ngIf="!widget" class="timestamp-first text-left">
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen"></app-timestamp>
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen" [hideTimeSince]="true"></app-timestamp>
</td>
<td *ngIf="!widget" class="timestamp-update text-left">
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt" [hideTimeSince]="true"></app-timestamp>
</td>
<td *ngIf="!widget" class="location text-right text-truncate">
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>

View File

@ -1,7 +1,7 @@
.container-xl {
max-width: 1400px;
padding-bottom: 100px;
@media (min-width: 767.98px) {
@media (min-width: 960px) {
padding-left: 50px;
padding-right: 50px;
}
@ -15,40 +15,44 @@
width: 5%;
}
.widget .rank {
@media (min-width: 767.98px) {
@media (min-width: 960px) {
width: 13%;
}
@media (max-width: 767.98px) {
@media (max-width: 960px) {
padding-left: 0px;
padding-right: 0px;
}
}
.full .alias {
width: 10%;
width: 20%;
overflow: hidden;
text-overflow: ellipsis;
max-width: 350px;
@media (max-width: 767.98px) {
max-width: 175px;
@media (max-width: 960px) {
width: 40%;
max-width: 500px;
}
}
.widget .alias {
width: 55%;
width: 60%;
overflow: hidden;
text-overflow: ellipsis;
max-width: 350px;
@media (max-width: 767.98px) {
@media (max-width: 960px) {
max-width: 175px;
}
}
.full .capacity {
width: 10%;
@media (max-width: 960px) {
width: 30%;
}
}
.widget .capacity {
width: 32%;
@media (max-width: 767.98px) {
@media (max-width: 960px) {
padding-left: 0px;
padding-right: 0px;
}
@ -57,28 +61,31 @@
.full .channels {
width: 15%;
padding-right: 50px;
@media (max-width: 767.98px) {
@media (max-width: 960px) {
display: none;
}
}
.full .timestamp-first {
width: 15%;
@media (max-width: 767.98px) {
width: 10%;
@media (max-width: 960px) {
display: none;
}
}
.full .timestamp-update {
width: 15%;
@media (max-width: 767.98px) {
width: 10%;
@media (max-width: 960px) {
display: none;
}
}
.full .location {
width: 10%;
@media (max-width: 767.98px) {
width: 15%;
@media (max-width: 960px) {
width: 30%;
}
@media (max-width: 600px) {
display: none;
}
}

View File

@ -29,10 +29,10 @@
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
</td>
<td *ngIf="!widget" class="timestamp-first text-left">
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen"></app-timestamp>
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen" [hideTimeSince]="true"></app-timestamp>
</td>
<td *ngIf="!widget" class="timestamp-update text-left">
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt" [hideTimeSince]="true"></app-timestamp>
</td>
<td *ngIf="!widget" class="location text-right text-truncate">
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>

View File

@ -1,7 +1,7 @@
.container-xl {
max-width: 1400px;
padding-bottom: 100px;
@media (min-width: 767.98px) {
@media (min-width: 960px) {
padding-left: 50px;
padding-right: 50px;
}
@ -15,70 +15,77 @@
width: 5%;
}
.widget .rank {
@media (min-width: 767.98px) {
@media (min-width: 960px) {
width: 13%;
}
@media (max-width: 767.98px) {
@media (max-width: 960px) {
padding-left: 0px;
padding-right: 0px;
}
}
.full .alias {
width: 10%;
width: 20%;
overflow: hidden;
text-overflow: ellipsis;
max-width: 350px;
@media (max-width: 767.98px) {
max-width: 175px;
@media (max-width: 960px) {
width: 40%;
max-width: 500px;
}
}
.widget .alias {
width: 55%;
width: 60%;
overflow: hidden;
text-overflow: ellipsis;
max-width: 350px;
@media (max-width: 767.98px) {
@media (max-width: 960px) {
max-width: 175px;
}
}
.full .channels {
.full .capacity {
width: 10%;
@media (max-width: 960px) {
width: 30%;
}
}
.widget .channels {
.widget .capacity {
width: 32%;
@media (max-width: 767.98px) {
@media (max-width: 960px) {
padding-left: 0px;
padding-right: 0px;
}
}
.full .capacity {
.full .channels {
width: 15%;
padding-right: 50px;
@media (max-width: 767.98px) {
@media (max-width: 960px) {
display: none;
}
}
.full .timestamp-first {
width: 15%;
@media (max-width: 767.98px) {
width: 10%;
@media (max-width: 960px) {
display: none;
}
}
.full .timestamp-update {
width: 15%;
@media (max-width: 767.98px) {
width: 10%;
@media (max-width: 960px) {
display: none;
}
}
.full .location {
width: 10%;
@media (max-width: 767.98px) {
width: 15%;
@media (max-width: 960px) {
width: 30%;
}
@media (max-width: 600px) {
display: none;
}
}

View File

@ -5,7 +5,7 @@ import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITrans
import { Observable } from 'rxjs';
import { StateService } from './state.service';
import { WebsocketResponse } from '../interfaces/websocket.interface';
import { Outspend } from '../interfaces/electrs.interface';
import { Outspend, Transaction } from '../interfaces/electrs.interface';
@Injectable({
providedIn: 'root'
@ -119,6 +119,14 @@ export class ApiService {
return this.httpClient.get<AddressInformation>(this.apiBaseUrl + this.apiBasePath + '/api/v1/validate-address/' + address);
}
getRbfHistory$(txid: string): Observable<string[]> {
return this.httpClient.get<string[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/replaces');
}
getRbfCachedTx$(txid: string): Observable<Transaction> {
return this.httpClient.get<Transaction>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/cached');
}
listLiquidPegsMonth$(): Observable<LiquidPegs[]> {
return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month');
}

View File

@ -1,7 +1,7 @@
<span *ngIf="seconds === undefined">-</span>
<span *ngIf="seconds !== undefined">
&lrm;{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<div class="lg-inline" *ngIf="!hideTimeSince">
<i class="symbol">(<app-time-since [time]="seconds" [fastRender]="true"></app-time-since>)</i>
</div>
</span>

View File

@ -10,6 +10,7 @@ export class TimestampComponent implements OnChanges {
@Input() unixTime: number;
@Input() dateString: string;
@Input() customFormat: string;
@Input() hideTimeSince: boolean = false;
seconds: number | undefined = undefined;

View File

@ -0,0 +1,19 @@
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null">
<ng-container *ngIf="link">
<a [routerLink]="link" class="truncate-link">
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
</a>
</ng-container>
<ng-container *ngIf="!link">
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
</ng-container>
<ng-content></ng-content>
</span>
<ng-template #ltrTruncated>
<span class="first">{{text.slice(0,-lastChars)}}</span><span class="last-four">{{text.slice(-lastChars)}}</span>
</ng-template>
<ng-template #rtlTruncated>
<span class="first">{{text.slice(lastChars)}}</span><span class="last-four">{{text.slice(0,lastChars)}}</span>
</ng-template>

View File

@ -0,0 +1,26 @@
.truncate {
text-overflow: unset;
display: flex;
flex-direction: row;
align-items: baseline;
.truncate-link {
display: flex;
flex-direction: row;
align-items: baseline;
flex-shrink: 1;
overflow: hidden;
}
.first {
flex-grow: 0;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.last-four {
flex-shrink: 0;
flex-grow: 0;
}
}

View File

@ -0,0 +1,23 @@
import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-truncate',
templateUrl: './truncate.component.html',
styleUrls: ['./truncate.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TruncateComponent {
@Input() text: string;
@Input() link: any = null;
@Input() lastChars: number = 4;
@Input() maxWidth: number = null;
rtl: boolean;
constructor(
@Inject(LOCALE_ID) private locale: string,
) {
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
this.rtl = true;
}
}
}

View File

@ -5,6 +5,6 @@ import { Pipe, PipeTransform } from '@angular/core';
})
export class Decimal2HexPipe implements PipeTransform {
transform(decimal: number): string {
return `0x` + decimal.toString(16);
return `0x` + ( decimal.toString(16) ).padStart(8, '0');
}
}

View File

@ -77,6 +77,7 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index
import { SvgImagesComponent } from '../components/svg-images/svg-images.component';
import { ChangeComponent } from '../components/change/change.component';
import { SatsComponent } from './components/sats/sats.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';
import { ToggleComponent } from './components/toggle/toggle.component';
@ -152,6 +153,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
SvgImagesComponent,
ChangeComponent,
SatsComponent,
TruncateComponent,
SearchResultsComponent,
TimestampComponent,
ToggleComponent,
@ -252,6 +254,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
SvgImagesComponent,
ChangeComponent,
SatsComponent,
TruncateComponent,
SearchResultsComponent,
TimestampComponent,
ToggleComponent,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -106,6 +106,7 @@ http {
~*^vi vi;
~*^zh zh;
~*^hi hi;
~*^ne ne;
~*^lt lt;
}
@ -142,6 +143,7 @@ http {
~*^vi vi;
~*^zh zh;
~*^hi hi;
~*^ne ne;
~*^lt lt;
}