Liquid peg outs address indexing: support for duplicates and minor fixes

This commit is contained in:
natsee 2024-01-30 20:01:17 +01:00
parent 1e15428a63
commit 163c0b6d78
No known key found for this signature in database
GPG Key ID: 233CF3150A89BED8
11 changed files with 60 additions and 60 deletions

View File

@ -154,7 +154,9 @@ class ElementsParser {
// First, get the current UTXOs that need to be scanned in the block // First, get the current UTXOs that need to be scanned in the block
const utxos = await this.$getFederationUtxosToScan(auditProgress.lastBlockAudit); const utxos = await this.$getFederationUtxosToScan(auditProgress.lastBlockAudit);
// logger.debug(`Found ${utxos.length} Federation UTXOs to scan in Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip}`);
// Get the peg-out addresses that need to be scanned
const redeemAddresses = await this.$getRedeemAddressesToScan();
// The fast way: check if these UTXOs are still unspent as of the current block with gettxout // The fast way: check if these UTXOs are still unspent as of the current block with gettxout
let spentAsTip: any[]; let spentAsTip: any[];
@ -163,41 +165,33 @@ class ElementsParser {
const utxosToParse = await this.$getFederationUtxosToParse(utxos); const utxosToParse = await this.$getFederationUtxosToParse(utxos);
spentAsTip = utxosToParse.spentAsTip; spentAsTip = utxosToParse.spentAsTip;
unspentAsTip = utxosToParse.unspentAsTip; unspentAsTip = utxosToParse.unspentAsTip;
// logger.debug(`${unspentAsTip.length} / ${utxos.length} Federation UTXOs are unspent as of tip`); logger.debug(`Found ${utxos.length} Federation UTXOs and ${redeemAddresses.length} Peg-Out Addresses to scan in Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip}`);
} else { // If the audit status is too far in the past, it is useless to look for still unspent txos since they will all be spent as of the tip logger.debug(`${unspentAsTip.length} / ${utxos.length} Federation UTXOs are unspent as of tip`);
} else { // If the audit status is too far in the past, it is useless and wasteful to look for still unspent txos since they will all be spent as of the tip
spentAsTip = utxos; spentAsTip = utxos;
unspentAsTip = []; unspentAsTip = [];
}
// Get the peg-out addresses that need to be scanned // Logging
const redeemAddresses = await this.$getRedeemAddressesToScan();
// if (redeemAddresses.length > 0) logger.debug(`Found ${redeemAddresses.length} peg-out addresses to scan`);
// Logging during initial indexing
if (auditProgress.confirmedTip - auditProgress.lastBlockAudit > 150) {
const elapsedSeconds = (Date.now() / 1000) - timer; const elapsedSeconds = (Date.now() / 1000) - timer;
if (elapsedSeconds > 5) { if (elapsedSeconds > 5) {
const runningFor = (Date.now() / 1000) - startedAt; const runningFor = (Date.now() / 1000) - startedAt;
const blockPerSeconds = indexedThisRun / elapsedSeconds; const blockPerSeconds = indexedThisRun / elapsedSeconds;
indexingSpeeds.push(blockPerSeconds); indexingSpeeds.push(blockPerSeconds);
if (indexingSpeeds.length > 100) indexingSpeeds.shift(); // Keep the length of the up to 100 last indexing speeds if (indexingSpeeds.length > 100) indexingSpeeds.shift(); // Keep the length of the up to 100 last indexing speeds
const eta = (auditProgress.confirmedTip - auditProgress.lastBlockAudit) / (indexingSpeeds.reduce((a, b) => a + b, 0) / indexingSpeeds.length); const meanIndexingSpeed = indexingSpeeds.reduce((a, b) => a + b, 0) / indexingSpeeds.length;
logger.debug(`Scanning ${utxos.length} Federation UTXOs and ${redeemAddresses.length} Peg-Out Addresses at height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip} | ~${blockPerSeconds.toFixed(2)} blocks/sec | elapsed: ${(runningFor / 60).toFixed(2)} minutes | ETA: ${(eta / 60).toFixed(2)} minutes`); const eta = (auditProgress.confirmedTip - auditProgress.lastBlockAudit) / meanIndexingSpeed;
logger.debug(`Scanning ${utxos.length} Federation UTXOs and ${redeemAddresses.length} Peg-Out Addresses at Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip} | ~${meanIndexingSpeed.toFixed(2)} blocks/sec | elapsed: ${(runningFor / 60).toFixed(0)} minutes | ETA: ${(eta / 60).toFixed(0)} minutes`);
timer = Date.now() / 1000; timer = Date.now() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
} }
} }
// The slow way: parse the block to look for the spending tx // The slow way: parse the block to look for the spending tx
// logger.debug(`${spentAsTip.length} / ${utxos.length} Federation UTXOs are spent as of tip`);
const blockHash: IBitcoinApi.ChainTips = await bitcoinSecondClient.getBlockHash(auditProgress.lastBlockAudit); const blockHash: IBitcoinApi.ChainTips = await bitcoinSecondClient.getBlockHash(auditProgress.lastBlockAudit);
const block: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(blockHash, 2); const block: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(blockHash, 2);
const nbUtxos = spentAsTip.length;
await DB.query('START TRANSACTION;'); await DB.query('START TRANSACTION;');
await this.$parseBitcoinBlock(block, spentAsTip, unspentAsTip, auditProgress.confirmedTip, redeemAddresses); await this.$parseBitcoinBlock(block, spentAsTip, unspentAsTip, auditProgress.confirmedTip, redeemAddresses);
await DB.query(`COMMIT;`); await DB.query(`COMMIT;`);
// logger.debug(`Watched for spending of ${nbUtxos} Federation UTXOs in block ${auditProgress.lastBlockAudit} / ${auditProgress.confirmedTip}`);
// Finally, update the lastblockupdate of the remaining UTXOs and save to the database // Finally, update the lastblockupdate of the remaining UTXOs and save to the database
const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`) const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`)
@ -236,14 +230,15 @@ class ElementsParser {
return {spentAsTip, unspentAsTip}; return {spentAsTip, unspentAsTip};
} }
protected async $parseBitcoinBlock(block: IBitcoinApi.Block, spentAsTip: any[], unspentAsTip: any[], confirmedTip: number, redeemAddresses: string[] = []) { protected async $parseBitcoinBlock(block: IBitcoinApi.Block, spentAsTip: any[], unspentAsTip: any[], confirmedTip: number, redeemAddressesData: any[] = []) {
let mightRedeemInThisBlock = false; // If a Federation UTXO is spent in this block, we might find a peg-out address in the outputs... const redeemAddresses: string[] = redeemAddressesData.map(redeemAddress => redeemAddress.bitcoinaddress);
for (const tx of block.tx) { for (const tx of block.tx) {
let mightRedeemInThisTx = false; // If a Federation UTXO is spent in this block, we might find a peg-out address in the outputs...
// Check if the Federation UTXOs that was spent as of tip are spent in this block // Check if the Federation UTXOs that was spent as of tip are spent in this block
for (const input of tx.vin) { for (const input of tx.vin) {
const txo = spentAsTip.find(txo => txo.txid === input.txid && txo.txindex === input.vout); const txo = spentAsTip.find(txo => txo.txid === input.txid && txo.txindex === input.vout);
if (txo) { if (txo) {
mightRedeemInThisBlock = true; mightRedeemInThisTx = true;
await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, lasttimeupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, block.time, txo.txid, txo.txindex]); await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, lasttimeupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, block.time, txo.txid, txo.txindex]);
// Remove the TXO from the utxo array // Remove the TXO from the utxo array
spentAsTip.splice(spentAsTip.indexOf(txo), 1); spentAsTip.splice(spentAsTip.indexOf(txo), 1);
@ -269,16 +264,31 @@ class ElementsParser {
logger.debug(`Added new Federation UTXO ${tx.txid}:${output.n} (${output.value * 100000000} sats), change address: ${output.scriptPubKey.address}`); logger.debug(`Added new Federation UTXO ${tx.txid}:${output.n} (${output.value * 100000000} sats), change address: ${output.scriptPubKey.address}`);
} }
} }
if (mightRedeemInThisBlock && output.scriptPubKey.address && redeemAddresses.includes(output.scriptPubKey.address)) { if (mightRedeemInThisTx && output.scriptPubKey.address && redeemAddresses.includes(output.scriptPubKey.address)) {
const query_add_redeem = `UPDATE elements_pegs SET bitcointxid = ?, bitcoinindex = ? WHERE bitcoinaddress = ?`; // Find the number of times output.scriptPubKey.address appears in redeemAddresses. There can be address reuse for peg-outs...
const params_add_redeem: (string | number)[] = [tx.txid, output.n, output.scriptPubKey.address]; const matchingAddress: any[] = redeemAddressesData.filter(redeemAddress => redeemAddress.bitcoinaddress === output.scriptPubKey.address && -redeemAddress.amount === Math.round(output.value * 100000000));
await DB.query(query_add_redeem, params_add_redeem); if (matchingAddress.length > 0) {
redeemAddresses.splice(redeemAddresses.indexOf(output.scriptPubKey.address), 1); if (matchingAddress.length > 1) {
logger.debug(`Added redeem txid ${tx.txid}:${output.n} to peg-out address ${output.scriptPubKey.address}`); // If there are more than one peg out address with the same amount, we can't know which one redeemed the UTXO: we take the oldest one
matchingAddress.sort((a, b) => a.datetime - b.datetime);
logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${matchingAddress[0].bitcoinaddress}, amount ${matchingAddress[0].amount}, datetime ${matchingAddress[0].datetime}`);
} else {
logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${matchingAddress[0].bitcoinaddress}, amount ${matchingAddress[0].amount}`);
}
const query_add_redeem = `UPDATE elements_pegs SET bitcointxid = ?, bitcoinindex = ? WHERE bitcoinaddress = ? AND amount = ? AND datetime = ?`;
const params_add_redeem: (string | number)[] = [tx.txid, output.n, matchingAddress[0].bitcoinaddress, matchingAddress[0].amount, matchingAddress[0].datetime];
await DB.query(query_add_redeem, params_add_redeem);
const index = redeemAddressesData.indexOf(matchingAddress[0]);
redeemAddressesData.splice(index, 1);
redeemAddresses.splice(index, 1);
} else { // The output amount does not match the peg-out amount... log it
logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${output.scriptPubKey.address} but output amount ${Math.round(output.value * 100000000)} does not match the peg-out amount!`);
}
} }
} }
} }
for (const utxo of spentAsTip) { for (const utxo of spentAsTip) {
await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, utxo.txid, utxo.txindex]); await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, utxo.txid, utxo.txindex]);
} }
@ -318,10 +328,10 @@ class ElementsParser {
return rows[0]['number']; return rows[0]['number'];
} }
protected async $getRedeemAddressesToScan(): Promise<string[]> { protected async $getRedeemAddressesToScan(): Promise<any[]> {
const query = `SELECT bitcoinaddress FROM elements_pegs where amount < 0 AND bitcoinaddress != '' AND bitcointxid = '';`; const query = `SELECT datetime, amount, bitcoinaddress FROM elements_pegs where amount < 0 AND bitcoinaddress != '' AND bitcointxid = '';`;
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows.map((row: any) => row.bitcoinaddress); return rows;
} }
///////////// DATA QUERY ////////////// ///////////// DATA QUERY //////////////
@ -423,9 +433,9 @@ class ElementsParser {
return rows[0]; return rows[0];
} }
// Get the 300 most recent pegouts from the federation // Get recent pegouts from the federation (3 months old)
public async $getRecentPegouts(): Promise<any> { public async $getRecentPegouts(): Promise<any> {
const query = `SELECT txid, txindex, amount, bitcoinaddress, bitcointxid, bitcoinindex, datetime AS blocktime FROM elements_pegs WHERE amount < 0 ORDER BY blocktime DESC LIMIT 300;`; const query = `SELECT txid, txindex, amount, bitcoinaddress, bitcointxid, bitcoinindex, datetime AS blocktime FROM elements_pegs WHERE amount < 0 AND datetime > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -90, CURRENT_TIMESTAMP())) ORDER BY blocktime;`;
const [rows] = await DB.query(query); const [rows] = await DB.query(query);
return rows; return rows;
} }

View File

@ -1,5 +1,4 @@
<div [ngClass]="{'widget': widget, 'address-container': !widget}"> <div [ngClass]="{'widget': widget}">
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -1,10 +1,3 @@
.address-container {
@media (min-width: 1100px) {
margin-left: 80px;
margin-right: 80px;
}
}
.spinner-border { .spinner-border {
height: 25px; height: 25px;
width: 25px; width: 25px;

View File

@ -3,7 +3,7 @@
<div class="fee-estimation-container"> <div class="fee-estimation-container">
<div class="item"> <div class="item">
<a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]"> <a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]">
<h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5> <h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container>&nbsp;<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a> </a>
<div class="card-text"> <div class="card-text">
<div class="fee-text">{{ federationAddresses.length }} <span i18n="shared.addresses">addresses</span></div> <div class="fee-text">{{ federationAddresses.length }} <span i18n="shared.addresses">addresses</span></div>
@ -19,7 +19,7 @@
<div class="fee-estimation-container loading-container"> <div class="fee-estimation-container loading-container">
<div class="item"> <div class="item">
<a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]"> <a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]">
<h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5> <h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container>&nbsp;<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a> </a>
<div class="card-text"> <div class="card-text">
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>

View File

@ -1,7 +1,5 @@
<div [ngClass]="{'widget': widget}"> <div [ngClass]="{'widget': widget}">
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div> <div class="clearfix"></div>
<div style="min-height: 295px"> <div style="min-height: 295px">

View File

@ -5,8 +5,6 @@
<h1 i18n="liquid.recent-pegs">Recent Peg-In / Out's</h1> <h1 i18n="liquid.recent-pegs">Recent Peg-In / Out's</h1>
</div> </div>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div> <div class="clearfix"></div>
<div style="min-height: 295px"> <div style="min-height: 295px">
@ -14,7 +12,8 @@
<thead style="vertical-align: middle;"> <thead style="vertical-align: middle;">
<th class="transaction text-left" [ngClass]="{'widget': widget}" i18n="shared.transaction">Transaction</th> <th class="transaction text-left" [ngClass]="{'widget': widget}" i18n="shared.transaction">Transaction</th>
<th class="amount text-right" [ngClass]="{'widget': widget}" i18n="shared.amount">Amount</th> <th class="amount text-right" [ngClass]="{'widget': widget}" i18n="shared.amount">Amount</th>
<th class="output text-left" *ngIf="!widget" i18n="liquid.bitcoin-funding-redeem">BTC Funding / Redeem</th> <th class="output text-left" *ngIf="!widget" i18n="liquid.fund-redemption-tx">Fund / Redemption Tx</th>
<th class="address text-left" *ngIf="!widget" i18n="liquid.bitcoin-address">BTC Address</th>
<th class="timestamp text-right" i18n="shared.date" [ngClass]="{'widget': widget}">Date</th> <th class="timestamp text-right" i18n="shared.date" [ngClass]="{'widget': widget}">Date</th>
</thead> </thead>
<tbody *ngIf="recentPegs$ | async as pegs; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> <tbody *ngIf="recentPegs$ | async as pegs; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
@ -23,17 +22,17 @@
<td class="transaction text-left widget"> <td class="transaction text-left widget">
<ng-container *ngIf="peg.amount > 0"> <ng-container *ngIf="peg.amount > 0">
<a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex"> <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex">
<app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate> <app-truncate [text]="peg.txid"></app-truncate>
</a> </a>
</ng-container> </ng-container>
<ng-container *ngIf="peg.amount < 0"> <ng-container *ngIf="peg.amount < 0">
<a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex"> <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex">
<app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate> <app-truncate [text]="peg.txid"></app-truncate>
</a> </a>
</ng-container> </ng-container>
</td> </td>
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0}"> <td class="amount text-right widget" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0}">
{{ peg.amount > 0 ? '+' : '-' }}<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true"></app-amount> <app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
</td> </td>
<td class="timestamp text-right widget"> <td class="timestamp text-right widget">
<app-time kind="since" [time]="peg.blocktime"></app-time> <app-time kind="since" [time]="peg.blocktime"></app-time>
@ -55,7 +54,7 @@
</ng-container> </ng-container>
</td> </td>
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0}"> <td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0}">
{{ peg.amount > 0 ? '+' : '-' }}<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true"></app-amount> <app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
</td> </td>
<td class="output text-left"> <td class="output text-left">
<ng-container *ngIf="peg.bitcointxid; else redeemInProgress"> <ng-container *ngIf="peg.bitcointxid; else redeemInProgress">
@ -65,7 +64,7 @@
</ng-container> </ng-container>
<ng-template #redeemInProgress> <ng-template #redeemInProgress>
<ng-container *ngIf="peg.bitcoinaddress; else noRedeem"> <ng-container *ngIf="peg.bitcoinaddress; else noRedeem">
<span class="text-muted" i18n="liquid.redemption-in-progress">BTC Redemption in progress...</span> <i><span class="text-muted" i18n="liquid.redemption-in-progress">Peg out in progress...</span></i>
</ng-container> </ng-container>
</ng-template> </ng-template>
</td> </td>

View File

@ -32,7 +32,7 @@ tr, td, th {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
max-width: 160px; max-width: 120px;
} }
.transaction.widget { .transaction.widget {
width: 40%; width: 40%;
@ -62,7 +62,7 @@ tr, td, th {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
max-width: 160px; max-width: 160px;
@media (max-width: 840px) { @media (max-width: 825px) {
display: none; display: none;
} }
} }
@ -72,7 +72,7 @@ tr, td, th {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
max-width: 160px; max-width: 160px;
@media (max-width: 527px) { @media (max-width: 840px) {
display: none; display: none;
} }
} }

View File

@ -14,8 +14,8 @@ import { WebsocketService } from '../../../services/websocket.service';
}) })
export class RecentPegsListComponent implements OnInit { export class RecentPegsListComponent implements OnInit {
@Input() widget: boolean = false; @Input() widget: boolean = false;
@Input() recentPegIns$: Observable<RecentPeg[]>; @Input() recentPegIns$: Observable<RecentPeg[]> = of([]);
@Input() recentPegOuts$: Observable<RecentPeg[]>; @Input() recentPegOuts$: Observable<RecentPeg[]> = of([]);
env: Env; env: Env;
isLoading = true; isLoading = true;
@ -133,6 +133,7 @@ export class RecentPegsListComponent implements OnInit {
return b.blocktime - a.blocktime; return b.blocktime - a.blocktime;
}); });
}), }),
filter(recentPegs => recentPegs.length > 0),
tap(_ => this.isLoading = false), tap(_ => this.isLoading = false),
share() share()
); );

View File

@ -1,7 +1,7 @@
<div class="fee-estimation-container"> <div class="fee-estimation-container">
<div class="item"> <div class="item">
<a class="title-link" [routerLink]="['/audit/pegs' | relativeUrl]"> <a class="title-link" [routerLink]="['/audit/pegs' | relativeUrl]">
<h5 class="card-title"><ng-container i18n="liquid.recent-pegs">Recent Peg-In / Out's</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5> <h5 class="card-title"><ng-container i18n="liquid.recent-pegs">Recent Peg-In / Out's</ng-container>&nbsp;<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a> </a>
</div> </div>
</div> </div>

View File

@ -6,7 +6,7 @@
<div class="card-text"> <div class="card-text">
<div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div> <div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div>
<span class="fiat"> <span class="fiat">
<span>As of block&nbsp;<a [routerLink]="['/block', currentPeg.hash]" target="_blank">{{ currentPeg.lastBlockUpdate }}</a></span> <span>As of block&nbsp;<a [routerLink]="['/block', currentPeg.hash]">{{ currentPeg.lastBlockUpdate }}</a></span>
</span> </span>
</div> </div>
</div> </div>

View File

@ -276,7 +276,7 @@
</div> </div>
<div class="item"> <div class="item">
<a class="title-link" [routerLink]="['/audit' | relativeUrl]"> <a class="title-link" [routerLink]="['/audit' | relativeUrl]">
<h5 class="card-title"><ng-container i18n="dashboard.btc-reserves">BTC Reserves</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5> <h5 class="card-title"><ng-container i18n="dashboard.btc-reserves">BTC Reserves</ng-container>&nbsp;<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
</a> </a>
<ng-container *ngIf="(currentReserves$ | async) as currentReserves; else loadingTransactions"> <ng-container *ngIf="(currentReserves$ | async) as currentReserves; else loadingTransactions">
<p i18n-ngbTooltip="liquid.last-bitcoin-audit-block" [ngbTooltip]="'BTC reserves last updated at Bitcoin block ' + (currentReserves.lastBlockUpdate)" placement="top" class="card-text">{{ +(currentReserves.amount) / 100000000 | number: '1.2-2' }} <span class="bitcoin-color">BTC</span></p> <p i18n-ngbTooltip="liquid.last-bitcoin-audit-block" [ngbTooltip]="'BTC reserves last updated at Bitcoin block ' + (currentReserves.lastBlockUpdate)" placement="top" class="card-text">{{ +(currentReserves.amount) / 100000000 | number: '1.2-2' }} <span class="bitcoin-color">BTC</span></p>