Merge branch 'master' into mononaut/sliding-difficulty
This commit is contained in:
		
						commit
						5f14b32a06
					
				| @ -398,39 +398,24 @@ class ElementsParser { | ||||
|     return rows; | ||||
|   } | ||||
| 
 | ||||
|   // Get all of the federation addresses one month ago, most balances first
 | ||||
|   public async $getFederationAddressesOneMonthAgo(): Promise<any> { | ||||
|     const query = ` | ||||
|     SELECT COUNT(*) AS addresses_count_one_month FROM ( | ||||
|       SELECT bitcoinaddress, SUM(amount) AS balance | ||||
|       FROM federation_txos  | ||||
|       WHERE | ||||
|           (blocktime < UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP()))) | ||||
|         AND | ||||
|           ((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP())))) | ||||
|       GROUP BY bitcoinaddress | ||||
|     ) AS result;`;
 | ||||
|   // Get the total number of federation addresses
 | ||||
|   public async $getFederationAddressesNumber(): Promise<any> { | ||||
|     const query = `SELECT COUNT(DISTINCT bitcoinaddress) AS address_count FROM federation_txos WHERE unspent = 1;`; | ||||
|     const [rows] = await DB.query(query); | ||||
|     return rows[0]; | ||||
|   } | ||||
| 
 | ||||
|   // Get all of the UTXOs held by the federation one month ago, most recent first
 | ||||
|   public async $getFederationUtxosOneMonthAgo(): Promise<any> { | ||||
|     const query = ` | ||||
|     SELECT COUNT(*) AS utxos_count_one_month FROM federation_txos  | ||||
|     WHERE | ||||
|         (blocktime < UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP()))) | ||||
|       AND | ||||
|         ((unspent = 1) OR (unspent = 0 AND lasttimeupdate > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -30, CURRENT_TIMESTAMP())))) | ||||
|     ORDER BY blocktime DESC;`;
 | ||||
|   // Get the total number of federation utxos
 | ||||
|   public async $getFederationUtxosNumber(): Promise<any> { | ||||
|     const query = `SELECT COUNT(*) AS utxo_count FROM federation_txos WHERE unspent = 1;`; | ||||
|     const [rows] = await DB.query(query); | ||||
|     return rows[0]; | ||||
|   } | ||||
| 
 | ||||
|   // Get recent pegouts from the federation (3 months old)
 | ||||
|   public async $getRecentPegouts(): Promise<any> { | ||||
|     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); | ||||
|   // Get recent pegs in / out
 | ||||
|   public async $getPegsList(count: number = 0): Promise<any> { | ||||
|     const query = `SELECT txid, txindex, amount, bitcoinaddress, bitcointxid, bitcoinindex, datetime AS blocktime FROM elements_pegs ORDER BY block DESC LIMIT 15 OFFSET ?;`; | ||||
|     const [rows] = await DB.query(query, [count]); | ||||
|     return rows; | ||||
|   } | ||||
| 
 | ||||
| @ -443,6 +428,12 @@ class ElementsParser { | ||||
|       pegOutQuery[0][0] | ||||
|     ]; | ||||
|   } | ||||
| 
 | ||||
|   // Get the total pegs number
 | ||||
|   public async $getPegsCount(): Promise<any> { | ||||
|     const [rows] = await DB.query(`SELECT COUNT(*) AS pegs_count FROM elements_pegs;`); | ||||
|     return rows[0]; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default new ElementsParser(); | ||||
|  | ||||
| @ -17,14 +17,15 @@ class LiquidRoutes { | ||||
|       app | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs', this.$getElementsPegs) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', this.$getElementsPegsByMonth) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/list/:count', this.$getPegsList) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/volume', this.$getPegsVolumeDaily) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/count', this.$getPegsCount) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves', this.$getFederationReserves) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/month', this.$getFederationReservesByMonth) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegouts', this.$getPegOuts) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses', this.$getFederationAddresses) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses/previous-month', this.$getFederationAddressesOneMonthAgo) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses/total', this.$getFederationAddressesNumber) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos', this.$getFederationUtxos) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/previous-month', this.$getFederationUtxosOneMonthAgo) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/total', this.$getFederationUtxosNumber) | ||||
|         .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/status', this.$getFederationAuditStatus) | ||||
|         ; | ||||
|     } | ||||
| @ -142,12 +143,12 @@ class LiquidRoutes { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $getFederationAddressesOneMonthAgo(req: Request, res: Response) { | ||||
|   private async $getFederationAddressesNumber(req: Request, res: Response) { | ||||
|     try { | ||||
|       const federationAddresses = await elementsParser.$getFederationAddressesOneMonthAgo(); | ||||
|       const federationAddresses = await elementsParser.$getFederationAddressesNumber(); | ||||
|       res.header('Pragma', 'public'); | ||||
|       res.header('Cache-control', 'public'); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60 * 24).toUTCString()); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); | ||||
|       res.json(federationAddresses); | ||||
|     } catch (e) { | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
| @ -166,25 +167,25 @@ class LiquidRoutes { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $getFederationUtxosOneMonthAgo(req: Request, res: Response) { | ||||
|   private async $getFederationUtxosNumber(req: Request, res: Response) { | ||||
|     try { | ||||
|       const federationUtxos = await elementsParser.$getFederationUtxosOneMonthAgo(); | ||||
|       const federationUtxos = await elementsParser.$getFederationUtxosNumber(); | ||||
|       res.header('Pragma', 'public'); | ||||
|       res.header('Cache-control', 'public'); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60 * 24).toUTCString()); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); | ||||
|       res.json(federationUtxos); | ||||
|     } catch (e) { | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $getPegOuts(req: Request, res: Response) { | ||||
|   private async $getPegsList(req: Request, res: Response) { | ||||
|     try { | ||||
|       const recentPegOuts = await elementsParser.$getRecentPegouts(); | ||||
|       const recentPegs = await elementsParser.$getPegsList(parseInt(req.params?.count)); | ||||
|       res.header('Pragma', 'public'); | ||||
|       res.header('Cache-control', 'public'); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); | ||||
|       res.json(recentPegOuts); | ||||
|       res.json(recentPegs); | ||||
|     } catch (e) { | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
|     } | ||||
| @ -202,6 +203,18 @@ class LiquidRoutes { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $getPegsCount(req: Request, res: Response) { | ||||
|     try { | ||||
|       const pegsCount = await elementsParser.$getPegsCount(); | ||||
|       res.header('Pragma', 'public'); | ||||
|       res.header('Cache-control', 'public'); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); | ||||
|       res.json(pegsCount); | ||||
|     } catch (e) { | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export default new LiquidRoutes(); | ||||
|  | ||||
| @ -285,7 +285,7 @@ class StatisticsApi { | ||||
| 
 | ||||
|   public async $list2H(): Promise<OptimizedStatistic[]> { | ||||
|     try { | ||||
|       const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics ORDER BY statistics.added DESC LIMIT 120`; | ||||
|       const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL 2 HOUR) AND NOW() ORDER BY statistics.added DESC`; | ||||
|       const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); | ||||
|       return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); | ||||
|     } catch (e) { | ||||
| @ -296,7 +296,7 @@ class StatisticsApi { | ||||
| 
 | ||||
|   public async $list24H(): Promise<OptimizedStatistic[]> { | ||||
|     try { | ||||
|       const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics ORDER BY statistics.added DESC LIMIT 1440`; | ||||
|       const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL 24 HOUR) AND NOW() ORDER BY statistics.added DESC`; | ||||
|       const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); | ||||
|       return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); | ||||
|     } catch (e) { | ||||
|  | ||||
| @ -6,6 +6,7 @@ import statisticsApi from './statistics-api'; | ||||
| 
 | ||||
| class Statistics { | ||||
|   protected intervalTimer: NodeJS.Timer | undefined; | ||||
|   protected lastRun: number = 0; | ||||
|   protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined; | ||||
| 
 | ||||
|   public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) { | ||||
| @ -23,15 +24,21 @@ class Statistics { | ||||
|     setTimeout(() => { | ||||
|       this.runStatistics(); | ||||
|       this.intervalTimer = setInterval(() => { | ||||
|         this.runStatistics(); | ||||
|         this.runStatistics(true); | ||||
|       }, 1 * 60 * 1000); | ||||
|     }, difference); | ||||
|   } | ||||
| 
 | ||||
|   private async runStatistics(): Promise<void> { | ||||
|   public async runStatistics(skipIfRecent = false): Promise<void> { | ||||
|     if (!memPool.isInSync()) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (skipIfRecent && new Date().getTime() / 1000 - this.lastRun < 30) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.lastRun = new Date().getTime() / 1000; | ||||
|     const currentMempool = memPool.getMempool(); | ||||
|     const txPerSecond = memPool.getTxPerSecond(); | ||||
|     const vBytesPerSecond = memPool.getVBytesPerSecond(); | ||||
|  | ||||
| @ -23,6 +23,7 @@ import priceUpdater from '../tasks/price-updater'; | ||||
| import { ApiPrice } from '../repositories/PricesRepository'; | ||||
| import accelerationApi from './services/acceleration'; | ||||
| import mempool from './mempool'; | ||||
| import statistics from './statistics/statistics'; | ||||
| 
 | ||||
| interface AddressTransactions { | ||||
|   mempool: MempoolTransactionExtended[], | ||||
| @ -723,6 +724,7 @@ class WebsocketHandler { | ||||
|     } | ||||
| 
 | ||||
|     this.printLogs(); | ||||
|     await statistics.runStatistics(); | ||||
| 
 | ||||
|     const _memPool = memPool.getMempool(); | ||||
| 
 | ||||
| @ -1014,6 +1016,8 @@ class WebsocketHandler { | ||||
|         client.send(this.serializeResponse(response)); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     await statistics.runStatistics(); | ||||
|   } | ||||
| 
 | ||||
|   // takes a dictionary of JSON serialized values
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <div id="become-sponsor-container"> | ||||
| <div id="become-sponsor-container" [ngClass]="context"> | ||||
|   <div class="become-sponsor community"> | ||||
|     <p style="font-weight: 700; font-size: 18px;">If you're an individual...</p> | ||||
|     <a [href]="host + '/sponsor'" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button" (click)="onSponsorClick($event)">Become a Community Sponsor</a> | ||||
| @ -13,4 +13,4 @@ | ||||
|     <p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Co-branded instance</p> | ||||
|     <p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  | ||||
| @ -6,6 +6,11 @@ | ||||
|   align-items: center; | ||||
|   gap: 20px; | ||||
|   margin: 68px auto; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| #become-sponsor-container.account { | ||||
|   margin: 20px auto; | ||||
| } | ||||
| 
 | ||||
| .become-sponsor { | ||||
|  | ||||
| @ -8,6 +8,7 @@ import { EnterpriseService } from '../../services/enterprise.service'; | ||||
| }) | ||||
| export class AboutSponsorsComponent { | ||||
|   @Input() host = 'https://mempool.space'; | ||||
|   @Input() context = 'about'; | ||||
| 
 | ||||
|   constructor(private enterpriseService: EnterpriseService) { | ||||
|   } | ||||
|  | ||||
| @ -78,10 +78,7 @@ | ||||
|       <li class="nav-item" routerLinkActive="active" id="btn-assets"> | ||||
|         <a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a> | ||||
|       </li> | ||||
|       <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-audit"> | ||||
|         <a class="nav-link" [routerLink]="['/audit']" (click)="collapse()"><fa-icon [icon]="['fas', 'scale-balanced']" [fixedWidth]="true" i18n-title="master-page.btc-reserves-audit" title="BTC Reserves Audit"></fa-icon></a> | ||||
|       </li> | ||||
|       <li [hidden]="isMobile" class="nav-item mr-2" routerLinkActive="active" id="btn-docs"> | ||||
|       <li class="nav-item mr-2" routerLinkActive="active" id="btn-docs"> | ||||
|         <a class="nav-link" [routerLink]="['/docs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'book']" [fixedWidth]="true" i18n-title="master-page.docs" title="Docs"></fa-icon></a> | ||||
|       </li> | ||||
|       <li class="nav-item" routerLinkActive="active" id="btn-about"> | ||||
|  | ||||
| @ -23,6 +23,11 @@ li.nav-item { | ||||
|   margin: auto 10px; | ||||
|   padding-left: 10px; | ||||
|   padding-right: 10px; | ||||
|   @media (max-width: 429px) { | ||||
|     margin: auto 5px; | ||||
|     padding-left: 6px; | ||||
|     padding-right: 6px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media (min-width: 992px) { | ||||
|  | ||||
| @ -6,11 +6,14 @@ | ||||
| 
 | ||||
| tr, td, th { | ||||
|   border: 0px; | ||||
|   padding-top: 0.65rem !important; | ||||
|   padding-bottom: 0.6rem !important; | ||||
|   padding-right: 2rem !important; | ||||
|   .widget { | ||||
|     padding-right: 1rem !important; | ||||
|   padding-top: 0.65rem; | ||||
|   padding-bottom: 0.6rem; | ||||
|   padding-right: 2rem; | ||||
|   .widget &.widget { | ||||
|     padding-right: 1rem; | ||||
|     @media (max-width: 510px) { | ||||
|       padding-right: 0.5rem; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,18 +1,15 @@ | ||||
| <div *ngIf="(federationAddresses$ | async) as federationAddresses; else loadingData"> | ||||
|    | ||||
|     <div class="fee-estimation-container"> | ||||
|       <div class="item"> | ||||
|         <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> | ||||
|         </a> | ||||
|         <div class="card-text"> | ||||
|           <div class="fee-text">{{ federationAddresses.length }} <span i18n="shared.addresses">addresses</span></div> | ||||
|           <span class="fiat" *ngIf="(federationAddressesOneMonthAgo$ | async) as federationAddressesOneMonthAgo; else loadingSkeleton" i18n-ngbTooltip="liquid.percentage-change-last-month" ngbTooltip="Percentage change past month" placement="bottom"> | ||||
|             <app-change [current]="federationAddresses.length" [previous]="federationAddressesOneMonthAgo.addresses_count_one_month"></app-change> | ||||
|           </span> | ||||
|         </div> | ||||
| <div *ngIf="(federationWalletStats$ | async) as federationWalletStats; else loadingData"> | ||||
|   <div class="fee-estimation-container"> | ||||
|     <div class="item"> | ||||
|       <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> | ||||
|       </a> | ||||
|       <div class="card-text"> | ||||
|         <div class="fee-text">{{ federationWalletStats.address_count }} <span i18n="shared.addresses">addresses</span></div> | ||||
|         <div class="fiat">{{ federationWalletStats.utxo_count }} <span i18n="shared.utxos">UTXOs</span></div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #loadingData> | ||||
| @ -30,5 +27,5 @@ | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #loadingSkeleton> | ||||
|   <div class="skeleton-loader skeleton-loader-transactions" style="margin-top: 2px; margin-bottom: 5px;"></div> | ||||
|   <div class="skeleton-loader skeleton-loader-transactions" style="margin-top: 8px; margin-bottom: 8px;"></div> | ||||
| </ng-template> | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| .fee-estimation-container { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   margin-bottom: 10px; | ||||
|   @media (min-width: 376px) { | ||||
|     flex-direction: row; | ||||
|   }   | ||||
| @ -21,6 +22,7 @@ | ||||
|     } | ||||
| 
 | ||||
|     .card-text { | ||||
|       padding-top: 9px; | ||||
|       font-size: 22px; | ||||
|       span { | ||||
|         font-size: 11px; | ||||
| @ -48,10 +50,6 @@ | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .loading-container{ | ||||
|   min-height: 76px; | ||||
| } | ||||
| 
 | ||||
| .card-text { | ||||
|   .skeleton-loader { | ||||
|     width: 100%; | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | ||||
| import { Observable } from 'rxjs'; | ||||
| import { FederationAddress } from '../../../interfaces/node-api.interface'; | ||||
| import { Observable, combineLatest, map, of } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-federation-addresses-stats', | ||||
| @ -9,12 +8,24 @@ import { FederationAddress } from '../../../interfaces/node-api.interface'; | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class FederationAddressesStatsComponent implements OnInit { | ||||
|   @Input() federationAddresses$: Observable<FederationAddress[]>; | ||||
|   @Input() federationAddressesOneMonthAgo$: Observable<any>; | ||||
|   @Input() federationAddressesNumber$: Observable<number>; | ||||
|   @Input() federationUtxosNumber$: Observable<number>; | ||||
|   federationWalletStats$: Observable<any>; | ||||
| 
 | ||||
|   constructor() { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.federationWalletStats$ = combineLatest([ | ||||
|       this.federationAddressesNumber$ ?? of(undefined), | ||||
|       this.federationUtxosNumber$ ?? of(undefined) | ||||
|     ]).pipe( | ||||
|       map(([address_count, utxo_count]) => { | ||||
|         if (address_count === undefined || utxo_count === undefined) { | ||||
|           return undefined; | ||||
|         } | ||||
|         return { address_count, utxo_count} | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -1,135 +1,133 @@ | ||||
| <div class="container-xl"> | ||||
|   <div [ngClass]="{'widget': widget}"> | ||||
| <div [ngClass]="{'container-xl': !widget, 'widget': widget}"> | ||||
| 
 | ||||
|     <div *ngIf="!widget"> | ||||
|       <h1 i18n="liquid.recent-pegs">Recent Peg-In / Out's</h1> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="clearfix"></div> | ||||
|      | ||||
|     <div style="min-height: 295px"> | ||||
|       <table class="table table-borderless"> | ||||
|         <thead style="vertical-align: middle;"> | ||||
|           <th class="transaction text-left" [ngClass]="{'widget': widget}" i18n="shared.transaction">Transaction</th> | ||||
|           <th class="timestamp text-left" i18n="shared.date" [ngClass]="{'widget': widget}">Date</th> | ||||
|           <th class="amount text-right" [ngClass]="{'widget': widget}" i18n="shared.amount">Amount</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> | ||||
|         </thead> | ||||
|         <tbody *ngIf="recentPegs$ | async as pegs; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> | ||||
|           <ng-container *ngIf="widget; else regularRows"> | ||||
|             <tr *ngFor="let peg of pegs | slice:0:5"> | ||||
|               <td class="transaction text-left widget"> | ||||
|                 <ng-container *ngIf="peg.amount > 0"> | ||||
|                   <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex"> | ||||
|                     <app-truncate [text]="peg.txid"></app-truncate> | ||||
|                   </a> | ||||
|                 </ng-container> | ||||
|                 <ng-container *ngIf="peg.amount < 0"> | ||||
|                   <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex"> | ||||
|                     <app-truncate [text]="peg.txid"></app-truncate> | ||||
|                   </a> | ||||
|                 </ng-container> | ||||
|               </td> | ||||
|               <td class="timestamp text-left widget"> | ||||
|                 <app-time kind="since" [time]="peg.blocktime"></app-time> | ||||
|               </td> | ||||
|               <td class="amount text-right widget" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}"> | ||||
|                 <app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </ng-container> | ||||
|           <ng-template #regularRows> | ||||
|             <tr *ngFor="let peg of pegs | slice:(page - 1) * pageSize:page * pageSize"> | ||||
|               <td class="transaction text-left"> | ||||
|                 <ng-container *ngIf="peg.amount > 0"> | ||||
|                   <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex"> | ||||
|                     <app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate> | ||||
|                   </a> | ||||
|                 </ng-container> | ||||
|                 <ng-container *ngIf="peg.amount < 0"> | ||||
|                   <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex"> | ||||
|                     <app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate> | ||||
|                   </a> | ||||
|                 </ng-container> | ||||
|               </td> | ||||
|               <td class="timestamp text-left"> | ||||
|                 ‎{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }} | ||||
|                 <div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="peg.blocktime"></app-time>)</i></div> | ||||
|               </td> | ||||
|               <td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}"> | ||||
|                 <app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount> | ||||
|               </td> | ||||
|               <td class="output text-left"> | ||||
|                 <ng-container *ngIf="peg.bitcointxid; else redeemInProgress"> | ||||
|                   <a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + peg.bitcointxid + ':' + peg.bitcoinindex }}" target="_blank" style="color:#b86d12"> | ||||
|                     <app-truncate [text]="peg.bitcointxid + ':' + peg.bitcoinindex" [lastChars]="6"></app-truncate> | ||||
|                   </a> | ||||
|                 </ng-container> | ||||
|                 <ng-template #redeemInProgress> | ||||
|                   <ng-container *ngIf="peg.bitcoinaddress; else noRedeem"> | ||||
|                     <i><span class="text-muted" i18n="liquid.redemption-in-progress">Peg out in progress...</span></i> | ||||
|                   </ng-container> | ||||
|                 </ng-template> | ||||
|               </td> | ||||
|               <td class="address text-left"> | ||||
|                 <ng-container *ngIf="peg.bitcoinaddress; else noRedeem"> | ||||
|                   <a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + peg.bitcoinaddress }}" target="_blank" style="color:#b86d12"> | ||||
|                     <app-truncate [text]="peg.bitcoinaddress" [lastChars]="6"></app-truncate> | ||||
|                   </a> | ||||
|                 </ng-container> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </ng-template> | ||||
|         </tbody> | ||||
|         <ng-template #skeleton> | ||||
|           <tbody *ngIf="widget; else regularRowsSkeleton"> | ||||
|             <tr *ngFor="let item of skeletonLines"> | ||||
|               <td class="transaction text-left widget"> | ||||
|                 <span class="skeleton-loader" style="max-width: 400px"></span> | ||||
|               </td> | ||||
|               <td class="timestamp text-left widget"> | ||||
|                 <span class="skeleton-loader" style="max-width: 300px"></span> | ||||
|               </td> | ||||
|               <td class="amount text-right widget"> | ||||
|                 <span class="skeleton-loader" style="max-width: 300px"></span> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|           <ng-template #regularRowsSkeleton> | ||||
|             <tr *ngFor="let item of skeletonLines"> | ||||
|               <td class="transaction text-left"> | ||||
|                 <span class="skeleton-loader" style="max-width: 300px"></span> | ||||
|               </td> | ||||
|               <td class="timestamp text-left"> | ||||
|                 <span class="skeleton-loader" style="max-width: 140px"></span> | ||||
|               </td> | ||||
|               <td class="amount text-right"> | ||||
|                 <span class="skeleton-loader" style="max-width: 140px"></span> | ||||
|               </td> | ||||
|               <td class="output text-left"> | ||||
|                 <span class="skeleton-loader" style="max-width: 300px"></span> | ||||
|               </td> | ||||
|               <td class="address text-left"> | ||||
|                 <span class="skeleton-loader" style="max-width: 140px"></span> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </ng-template> | ||||
|         </ng-template> | ||||
|       </table> | ||||
| 
 | ||||
|       <ngb-pagination *ngIf="!widget && recentPegs$ | async as pegs" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''" | ||||
|         [collectionSize]="pegs.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page" | ||||
|         (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false"> | ||||
|       </ngb-pagination> | ||||
| 
 | ||||
|       <ng-template [ngIf]="!widget"> | ||||
|         <div class="clearfix"></div> | ||||
|         <br> | ||||
|       </ng-template> | ||||
|     </div> | ||||
|      | ||||
|   <div *ngIf="!widget"> | ||||
|     <h1 i18n="liquid.recent-pegs">Recent Peg-In / Out's</h1> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="clearfix"></div> | ||||
|    | ||||
|   <div style="min-height: 295px"> | ||||
|     <table class="table table-borderless"> | ||||
|       <thead style="vertical-align: middle;"> | ||||
|         <th class="transaction text-left" [ngClass]="{'widget': widget}" i18n="shared.transaction">Transaction</th> | ||||
|         <th class="timestamp text-left" i18n="shared.date" [ngClass]="{'widget': widget}">Date</th> | ||||
|         <th class="amount text-right" [ngClass]="{'widget': widget}" i18n="shared.amount">Amount</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> | ||||
|       </thead> | ||||
|       <tbody *ngIf="recentPegsList$ | async as pegs; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> | ||||
|         <ng-container *ngIf="widget; else regularRows"> | ||||
|           <tr *ngFor="let peg of pegs | slice:0:5"> | ||||
|             <td class="transaction text-left widget"> | ||||
|               <ng-container *ngIf="peg.amount > 0"> | ||||
|                 <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex"> | ||||
|                   <app-truncate [text]="peg.txid"></app-truncate> | ||||
|                 </a> | ||||
|               </ng-container> | ||||
|               <ng-container *ngIf="peg.amount < 0"> | ||||
|                 <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex"> | ||||
|                   <app-truncate [text]="peg.txid"></app-truncate> | ||||
|                 </a> | ||||
|               </ng-container> | ||||
|             </td> | ||||
|             <td class="timestamp text-left widget"> | ||||
|               <app-time kind="since" [time]="peg.blocktime"></app-time> | ||||
|             </td> | ||||
|             <td class="amount text-right widget" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}"> | ||||
|               <app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </ng-container> | ||||
|         <ng-template #regularRows> | ||||
|           <tr *ngFor="let peg of pegs;"> | ||||
|             <td class="transaction text-left"> | ||||
|               <ng-container *ngIf="peg.amount > 0"> | ||||
|                 <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex"> | ||||
|                   <app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate> | ||||
|                 </a> | ||||
|               </ng-container> | ||||
|               <ng-container *ngIf="peg.amount < 0"> | ||||
|                 <a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex"> | ||||
|                   <app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate> | ||||
|                 </a> | ||||
|               </ng-container> | ||||
|             </td> | ||||
|             <td class="timestamp text-left"> | ||||
|               ‎{{ peg.blocktime * 1000 | date:'yyyy-MM-dd HH:mm' }} | ||||
|               <div class="symbol lg-inline relative-time"><i>(<app-time kind="since" [time]="peg.blocktime"></app-time>)</i></div> | ||||
|             </td> | ||||
|             <td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}"> | ||||
|               <app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount> | ||||
|             </td> | ||||
|             <td class="output text-left"> | ||||
|               <ng-container *ngIf="peg.bitcointxid; else redeemInProgress"> | ||||
|                 <a href="{{ env.MEMPOOL_WEBSITE_URL + '/tx/' + peg.bitcointxid + ':' + peg.bitcoinindex }}" target="_blank" style="color:#b86d12"> | ||||
|                   <app-truncate [text]="peg.bitcointxid + ':' + peg.bitcoinindex" [lastChars]="6"></app-truncate> | ||||
|                 </a> | ||||
|               </ng-container> | ||||
|               <ng-template #redeemInProgress> | ||||
|                 <ng-container *ngIf="peg.bitcoinaddress; else noRedeem"> | ||||
|                   <i><span class="text-muted" i18n="liquid.redemption-in-progress">Peg out in progress...</span></i> | ||||
|                 </ng-container> | ||||
|               </ng-template> | ||||
|             </td> | ||||
|             <td class="address text-left"> | ||||
|               <ng-container *ngIf="peg.bitcoinaddress; else noRedeem"> | ||||
|                 <a href="{{ env.MEMPOOL_WEBSITE_URL + '/address/' + peg.bitcoinaddress }}" target="_blank" style="color:#b86d12"> | ||||
|                   <app-truncate [text]="peg.bitcoinaddress" [lastChars]="6"></app-truncate> | ||||
|                 </a> | ||||
|               </ng-container> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </ng-template> | ||||
|       </tbody> | ||||
|       <ng-template #skeleton> | ||||
|         <tbody *ngIf="widget; else regularRowsSkeleton"> | ||||
|           <tr *ngFor="let item of skeletonLines"> | ||||
|             <td class="transaction text-left widget"> | ||||
|               <span class="skeleton-loader" style="max-width: 400px"></span> | ||||
|             </td> | ||||
|             <td class="timestamp text-left widget"> | ||||
|               <span class="skeleton-loader" style="max-width: 300px"></span> | ||||
|             </td> | ||||
|             <td class="amount text-right widget"> | ||||
|               <span class="skeleton-loader" style="max-width: 300px"></span> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|         <ng-template #regularRowsSkeleton> | ||||
|           <tr *ngFor="let item of skeletonLines"> | ||||
|             <td class="transaction text-left"> | ||||
|               <span class="skeleton-loader" style="max-width: 300px"></span> | ||||
|             </td> | ||||
|             <td class="timestamp text-left"> | ||||
|               <span class="skeleton-loader" style="max-width: 240px"></span> | ||||
|             </td> | ||||
|             <td class="amount text-right"> | ||||
|               <span class="skeleton-loader" style="max-width: 140px"></span> | ||||
|             </td> | ||||
|             <td class="output text-left"> | ||||
|               <span class="skeleton-loader" style="max-width: 300px"></span> | ||||
|             </td> | ||||
|             <td class="address text-left"> | ||||
|               <span class="skeleton-loader" style="max-width: 240px"></span> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </ng-template> | ||||
|       </ng-template> | ||||
|     </table> | ||||
| 
 | ||||
|     <ngb-pagination *ngIf="!widget && pegsCount$ | async as pegsCount" class="pagination-container float-right mt-2" [class]="isLoading || isPegCountLoading ? 'disabled' : ''" | ||||
|       [collectionSize]="pegsCount" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page" | ||||
|       (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false"> | ||||
|     </ngb-pagination> | ||||
| 
 | ||||
|     <ng-template [ngIf]="!widget"> | ||||
|       <div class="clearfix"></div> | ||||
|       <br> | ||||
|     </ng-template> | ||||
|   </div> | ||||
|    | ||||
| </div> | ||||
| 
 | ||||
| <br> | ||||
|  | ||||
| @ -6,11 +6,14 @@ | ||||
| 
 | ||||
| tr, td, th { | ||||
|   border: 0px; | ||||
|   padding-top: 0.65rem !important; | ||||
|   padding-bottom: 0.6rem !important; | ||||
|   padding-right: 2rem !important; | ||||
|   .widget { | ||||
|     padding-right: 1rem !important; | ||||
|   padding-top: 0.65rem; | ||||
|   padding-bottom: 0.6rem; | ||||
|   padding-right: 2rem; | ||||
|   .widget &.widget { | ||||
|     padding-right: 1rem; | ||||
|     @media (max-width: 510px) { | ||||
|       padding-right: 0.5rem; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @ -93,7 +96,7 @@ tr, td, th { | ||||
|     display: block; | ||||
|   } | ||||
| 
 | ||||
|   @media (max-width: 500px) { | ||||
|   @media (max-width: 510px) { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; | ||||
| import { Observable, Subject, combineLatest, of, timer } from 'rxjs'; | ||||
| import { BehaviorSubject, Observable, Subject, combineLatest, of, timer } from 'rxjs'; | ||||
| import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; | ||||
| import { ApiService } from '../../../services/api.service'; | ||||
| import { Env, StateService } from '../../../services/state.service'; | ||||
| import { AuditStatus, CurrentPegs, FederationUtxo, RecentPeg } from '../../../interfaces/node-api.interface'; | ||||
| import { AuditStatus, CurrentPegs, RecentPeg } from '../../../interfaces/node-api.interface'; | ||||
| import { WebsocketService } from '../../../services/websocket.service'; | ||||
| import { SeoService } from '../../../services/seo.service'; | ||||
| 
 | ||||
| @ -15,21 +15,22 @@ import { SeoService } from '../../../services/seo.service'; | ||||
| }) | ||||
| export class RecentPegsListComponent implements OnInit { | ||||
|   @Input() widget: boolean = false; | ||||
|   @Input() recentPegIns$: Observable<RecentPeg[]> = of([]); | ||||
|   @Input() recentPegOuts$: Observable<RecentPeg[]> = of([]); | ||||
|   @Input() recentPegsList$: Observable<RecentPeg[]>; | ||||
| 
 | ||||
|   env: Env; | ||||
|   isLoading = true; | ||||
|   isPegCountLoading = true; | ||||
|   page = 1; | ||||
|   pageSize = 15; | ||||
|   maxSize = window.innerWidth <= 767.98 ? 3 : 5; | ||||
|   skeletonLines: number[] = []; | ||||
|   auditStatus$: Observable<AuditStatus>; | ||||
|   auditUpdated$: Observable<boolean>; | ||||
|   federationUtxos$: Observable<FederationUtxo[]>; | ||||
|   recentPegs$: Observable<RecentPeg[]>; | ||||
|   lastReservesBlockUpdate: number = 0; | ||||
|   currentPeg$: Observable<CurrentPegs>; | ||||
|   pegsCount$: Observable<number>; | ||||
|   startingIndexSubject: BehaviorSubject<number> = new BehaviorSubject(0); | ||||
|   currentIndex: number = 0; | ||||
|   lastPegBlockUpdate: number = 0; | ||||
|   lastPegAmount: string = ''; | ||||
|   isLoad: boolean = true; | ||||
| @ -93,53 +94,36 @@ export class RecentPegsListComponent implements OnInit { | ||||
|         share() | ||||
|       ); | ||||
| 
 | ||||
|       this.federationUtxos$ = this.auditUpdated$.pipe( | ||||
|       this.pegsCount$ = this.auditUpdated$.pipe( | ||||
|         filter(auditUpdated => auditUpdated === true), | ||||
|         throttleTime(40000), | ||||
|         switchMap(_ => this.apiService.federationUtxos$()), | ||||
|         tap(() => this.isPegCountLoading = true), | ||||
|         switchMap(_ => this.apiService.pegsCount$()), | ||||
|         map((data) => data.pegs_count), | ||||
|         tap(() => this.isPegCountLoading = false), | ||||
|         share() | ||||
|       ); | ||||
| 
 | ||||
|       this.recentPegIns$ = this.federationUtxos$.pipe( | ||||
|         map(federationUtxos => federationUtxos.filter(utxo => utxo.pegtxid).map(utxo => { | ||||
|           return { | ||||
|             txid: utxo.pegtxid, | ||||
|             txindex: utxo.pegindex, | ||||
|             amount: utxo.amount, | ||||
|             bitcoinaddress: utxo.bitcoinaddress, | ||||
|             bitcointxid: utxo.txid, | ||||
|             bitcoinindex: utxo.txindex, | ||||
|             blocktime: utxo.pegblocktime, | ||||
|           } | ||||
|         })), | ||||
|         share() | ||||
|       ); | ||||
| 
 | ||||
|       this.recentPegOuts$ = this.auditUpdated$.pipe( | ||||
|         filter(auditUpdated => auditUpdated === true), | ||||
|         throttleTime(40000), | ||||
|         switchMap(_ => this.apiService.recentPegOuts$()), | ||||
|       this.recentPegsList$ = combineLatest([ | ||||
|         this.auditStatus$, | ||||
|         this.auditUpdated$, | ||||
|         this.startingIndexSubject | ||||
|       ]).pipe( | ||||
|         filter(([auditStatus, auditUpdated, startingIndex]) => { | ||||
|           const auditStatusCheck = auditStatus.isAuditSynced === true; | ||||
|           const auditUpdatedCheck = auditUpdated === true; | ||||
|           const startingIndexCheck = startingIndex !== this.currentIndex; | ||||
|           return auditStatusCheck && (auditUpdatedCheck || startingIndexCheck); | ||||
|         }), | ||||
|         tap(([_, __, startingIndex]) => { | ||||
|           this.currentIndex = startingIndex; | ||||
|           this.isLoading = true; | ||||
|         }), | ||||
|         switchMap(([_, __, startingIndex]) => this.apiService.recentPegsList$(startingIndex)), | ||||
|         tap(() => this.isLoading = false), | ||||
|         share() | ||||
|       ); | ||||
|    | ||||
|     } | ||||
| 
 | ||||
|     this.recentPegs$ = combineLatest([ | ||||
|       this.recentPegIns$, | ||||
|       this.recentPegOuts$ | ||||
|     ]).pipe( | ||||
|       map(([recentPegIns, recentPegOuts]) => { | ||||
|         return [ | ||||
|           ...recentPegIns, | ||||
|           ...recentPegOuts | ||||
|         ].sort((a, b) => { | ||||
|           return b.blocktime - a.blocktime; | ||||
|         }); | ||||
|       }), | ||||
|       filter(recentPegs => recentPegs.length > 0), | ||||
|       tap(_ => this.isLoading = false), | ||||
|       share() | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
| @ -148,6 +132,7 @@ export class RecentPegsListComponent implements OnInit { | ||||
|   } | ||||
| 
 | ||||
|   pageChange(page: number): void { | ||||
|     this.startingIndexSubject.next((page - 1) * 15); | ||||
|     this.page = page; | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| .fee-estimation-container { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   margin-bottom: 10px; | ||||
|   @media (min-width: 376px) { | ||||
|     flex-direction: row; | ||||
|   }   | ||||
|  | ||||
| @ -1,98 +0,0 @@ | ||||
| <div class="container-xl dashboard-container" *ngIf="(auditStatus$ | async)?.isAuditSynced; else auditInProgress"> | ||||
| 
 | ||||
|   <div class="row row-cols-1 row-cols-md-2"> | ||||
| 
 | ||||
|     <div class="col"> | ||||
|       <div class="card"> | ||||
|         <div class="card-body"> | ||||
|           <app-reserves-supply-stats [currentPeg$]="currentPeg$" [currentReserves$]="currentReserves$"></app-reserves-supply-stats> | ||||
|           <app-reserves-ratio [currentPeg]="currentPeg$ | async" [currentReserves]="currentReserves$ | async"></app-reserves-ratio> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="col" style="margin-bottom: 1.47rem"> | ||||
|       <div class="card">  | ||||
|         <div class="card-title"> | ||||
|           <app-reserves-ratio-stats [fullHistory$]="fullHistory$"></app-reserves-ratio-stats> | ||||
|         </div> | ||||
|         <div class="card-body pl-0" style="padding-top: 10px;"> | ||||
|           <app-reserves-ratio-graph [data]="fullHistory$ | async"></app-reserves-ratio-graph> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="col"> | ||||
|       <div class="card"> | ||||
|         <div class="card-body"> | ||||
|           <app-recent-pegs-stats [pegsVolume$]="pegsVolume$"></app-recent-pegs-stats> | ||||
|           <app-recent-pegs-list [recentPegIns$]="recentPegIns$" [recentPegOuts$]="recentPegOuts$"[widget]="true"></app-recent-pegs-list> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="col" style="margin-bottom: 1.47rem"> | ||||
|       <div class="card"> | ||||
|         <div class="card-body"> | ||||
|           <app-federation-addresses-stats [federationAddresses$]="federationAddresses$" [federationAddressesOneMonthAgo$]="federationAddressesOneMonthAgo$"></app-federation-addresses-stats> | ||||
|           <app-federation-addresses-list [federationAddresses$]="federationAddresses$" [widget]="true"></app-federation-addresses-list> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| <ng-template #loadingSkeleton> | ||||
|   <div class="container-xl dashboard-container"> | ||||
| 
 | ||||
|     <div class="row row-cols-1 row-cols-md-2"> | ||||
|    | ||||
|       <div class="col"> | ||||
|         <div class="card"> | ||||
|           <div class="card-body"> | ||||
|             <app-reserves-supply-stats></app-reserves-supply-stats> | ||||
|             <app-reserves-ratio></app-reserves-ratio> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="col" style="margin-bottom: 1.47rem"> | ||||
|         <div class="card">  | ||||
|           <div class="card-title"> | ||||
|             <app-reserves-ratio-stats></app-reserves-ratio-stats> | ||||
|           </div> | ||||
|           <div class="card-body pl-0" style="padding-top: 10px;"> | ||||
|             <app-reserves-ratio-graph></app-reserves-ratio-graph> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|    | ||||
|       <div class="col"> | ||||
|         <div class="card"> | ||||
|           <div class="card-body"> | ||||
|             <app-recent-pegs-stats></app-recent-pegs-stats> | ||||
|             <app-recent-pegs-list [widget]="true"></app-recent-pegs-list> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|    | ||||
|       <div class="col" style="margin-bottom: 1.47rem"> | ||||
|         <div class="card"> | ||||
|           <div class="card-body"> | ||||
|             <app-federation-addresses-stats></app-federation-addresses-stats> | ||||
|             <app-federation-addresses-list [widget]="true"></app-federation-addresses-list> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #auditInProgress> | ||||
|   <ng-container *ngIf="(auditStatus$ | async) as auditStatus; else loadingSkeleton"> | ||||
|     <div class="in-progress-message" *ngIf="auditStatus.lastBlockAudit && auditStatus.bitcoinHeaders; else loadingSkeleton"> | ||||
|       <span i18n="liquid.audit-in-progress">Audit in progress: Bitcoin block height #{{ auditStatus.lastBlockAudit }} / #{{ auditStatus.bitcoinHeaders }}</span> | ||||
|     </div> | ||||
|   </ng-container> | ||||
| </ng-template> | ||||
| @ -1,138 +0,0 @@ | ||||
| .dashboard-container { | ||||
|   text-align: center; | ||||
|   margin-top: 0.5rem; | ||||
|   .col { | ||||
|     margin-bottom: 1.5rem; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .card { | ||||
|   background-color: #1d1f31; | ||||
| } | ||||
| 
 | ||||
| .card-title { | ||||
|   padding-top: 20px; | ||||
| } | ||||
| 
 | ||||
| .card-body.pool-ranking { | ||||
|   padding: 1.25rem 0.25rem 0.75rem 0.25rem; | ||||
| } | ||||
| .card-text { | ||||
|   font-size: 22px; | ||||
| } | ||||
| 
 | ||||
| #blockchain-container { | ||||
|   position: relative; | ||||
|   overflow-x: scroll; | ||||
|   overflow-y: hidden; | ||||
|   scrollbar-width: none; | ||||
|   -ms-overflow-style: none; | ||||
| } | ||||
| 
 | ||||
| #blockchain-container::-webkit-scrollbar { | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| .fade-border { | ||||
|   -webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%) | ||||
| } | ||||
| 
 | ||||
| .in-progress-message { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   margin-top: 20px; | ||||
|   text-align: center; | ||||
|   padding-bottom: 3px; | ||||
|   font-weight: 500; | ||||
| } | ||||
| 
 | ||||
| .more-padding { | ||||
|   padding: 24px 20px !important; | ||||
| } | ||||
| 
 | ||||
| .card-wrapper { | ||||
|   .card { | ||||
|     height: auto !important; | ||||
|   } | ||||
|   .card-body { | ||||
|     display: flex; | ||||
|     flex: inherit; | ||||
|     text-align: center; | ||||
|     flex-direction: column; | ||||
|     justify-content: space-around; | ||||
|     padding: 22px 20px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .skeleton-loader { | ||||
|   width: 100%; | ||||
|   display: block; | ||||
|   &:first-child { | ||||
|     max-width: 90px; | ||||
|     margin: 15px auto 3px; | ||||
|   } | ||||
|   &:last-child { | ||||
|     margin: 10px auto 3px; | ||||
|     max-width: 55px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .card-text { | ||||
|   font-size: 22px; | ||||
| } | ||||
| 
 | ||||
| .title-link, .title-link:hover, .title-link:focus, .title-link:active { | ||||
|   display: block; | ||||
|   margin-bottom: 10px; | ||||
|   text-decoration: none; | ||||
|   color: inherit; | ||||
| } | ||||
| 
 | ||||
| .lastest-blocks-table { | ||||
|   width: 100%; | ||||
|   text-align: left; | ||||
|   tr, td, th { | ||||
|     border: 0px; | ||||
|     padding-top: 0.65rem !important; | ||||
|     padding-bottom: 0.8rem !important; | ||||
|   } | ||||
|   .table-cell-height { | ||||
|     width: 25%; | ||||
|   } | ||||
|   .table-cell-fee { | ||||
|     width: 25%; | ||||
|     text-align: right; | ||||
|   } | ||||
|   .table-cell-pool { | ||||
|     text-align: left; | ||||
|     width: 30%; | ||||
| 
 | ||||
|     @media (max-width: 875px) { | ||||
|       display: none; | ||||
|     } | ||||
| 
 | ||||
|     .pool-name { | ||||
|       margin-left: 1em; | ||||
|     } | ||||
|   } | ||||
|   .table-cell-acceleration-count { | ||||
|     text-align: right; | ||||
|     width: 20%; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .card { | ||||
|   height: 385px; | ||||
| } | ||||
| .list-card { | ||||
|   height: 410px; | ||||
|   @media (max-width: 767px) { | ||||
|     height: auto; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .mempool-block-wrapper { | ||||
|   max-height: 380px; | ||||
|   max-width: 380px; | ||||
|   margin: auto; | ||||
| } | ||||
| @ -1,212 +0,0 @@ | ||||
| import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | ||||
| import { SeoService } from '../../../services/seo.service'; | ||||
| import { WebsocketService } from '../../../services/websocket.service'; | ||||
| import { StateService } from '../../../services/state.service'; | ||||
| import { Observable, Subject, combineLatest, delayWhen, filter, interval, map, of, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime, timer } from 'rxjs'; | ||||
| import { ApiService } from '../../../services/api.service'; | ||||
| import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo, PegsVolume, RecentPeg } from '../../../interfaces/node-api.interface'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-reserves-audit-dashboard', | ||||
|   templateUrl: './reserves-audit-dashboard.component.html', | ||||
|   styleUrls: ['./reserves-audit-dashboard.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class ReservesAuditDashboardComponent implements OnInit { | ||||
|   auditStatus$: Observable<AuditStatus>; | ||||
|   auditUpdated$: Observable<boolean>; | ||||
|   currentPeg$: Observable<CurrentPegs>; | ||||
|   currentReserves$: Observable<CurrentPegs>; | ||||
|   federationUtxos$: Observable<FederationUtxo[]>; | ||||
|   recentPegIns$: Observable<RecentPeg[]>; | ||||
|   recentPegOuts$: Observable<RecentPeg[]>; | ||||
|   pegsVolume$: Observable<PegsVolume[]>; | ||||
|   federationAddresses$: Observable<FederationAddress[]>; | ||||
|   federationAddressesOneMonthAgo$: Observable<any>; | ||||
|   liquidPegsMonth$: Observable<any>; | ||||
|   liquidReservesMonth$: Observable<any>; | ||||
|   fullHistory$: Observable<any>; | ||||
|   isLoad: boolean = true; | ||||
|   private lastPegBlockUpdate: number = 0; | ||||
|   private lastPegAmount: string = ''; | ||||
|   private lastReservesBlockUpdate: number = 0; | ||||
| 
 | ||||
|   private destroy$ = new Subject(); | ||||
| 
 | ||||
|   constructor( | ||||
|     private seoService: SeoService, | ||||
|     private websocketService: WebsocketService, | ||||
|     private apiService: ApiService, | ||||
|     private stateService: StateService, | ||||
|   ) { | ||||
|     this.seoService.setTitle($localize`:@@liquid.reserves-audit:Reserves Audit Dashboard`); | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.websocketService.want(['blocks', 'mempool-blocks']); | ||||
| 
 | ||||
|     this.auditStatus$ = this.stateService.blocks$.pipe( | ||||
|       takeUntil(this.destroy$), | ||||
|       throttleTime(40000), | ||||
|       delayWhen(_ => this.isLoad ? timer(0) : timer(2000)), | ||||
|       tap(() => this.isLoad = false), | ||||
|       switchMap(() => this.apiService.federationAuditSynced$()), | ||||
|       shareReplay(1), | ||||
|     ); | ||||
| 
 | ||||
|     this.currentPeg$ = this.auditStatus$.pipe( | ||||
|       filter(auditStatus => auditStatus.isAuditSynced === true), | ||||
|       switchMap(_ => | ||||
|         this.apiService.liquidPegs$().pipe( | ||||
|           filter((currentPegs) => currentPegs.lastBlockUpdate >= this.lastPegBlockUpdate), | ||||
|           tap((currentPegs) => { | ||||
|             this.lastPegBlockUpdate = currentPegs.lastBlockUpdate; | ||||
|           }) | ||||
|         ) | ||||
|       ), | ||||
|       share() | ||||
|     ); | ||||
| 
 | ||||
|     this.auditUpdated$ = combineLatest([ | ||||
|       this.auditStatus$, | ||||
|       this.currentPeg$ | ||||
|     ]).pipe( | ||||
|       filter(([auditStatus, _]) => auditStatus.isAuditSynced === true), | ||||
|       map(([auditStatus, currentPeg]) => ({ | ||||
|         lastBlockAudit: auditStatus.lastBlockAudit, | ||||
|         currentPegAmount: currentPeg.amount | ||||
|       })), | ||||
|       switchMap(({ lastBlockAudit, currentPegAmount }) => { | ||||
|         const blockAuditCheck = lastBlockAudit > this.lastReservesBlockUpdate; | ||||
|         const amountCheck = currentPegAmount !== this.lastPegAmount; | ||||
|         this.lastPegAmount = currentPegAmount; | ||||
|         return of(blockAuditCheck || amountCheck); | ||||
|       }), | ||||
|       share() | ||||
|     ); | ||||
| 
 | ||||
|     this.currentReserves$ = this.auditUpdated$.pipe( | ||||
|       filter(auditUpdated => auditUpdated === true), | ||||
|       throttleTime(40000), | ||||
|       switchMap(_ => | ||||
|         this.apiService.liquidReserves$().pipe( | ||||
|           filter((currentReserves) => currentReserves.lastBlockUpdate >= this.lastReservesBlockUpdate), | ||||
|           tap((currentReserves) => { | ||||
|             this.lastReservesBlockUpdate = currentReserves.lastBlockUpdate; | ||||
|           }) | ||||
|         ) | ||||
|       ), | ||||
|       share() | ||||
|     ); | ||||
| 
 | ||||
|     this.federationUtxos$ = this.auditUpdated$.pipe( | ||||
|       filter(auditUpdated => auditUpdated === true), | ||||
|       throttleTime(40000), | ||||
|       switchMap(_ => this.apiService.federationUtxos$()), | ||||
|       share() | ||||
|     ); | ||||
| 
 | ||||
|     this.recentPegIns$ = this.federationUtxos$.pipe( | ||||
|       map(federationUtxos => federationUtxos.filter(utxo => utxo.pegtxid).map(utxo => { | ||||
|         return { | ||||
|           txid: utxo.pegtxid, | ||||
|           txindex: utxo.pegindex, | ||||
|           amount: utxo.amount, | ||||
|           bitcoinaddress: utxo.bitcoinaddress, | ||||
|           bitcointxid: utxo.txid, | ||||
|           bitcoinindex: utxo.txindex, | ||||
|           blocktime: utxo.pegblocktime, | ||||
|         } | ||||
|       })), | ||||
|       share() | ||||
|     ); | ||||
| 
 | ||||
|     this.recentPegOuts$ = this.auditUpdated$.pipe( | ||||
|       filter(auditUpdated => auditUpdated === true), | ||||
|       throttleTime(40000), | ||||
|       switchMap(_ => this.apiService.recentPegOuts$()), | ||||
|       share() | ||||
|     ); | ||||
| 
 | ||||
|     this.pegsVolume$ = this.auditUpdated$.pipe( | ||||
|       filter(auditUpdated => auditUpdated === true), | ||||
|       throttleTime(40000), | ||||
|       switchMap(_ => this.apiService.pegsVolume$()), | ||||
|       share() | ||||
|     ); | ||||
| 
 | ||||
|     this.federationAddresses$ = this.auditUpdated$.pipe( | ||||
|       filter(auditUpdated => auditUpdated === true), | ||||
|       throttleTime(40000), | ||||
|       switchMap(_ => this.apiService.federationAddresses$()), | ||||
|       share() | ||||
|     ); | ||||
| 
 | ||||
|     this.federationAddressesOneMonthAgo$ = interval(60 * 60 * 1000) | ||||
|       .pipe( | ||||
|         startWith(0), | ||||
|         switchMap(() => this.apiService.federationAddressesOneMonthAgo$()) | ||||
|       ); | ||||
| 
 | ||||
|     this.liquidPegsMonth$ = interval(60 * 60 * 1000) | ||||
|       .pipe( | ||||
|         startWith(0), | ||||
|         switchMap(() => this.apiService.listLiquidPegsMonth$()), | ||||
|         map((pegs) => { | ||||
|           const labels = pegs.map(stats => stats.date); | ||||
|           const series = pegs.map(stats => parseFloat(stats.amount) / 100000000); | ||||
|           series.reduce((prev, curr, i) => series[i] = prev + curr, 0); | ||||
|           return { | ||||
|             series, | ||||
|             labels | ||||
|           }; | ||||
|         }), | ||||
|         share(), | ||||
|       ); | ||||
| 
 | ||||
|     this.liquidReservesMonth$ = interval(60 * 60 * 1000).pipe( | ||||
|       startWith(0), | ||||
|       switchMap(() => this.apiService.listLiquidReservesMonth$()), | ||||
|       map(reserves => { | ||||
|         const labels = reserves.map(stats => stats.date); | ||||
|         const series = reserves.map(stats => parseFloat(stats.amount) / 100000000); | ||||
|         return { | ||||
|           series, | ||||
|           labels | ||||
|         }; | ||||
|       }), | ||||
|       share() | ||||
|     ); | ||||
| 
 | ||||
|     this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$, this.currentReserves$]) | ||||
|       .pipe( | ||||
|         map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => { | ||||
|           liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000; | ||||
| 
 | ||||
|           if (liquidPegs.series.length === liquidReserves?.series.length) { | ||||
|             liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000; | ||||
|           } else if (liquidPegs.series.length === liquidReserves?.series.length + 1) { | ||||
|             liquidReserves.series.push(parseFloat(currentReserves?.amount) / 100000000); | ||||
|             liquidReserves.labels.push(liquidPegs.labels[liquidPegs.labels.length - 1]); | ||||
|           } else { | ||||
|             liquidReserves = { | ||||
|               series: [], | ||||
|               labels: [] | ||||
|             }; | ||||
|           } | ||||
| 
 | ||||
|           return { | ||||
|             liquidPegs, | ||||
|             liquidReserves | ||||
|           }; | ||||
|         }), | ||||
|         share() | ||||
|       ); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.destroy$.next(1); | ||||
|     this.destroy$.complete(); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| @ -14,7 +14,7 @@ | ||||
|         <h5 class="card-title" i18n="liquid.avg-peg-ratio">Avg Peg Ratio</h5>  | ||||
|         <div class="card-text"> | ||||
|           <div class="fee-text" [ngClass]="{'danger' : unbackedMonths.avg < 1, 'correct': unbackedMonths.avg >= 1}"> | ||||
|             {{ unbackedMonths.avg.toFixed(5) }} | ||||
|             {{ (unbackedMonths.avg * 100).toFixed(3) }} % | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
| @ -1,4 +0,0 @@ | ||||
| <div class="echarts" echarts [initOpts]="ratioHistoryChartInitOptions" [options]="ratioHistoryChartOptions" (chartRendered)="rendered()"></div> | ||||
| <div class="text-center loadingGraphs" *ngIf="isLoading"> | ||||
|   <div class="spinner-border text-light"></div> | ||||
| </div> | ||||
| @ -1,6 +0,0 @@ | ||||
| .loadingGraphs { | ||||
|   position: absolute; | ||||
|   top: 50%; | ||||
|   left: calc(50% - 16px); | ||||
|   z-index: 100; | ||||
| } | ||||
| @ -1,195 +0,0 @@ | ||||
| import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core'; | ||||
| import { formatDate, formatNumber } from '@angular/common'; | ||||
| import { EChartsOption } from '../../../graphs/echarts'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-reserves-ratio-graph', | ||||
|   templateUrl: './reserves-ratio-graph.component.html', | ||||
|   styleUrls: ['./reserves-ratio-graph.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class ReservesRatioGraphComponent implements OnInit, OnChanges { | ||||
|   @Input() data: any; | ||||
|   ratioHistoryChartOptions: EChartsOption; | ||||
|   ratioSeries: number[] = []; | ||||
| 
 | ||||
|   height: number | string = '200'; | ||||
|   right: number | string = '10'; | ||||
|   top: number | string = '20'; | ||||
|   left: number | string = '50'; | ||||
|   template: ('widget' | 'advanced') = 'widget'; | ||||
|   isLoading = true; | ||||
| 
 | ||||
|   ratioHistoryChartInitOptions = { | ||||
|     renderer: 'svg' | ||||
|   }; | ||||
| 
 | ||||
|   constructor( | ||||
|     @Inject(LOCALE_ID) private locale: string, | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.isLoading = true; | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges() { | ||||
|     if (!this.data) { | ||||
|       return; | ||||
|     } | ||||
|     // Compute the ratio series: the ratio of the reserves to the pegs
 | ||||
|     this.ratioSeries = this.data.liquidReserves.series.map((value: number, index: number) => value / this.data.liquidPegs.series[index]); | ||||
|     // Truncate the ratio series and labels series to last 3 years
 | ||||
|     this.ratioSeries = this.ratioSeries.slice(Math.max(this.ratioSeries.length - 36, 0)); | ||||
|     this.data.liquidPegs.labels = this.data.liquidPegs.labels.slice(Math.max(this.data.liquidPegs.labels.length - 36, 0)); | ||||
|     // Cut the values that are too high or too low
 | ||||
|     this.ratioSeries = this.ratioSeries.map((value: number) => Math.min(Math.max(value, 0.995), 1.005)); | ||||
|     this.ratioHistoryChartOptions = this.createChartOptions(this.ratioSeries, this.data.liquidPegs.labels); | ||||
|   } | ||||
| 
 | ||||
|   rendered() { | ||||
|     if (!this.data) { | ||||
|       return; | ||||
|     } | ||||
|     this.isLoading = false; | ||||
|   } | ||||
| 
 | ||||
|   createChartOptions(ratioSeries: number[], labels: string[]): EChartsOption { | ||||
|     return { | ||||
|       grid: { | ||||
|         height: this.height, | ||||
|         right: this.right, | ||||
|         top: this.top, | ||||
|         left: this.left, | ||||
|       }, | ||||
|       animation: false, | ||||
|       dataZoom: [{ | ||||
|         type: 'inside', | ||||
|         realtime: true, | ||||
|         zoomOnMouseWheel: (this.template === 'advanced') ? true : false, | ||||
|         maxSpan: 100, | ||||
|         minSpan: 10, | ||||
|       }, { | ||||
|         show: (this.template === 'advanced') ? true : false, | ||||
|         type: 'slider', | ||||
|         brushSelect: false, | ||||
|         realtime: true, | ||||
|         selectedDataBackground: { | ||||
|           lineStyle: { | ||||
|             color: '#fff', | ||||
|             opacity: 0.45, | ||||
|           }, | ||||
|           areaStyle: { | ||||
|             opacity: 0, | ||||
|           } | ||||
|         } | ||||
|       }], | ||||
|       tooltip: { | ||||
|         trigger: 'axis', | ||||
|         position: (pos, params, el, elRect, size) => { | ||||
|           const obj = { top: -20 }; | ||||
|           obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80; | ||||
|           return obj; | ||||
|         }, | ||||
|         extraCssText: `width: ${(this.template === 'widget') ? '125px' : '135px'};
 | ||||
|                       background: transparent; | ||||
|                       border: none; | ||||
|                       box-shadow: none;`,
 | ||||
|         axisPointer: { | ||||
|           type: 'line', | ||||
|         }, | ||||
|         formatter: (params: any) => { | ||||
|           const colorSpan = (color: string) => `<span class="indicator" style="background-color: ${color};"></span>`; | ||||
|           let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>'; | ||||
|           const item = params[0]; | ||||
|           const formattedValue = formatNumber(item.value, this.locale, '1.5-5'); | ||||
|           const symbol = (item.value === 1.005) ? '≥ ' : (item.value === 0.995) ? '≤ ' : ''; | ||||
|           itemFormatted += `<div class="item">
 | ||||
|             <div class="indicator-container">${colorSpan(item.color)}</div> | ||||
|             <div style="margin-right: 5px"></div> | ||||
|             <div class="value">${symbol}${formattedValue}</div> | ||||
|           </div>`;
 | ||||
|           return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`; | ||||
|         } | ||||
|       }, | ||||
|       xAxis: { | ||||
|         type: 'category', | ||||
|         axisLabel: { | ||||
|           align: 'center', | ||||
|           fontSize: 11, | ||||
|           lineHeight: 12 | ||||
|         }, | ||||
|         boundaryGap: false, | ||||
|         data: labels.map((value: any) => `${formatDate(value, 'MMM\ny', this.locale)}`), | ||||
|       }, | ||||
|       yAxis: { | ||||
|         type: 'value', | ||||
|         axisLabel: { | ||||
|           fontSize: 11, | ||||
|         }, | ||||
|         splitLine: { | ||||
|           lineStyle: { | ||||
|             type: 'dotted', | ||||
|             color: '#ffffff66', | ||||
|             opacity: 0.25, | ||||
|           } | ||||
|         }, | ||||
|         min: 0.995, | ||||
|         max: 1.005, | ||||
|       }, | ||||
|       series: [ | ||||
|         { | ||||
|           data: ratioSeries, | ||||
|           name: '', | ||||
|           type: 'line', | ||||
|           smooth: true, | ||||
|           showSymbol: false, | ||||
|           lineStyle: { | ||||
|             width: 3, | ||||
|              | ||||
|           }, | ||||
|           markLine: { | ||||
|             silent: true, | ||||
|             symbol: 'none', | ||||
|             lineStyle: { | ||||
|               color: '#fff', | ||||
|               opacity: 1, | ||||
|               width: 1, | ||||
|             }, | ||||
|             data: [{ | ||||
|               yAxis: 1, | ||||
|               label: { | ||||
|                 show: false, | ||||
|                 color: '#ffffff', | ||||
|               } | ||||
|             }], | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|       visualMap: { | ||||
|         show: false, | ||||
|         top: 50, | ||||
|         right: 10, | ||||
|         pieces: [{ | ||||
|           gt: 0, | ||||
|           lte: 0.999, | ||||
|           color: '#D81B60' | ||||
|         }, | ||||
|         { | ||||
|           gt: 0.999, | ||||
|           lte: 1.001, | ||||
|           color: '#FDD835' | ||||
|         }, | ||||
|         { | ||||
|           gt: 1.001, | ||||
|           lte: 2, | ||||
|           color: '#7CB342' | ||||
|         } | ||||
|         ], | ||||
|         outOfRange: { | ||||
|           color: '#999' | ||||
|         } | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @ -1,4 +1,4 @@ | ||||
| import { Component, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core'; | ||||
| import { Component, ChangeDetectionStrategy, Input, OnChanges, OnInit, HostListener } from '@angular/core'; | ||||
| import { EChartsOption } from '../../../graphs/echarts'; | ||||
| import { CurrentPegs } from '../../../interfaces/node-api.interface'; | ||||
| 
 | ||||
| @ -32,6 +32,10 @@ export class ReservesRatioComponent implements OnInit, OnChanges { | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges() { | ||||
|     this.updateChartOptions(); | ||||
|   } | ||||
| 
 | ||||
|   updateChartOptions() { | ||||
|     if (!this.currentPeg || !this.currentReserves || this.currentPeg.amount === '0') { | ||||
|       return; | ||||
|     } | ||||
| @ -46,13 +50,43 @@ export class ReservesRatioComponent implements OnInit, OnChanges { | ||||
|   } | ||||
| 
 | ||||
|   createChartOptions(currentPeg: CurrentPegs, currentReserves: CurrentPegs): EChartsOption { | ||||
|     const value = parseFloat(currentReserves.amount) / parseFloat(currentPeg.amount); | ||||
|     const hideMaxAxisLabels = value >= 1.001; | ||||
|     const hideMinAxisLabels = value <= 0.999; | ||||
| 
 | ||||
|     let axisFontSize = 14; | ||||
|     let pointerLength = '50%'; | ||||
|     let pointerWidth = 16; | ||||
|     let offsetCenter = ['0%', '-22%']; | ||||
|     if (window.innerWidth >= 992) { | ||||
|       axisFontSize = 14; | ||||
|       pointerLength = '50%'; | ||||
|       pointerWidth = 16; | ||||
|       offsetCenter = value >= 1.0007 || value <= 0.9993 ? ['0%', '-30%'] : ['0%', '-22%']; | ||||
|     } else if (window.innerWidth >= 768) { | ||||
|       axisFontSize = 10; | ||||
|       pointerLength = '35%'; | ||||
|       pointerWidth = 12; | ||||
|       offsetCenter = value >= 1.0007 || value <= 0.9993 ? ['0%', '-37%'] : ['0%', '-27%']; | ||||
|     } else if (window.innerWidth >= 450) { | ||||
|       axisFontSize = 14; | ||||
|       pointerLength = '45%'; | ||||
|       pointerWidth = 14; | ||||
|       offsetCenter = value >= 1.0007 || value <= 0.9993 ? ['0%', '-32%'] : ['0%', '-22%']; | ||||
|     } else { | ||||
|       axisFontSize = 10; | ||||
|       pointerLength = '35%'; | ||||
|       pointerWidth = 12; | ||||
|       offsetCenter = value >= 1.0007 || value <= 0.9993 ? ['0%', '-37%'] : ['0%', '-27%']; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       series: [ | ||||
|         { | ||||
|           type: 'gauge', | ||||
|           startAngle: 180, | ||||
|           endAngle: 0, | ||||
|           center: ['50%', '70%'], | ||||
|           center: ['50%', '75%'], | ||||
|           radius: '100%', | ||||
|           min: 0.999, | ||||
|           max: 1.001, | ||||
| @ -68,13 +102,23 @@ export class ReservesRatioComponent implements OnInit, OnChanges { | ||||
|           }, | ||||
|           axisLabel: { | ||||
|             color: 'inherit',         | ||||
|             fontFamily: 'inherit',     | ||||
|             fontFamily: 'inherit',   | ||||
|             fontSize: axisFontSize,   | ||||
|             formatter: function (value) { | ||||
|               if (value === 0.999) { | ||||
|                 return hideMinAxisLabels ? '' : '99.9%'; | ||||
|               } else if (value === 1.001) { | ||||
|                 return hideMaxAxisLabels ? '' : '100.1%'; | ||||
|               } else { | ||||
|                 return '100%'; | ||||
|               } | ||||
|             }, | ||||
|           }, | ||||
|           pointer: { | ||||
|             icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z', | ||||
|             length: '50%', | ||||
|             width: 16, | ||||
|             offsetCenter: [0, '-27%'], | ||||
|             length: pointerLength, | ||||
|             width: pointerWidth, | ||||
|             offsetCenter: offsetCenter, | ||||
|             itemStyle: { | ||||
|               color: 'auto' | ||||
|             } | ||||
| @ -95,7 +139,7 @@ export class ReservesRatioComponent implements OnInit, OnChanges { | ||||
|           }, | ||||
|           title: { | ||||
|             show: true, | ||||
|             offsetCenter: [0, '-117.5%'], | ||||
|             offsetCenter: [0, '-127%'], | ||||
|             fontSize: 18, | ||||
|             color: '#4a68b9', | ||||
|             fontFamily: 'inherit', | ||||
| @ -108,19 +152,24 @@ export class ReservesRatioComponent implements OnInit, OnChanges { | ||||
|             fontFamily: 'inherit', | ||||
|             fontWeight: 500, | ||||
|             formatter: function (value) { | ||||
|               return (value).toFixed(5); | ||||
|               return (value * 100).toFixed(3) + '%'; | ||||
|             }, | ||||
|             color: 'inherit' | ||||
|           }, | ||||
|           data: [ | ||||
|             { | ||||
|               value: parseFloat(currentReserves.amount) / parseFloat(currentPeg.amount), | ||||
|               name: 'Peg-O-Meter' | ||||
|               value: value, | ||||
|               name: 'Assets vs Liabilities' | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       ] | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   onResize(): void { | ||||
|     this.updateChartOptions(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,44 +1,29 @@ | ||||
| <div *ngIf="(currentPeg$ | async) as currentPeg; else loadingData"> | ||||
|   <div *ngIf="(currentReserves$ | async) as currentReserves; else loadingData"> | ||||
|     <div class="fee-estimation-container"> | ||||
|       <div class="item"> | ||||
|         <h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5> | ||||
|         <div class="card-text"> | ||||
|           <div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div> | ||||
|           <span class="fiat"> | ||||
|             <span>As of block <a [routerLink]="['/block', currentPeg.hash]">{{ currentPeg.lastBlockUpdate }}</a></span> | ||||
|           </span> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="item"> | ||||
|         <h5 class="card-title" i18n="dashboard.btc-reserves">BTC Reserves</h5> | ||||
|         <div class="card-text"> | ||||
|           <div class="fee-text">{{ (+currentReserves.amount) / 100000000 | number: '1.2-2' }} <span style="color: #b86d12;">BTC</span></div> | ||||
|           <span class="fiat"> | ||||
|             <span>As of block <a href="{{ env.MEMPOOL_WEBSITE_URL + '/block/' + currentReserves.hash }}" target="_blank" style="color:#b86d12">{{ currentReserves.lastBlockUpdate }}</a></span> | ||||
|           </span> | ||||
|         </div> | ||||
|       </div> | ||||
| <div class="fee-estimation-container"> | ||||
|   <div class="item"> | ||||
|     <h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5> | ||||
|     <div *ngIf="(currentPeg$ | async) as currentPeg; else loadingData" class="card-text"> | ||||
|       <div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div> | ||||
|       <span class="fiat"> | ||||
|         <span><ng-container i18n="shared.as-of-block">As of block</ng-container> <a [routerLink]="['/block', currentPeg.hash]">{{ currentPeg.lastBlockUpdate }}</a></span> | ||||
|       </span> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="item"> | ||||
|     <h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5> | ||||
|     <div *ngIf="(currentReserves$ | async) as currentReserves; else loadingData" class="card-text"> | ||||
|       <div class="fee-text">{{ (+currentReserves.amount) / 100000000 | number: '1.2-2' }} <span style="color: #b86d12;">BTC</span></div> | ||||
|       <span class="fiat"> | ||||
|         <span><ng-container i18n="shared.as-of-block">As of block</ng-container> <a href="{{ env.MEMPOOL_WEBSITE_URL + '/block/' + currentReserves.hash }}" target="_blank" style="color:#b86d12">{{ currentReserves.lastBlockUpdate }}</a></span> | ||||
|       </span> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| <ng-template #loadingData> | ||||
|   <div class="fee-estimation-container loading-container"> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5> | ||||
|       <div class="card-text"> | ||||
|         <div class="skeleton-loader"></div> | ||||
|         <div class="skeleton-loader"></div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="dashboard.btc-reserves">BTC Reserves</h5> | ||||
|       <div class="card-text"> | ||||
|         <div class="skeleton-loader"></div> | ||||
|         <div class="skeleton-loader"></div> | ||||
|       </div> | ||||
|     </div> | ||||
|   <div class="card-text"> | ||||
|     <div class="skeleton-loader"></div> | ||||
|     <div class="skeleton-loader"></div> | ||||
|   </div> | ||||
| </ng-template> | ||||
| 
 | ||||
|  | ||||
| @ -41,6 +41,7 @@ | ||||
|     } | ||||
|     .fee-text{ | ||||
|       border-bottom: 1px solid #ffffff1c; | ||||
|       color: #ffffff; | ||||
|       width: fit-content; | ||||
|       margin: auto; | ||||
|       line-height: 1.45; | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| 
 | ||||
| <div class="container-xl dashboard-container"> | ||||
| <div class="container-xl dashboard-container" *ngIf="(network$ | async) !== 'liquid'; else liquidDashboard"> | ||||
|   <div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData"> | ||||
|     <ng-container *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'"> | ||||
|     <ng-container *ngIf="(network$ | async) !== 'liquidtestnet'"> | ||||
|       <div class="col card-wrapper"> | ||||
|         <div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div> | ||||
|         <div class="card"> | ||||
| @ -17,35 +17,26 @@ | ||||
|     <div class="col"> | ||||
|       <div class="card graph-card"> | ||||
|         <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2"> | ||||
|           <ng-template [ngIf]="(network$ | async) !== 'liquid'" [ngIfElse]="liquidPegs"> | ||||
|             <a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]"> | ||||
|               <h5 class="card-title d-inline"><span i18n="dashboard.mempool-goggles">Mempool Goggles</span>: {{ goggleCycle[goggleIndex].name }}</h5> | ||||
|               <span> </span> | ||||
|               <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon> | ||||
|             </a> | ||||
|             <div class="quick-filter"> | ||||
|               <div class="btn-group btn-group-toggle"> | ||||
|                 <label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex"  *ngFor="let filter of goggleCycle"> | ||||
|                   <input type="radio" [value]="'3m'" fragment="3m" (click)="setFilter(filter.index)" [attr.data-cy]="'3m'"> {{ filter.name }} | ||||
|                 </label> | ||||
|               </div> | ||||
|           <a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]"> | ||||
|             <h5 class="card-title d-inline"><span i18n="dashboard.mempool-goggles">Mempool Goggles</span>: {{ goggleCycle[goggleIndex].name }}</h5> | ||||
|             <span> </span> | ||||
|             <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon> | ||||
|           </a> | ||||
|           <div class="quick-filter"> | ||||
|             <div class="btn-group btn-group-toggle"> | ||||
|               <label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex"  *ngFor="let filter of goggleCycle"> | ||||
|                 <input type="radio" [value]="'3m'" fragment="3m" (click)="setFilter(filter.index)" [attr.data-cy]="'3m'"> {{ filter.name }} | ||||
|               </label> | ||||
|             </div> | ||||
|             <div class="mempool-block-wrapper"> | ||||
|               <app-mempool-block-overview | ||||
|                 [index]="0" | ||||
|                 [resolution]="goggleResolution" | ||||
|                 [filterFlags]="goggleFlags" | ||||
|                 [filterMode]="goggleMode" | ||||
|               ></app-mempool-block-overview> | ||||
|             </div> | ||||
|           </ng-template> | ||||
|           <ng-template #liquidPegs> | ||||
|             <div style="padding-left: 1.25rem;"> | ||||
|               <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container> | ||||
|               <hr> | ||||
|             </div> | ||||
|             <app-lbtc-pegs-graph [data]="fullHistory$ | async" [height]="lbtcPegGraphHeight"></app-lbtc-pegs-graph> | ||||
|           </ng-template> | ||||
|           </div> | ||||
|           <div class="mempool-block-wrapper"> | ||||
|             <app-mempool-block-overview | ||||
|               [index]="0" | ||||
|               [resolution]="goggleResolution" | ||||
|               [filterFlags]="goggleFlags" | ||||
|               [filterMode]="goggleMode" | ||||
|             ></app-mempool-block-overview> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| @ -53,7 +44,6 @@ | ||||
|       <div class="card graph-card"> | ||||
|         <div class="card-body"> | ||||
|           <ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container> | ||||
|           <ng-container *ngIf="stateService.network !== 'liquid'"> | ||||
|             <h5 class="card-title mt-3" i18n="dashboard.incoming-transactions">Incoming Transactions</h5> | ||||
|             <div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats"> | ||||
|               <app-incoming-transactions-graph | ||||
| @ -64,30 +54,11 @@ | ||||
|                 [windowPreferenceOverride]="'2h'" | ||||
|                 ></app-incoming-transactions-graph> | ||||
|             </div> | ||||
|           </ng-container> | ||||
|           <div class="mempool-graph" *ngIf="stateService.network === 'liquid'"> | ||||
|             <hr> | ||||
|             <table class="table table-borderless table-striped" *ngIf="(featuredAssets$ | async) as featuredAssets else loadingAssetsTable"> | ||||
|               <tbody> | ||||
|                 <tr *ngFor="let group of featuredAssets"> | ||||
|                   <td class="asset-icon"> | ||||
|                     <a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]"> | ||||
|                       <img class="assetIcon" [src]="'/api/v1/asset/' + group.asset + '/icon'"> | ||||
|                     </a> | ||||
|                   </td> | ||||
|                   <td class="asset-title"> | ||||
|                     <a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">{{ group.name }}</a> | ||||
|                   </td> | ||||
|                   <td class="circulating-amount"><app-asset-circulation [assetId]="group.asset"></app-asset-circulation></td> | ||||
|                 </tr> | ||||
|               </tbody> | ||||
|             </table> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="col" style="max-height: 410px"> | ||||
|       <div class="card" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else latestBlocks"> | ||||
|       <div class="card" *ngIf="(network$ | async) !== 'liquidtestnet'; else latestBlocks"> | ||||
|         <div class="card-body"> | ||||
|           <a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]"> | ||||
|             <h5 class="card-title d-inline" i18n="dashboard.recent-rbf-replacements">Recent Replacements</h5> | ||||
| @ -171,7 +142,7 @@ | ||||
|                     <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-satoshis"><app-amount *ngIf="(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"><app-fee-rate [fee]="transaction.fee" [weight]="transaction.vsize * 4"></app-fee-rate></td> | ||||
|               </tr> | ||||
| @ -184,25 +155,50 @@ | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #loadingAssetsTable> | ||||
|   <table class="table table-borderless table-striped asset-table"> | ||||
|     <tbody> | ||||
|       <tr *ngFor="let i of getArrayFromNumber(this.nbFeaturedAssets)"> | ||||
|         <td class="asset-icon"> | ||||
|           <div class="skeleton-loader skeleton-loader-transactions"></div> | ||||
|         </td> | ||||
|         <td class="asset-title"> | ||||
|           <div class="skeleton-loader skeleton-loader-transactions"></div> | ||||
|         </td> | ||||
|         <td class="asset-title d-none d-md-table-cell"> | ||||
|           <div class="skeleton-loader skeleton-loader-transactions"></div> | ||||
|         </td> | ||||
|         <td class="asset-title"> | ||||
|           <div class="skeleton-loader skeleton-loader-transactions"></div> | ||||
|         </td> | ||||
|       </tr> | ||||
|     </tbody> | ||||
|   </table> | ||||
| <ng-template #liquidDashboard> | ||||
|   <div class="container-xl dashboard-container" *ngIf="(auditStatus$ | async)?.isAuditSynced; else auditInProgress"> | ||||
| 
 | ||||
|     <div class="row row-cols-1 row-cols-md-2"> | ||||
|    | ||||
|       <div class="col"> | ||||
|         <div class="card-liquid card"> | ||||
|           <div class="card-title card-title-liquid"> | ||||
|             <app-reserves-supply-stats [currentPeg$]="currentPeg$" [currentReserves$]="currentReserves$"></app-reserves-supply-stats> | ||||
|           </div> | ||||
|           <div class="card-body pl-0" style="padding-top: 10px;"> | ||||
|             <app-lbtc-pegs-graph [data]="fullHistory$ | async" [height]="lbtcPegGraphHeight"></app-lbtc-pegs-graph> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|    | ||||
|       <div class="col" style="margin-bottom: 1.47rem"> | ||||
|         <div class="card-liquid card">  | ||||
|           <div class="card-body"> | ||||
|             <app-reserves-ratio-stats [fullHistory$]="fullHistory$"></app-reserves-ratio-stats> | ||||
|             <app-reserves-ratio [currentPeg]="currentPeg$ | async" [currentReserves]="currentReserves$ | async"></app-reserves-ratio> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="col"> | ||||
|         <div class="card card-liquid smaller"> | ||||
|           <div class="card-body"> | ||||
|             <app-recent-pegs-stats [pegsVolume$]="pegsVolume$"></app-recent-pegs-stats> | ||||
|             <app-recent-pegs-list [recentPegsList$]="recentPegsList$" [widget]="true"></app-recent-pegs-list> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="col" style="margin-bottom: 1.47rem"> | ||||
|         <div class="card-liquid card smaller"> | ||||
|           <div class="card-body"> | ||||
|             <app-federation-addresses-stats [federationAddressesNumber$]="federationAddressesNumber$" [federationUtxosNumber$]="federationUtxosNumber$"></app-federation-addresses-stats> | ||||
|             <app-federation-addresses-list [federationAddresses$]="federationAddresses$" [widget]="true"></app-federation-addresses-list> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #replacementsSkeleton> | ||||
| @ -283,21 +279,56 @@ | ||||
|   </div> | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #lbtcPegs let-mempoolInfoData> | ||||
|   <div class="mempool-info-data"> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5> | ||||
|       <ng-container *ngIf="(currentPeg$ | async) as currentPeg; else loadingTransactions"> | ||||
|         <p i18n-ngbTooltip="liquid.last-elements-audit-block" [ngbTooltip]="'L-BTC supply last updated at Liquid block ' + (currentPeg.lastBlockUpdate)" placement="top" class="card-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></p> | ||||
|       </ng-container> | ||||
|     </div> | ||||
|     <div class="item"> | ||||
|       <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> | ||||
|       </a> | ||||
|       <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> | ||||
|       </ng-container> | ||||
| <ng-template #loadingSkeletonLiquid> | ||||
|   <div class="container-xl dashboard-container"> | ||||
| 
 | ||||
|     <div class="row row-cols-1 row-cols-md-2"> | ||||
|    | ||||
|       <div class="col"> | ||||
|         <div class="card-liquid card"> | ||||
|           <div class="card-title card-title-liquid"> | ||||
|             <app-reserves-supply-stats></app-reserves-supply-stats> | ||||
|           </div> | ||||
|           <div class="card-body pl-0" style="padding-top: 10px;"> | ||||
|             <app-lbtc-pegs-graph [height]="lbtcPegGraphHeight"></app-lbtc-pegs-graph> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|    | ||||
|       <div class="col" style="margin-bottom: 1.47rem"> | ||||
|         <div class="card-liquid card">  | ||||
|           <div class="card-body"> | ||||
|             <app-reserves-ratio-stats></app-reserves-ratio-stats> | ||||
|             <app-reserves-ratio></app-reserves-ratio> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="col"> | ||||
|         <div class="card card-liquid smaller"> | ||||
|           <div class="card-body"> | ||||
|             <app-recent-pegs-stats></app-recent-pegs-stats> | ||||
|             <app-recent-pegs-list [widget]="true"></app-recent-pegs-list> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="col" style="margin-bottom: 1.47rem"> | ||||
|         <div class="card-liquid card smaller"> | ||||
|           <div class="card-body"> | ||||
|             <app-federation-addresses-stats></app-federation-addresses-stats> | ||||
|             <app-federation-addresses-list [widget]="true"></app-federation-addresses-list> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #auditInProgress> | ||||
|   <ng-container *ngIf="(auditStatus$ | async) as auditStatus; else loadingSkeletonLiquid"> | ||||
|     <div class="in-progress-message" *ngIf="auditStatus.lastBlockAudit && auditStatus.bitcoinHeaders; else loadingSkeletonLiquid"> | ||||
|       <span i18n="liquid.audit-in-progress">Audit in progress: Bitcoin block height #{{ auditStatus.lastBlockAudit }} / #{{ auditStatus.bitcoinHeaders }}</span> | ||||
|     </div> | ||||
|   </ng-container> | ||||
| </ng-template> | ||||
| @ -59,6 +59,10 @@ | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|   } | ||||
|   &.lbtc-pegs-stats {  | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|   } | ||||
|   h5 { | ||||
|     margin-bottom: 10px; | ||||
|   } | ||||
| @ -409,3 +413,27 @@ | ||||
|   margin-top: 5px; | ||||
|   margin-bottom: 6px; | ||||
| } | ||||
| 
 | ||||
| .card-liquid { | ||||
|   background-color: #1d1f31; | ||||
|   height: 418px; | ||||
|   @media (min-width: 992px) { | ||||
|     height: 512px; | ||||
|   } | ||||
|   &.smaller { | ||||
|     height: 408px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .card-title-liquid { | ||||
|   padding-top: 20px; | ||||
| } | ||||
| 
 | ||||
| .in-progress-message { | ||||
|   position: relative; | ||||
|   color: #ffffff91; | ||||
|   margin-top: 20px; | ||||
|   text-align: center; | ||||
|   padding-bottom: 3px; | ||||
|   font-weight: 500; | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { combineLatest, EMPTY, fromEvent, merge, Observable, of, Subject, Subscription, timer } from 'rxjs'; | ||||
| import { combineLatest, EMPTY, fromEvent, interval, merge, Observable, of, Subject, Subscription, timer } from 'rxjs'; | ||||
| import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; | ||||
| import { AuditStatus, BlockExtended, CurrentPegs, OptimizedMempoolStats } from '../interfaces/node-api.interface'; | ||||
| import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, OptimizedMempoolStats, PegsVolume, RecentPeg } from '../interfaces/node-api.interface'; | ||||
| import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface'; | ||||
| import { ApiService } from '../services/api.service'; | ||||
| import { StateService } from '../services/state.service'; | ||||
| @ -33,8 +33,6 @@ interface MempoolStatsData { | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush | ||||
| }) | ||||
| export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|   featuredAssets$: Observable<any>; | ||||
|   nbFeaturedAssets = 6; | ||||
|   network$: Observable<string>; | ||||
|   mempoolBlocksData$: Observable<MempoolBlocksData>; | ||||
|   mempoolInfoData$: Observable<MempoolInfoData>; | ||||
| @ -54,6 +52,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|   auditUpdated$: Observable<boolean>; | ||||
|   liquidReservesMonth$: Observable<any>; | ||||
|   currentReserves$: Observable<CurrentPegs>; | ||||
|   recentPegsList$: Observable<RecentPeg[]>; | ||||
|   pegsVolume$: Observable<PegsVolume[]>; | ||||
|   federationAddresses$: Observable<FederationAddress[]>; | ||||
|   federationAddressesNumber$: Observable<number>; | ||||
|   federationUtxosNumber$: Observable<number>; | ||||
|   fullHistory$: Observable<any>; | ||||
|   isLoad: boolean = true; | ||||
|   filterSubscription: Subscription; | ||||
| @ -184,26 +187,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|         }) | ||||
|       ); | ||||
| 
 | ||||
|     const windowResize$ = fromEvent(window, 'resize').pipe( | ||||
|       distinctUntilChanged(), | ||||
|       startWith(null) | ||||
|     ); | ||||
|    | ||||
|     this.featuredAssets$ = combineLatest([ | ||||
|       this.apiService.listFeaturedAssets$(), | ||||
|       windowResize$ | ||||
|     ]).pipe( | ||||
|         map(([featured, _]) => { | ||||
|           const newArray = []; | ||||
|           for (const feature of featured) { | ||||
|             if (feature.ticker !== 'L-BTC' && feature.asset) { | ||||
|               newArray.push(feature); | ||||
|             } | ||||
|           } | ||||
|           return newArray.slice(0, this.nbFeaturedAssets); | ||||
|         }), | ||||
|       ); | ||||
| 
 | ||||
|     this.transactions$ = this.stateService.transactions$ | ||||
|       .pipe( | ||||
|         scan((acc, tx) => { | ||||
| @ -269,7 +252,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|         share(), | ||||
|       ); | ||||
| 
 | ||||
|     if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') { | ||||
|     if (this.stateService.network === 'liquid') { | ||||
|       this.auditStatus$ = this.stateService.blocks$.pipe( | ||||
|         takeUntil(this.destroy$), | ||||
|         throttleTime(40000), | ||||
| @ -279,22 +262,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|         shareReplay(1) | ||||
|       ); | ||||
| 
 | ||||
|       ////////// Pegs historical data //////////
 | ||||
|       this.liquidPegsMonth$ = this.auditStatus$.pipe( | ||||
|         throttleTime(60 * 60 * 1000), | ||||
|         switchMap(() => this.apiService.listLiquidPegsMonth$()), | ||||
|         map((pegs) => { | ||||
|           const labels = pegs.map(stats => stats.date); | ||||
|           const series = pegs.map(stats => parseFloat(stats.amount) / 100000000); | ||||
|           series.reduce((prev, curr, i) => series[i] = prev + curr, 0); | ||||
|           return { | ||||
|             series, | ||||
|             labels | ||||
|           }; | ||||
|         }), | ||||
|         share(), | ||||
|       ); | ||||
| 
 | ||||
|       this.currentPeg$ = this.auditStatus$.pipe( | ||||
|         switchMap(_ => | ||||
|           this.apiService.liquidPegs$().pipe( | ||||
| @ -307,7 +274,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|         share() | ||||
|       ); | ||||
| 
 | ||||
|       ////////// BTC Reserves historical data //////////
 | ||||
|       this.auditUpdated$ = combineLatest([ | ||||
|         this.auditStatus$, | ||||
|         this.currentPeg$ | ||||
| @ -322,21 +288,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|           const amountCheck = currentPegAmount !== this.lastPegAmount; | ||||
|           this.lastPegAmount = currentPegAmount; | ||||
|           return of(blockAuditCheck || amountCheck); | ||||
|         }) | ||||
|       ); | ||||
| 
 | ||||
|       this.liquidReservesMonth$ = this.auditStatus$.pipe( | ||||
|         throttleTime(60 * 60 * 1000), | ||||
|         switchMap((auditStatus) => { | ||||
|           return auditStatus.isAuditSynced ? this.apiService.listLiquidReservesMonth$() : EMPTY; | ||||
|         }), | ||||
|         map(reserves => { | ||||
|           const labels = reserves.map(stats => stats.date); | ||||
|           const series = reserves.map(stats => parseFloat(stats.amount) / 100000000); | ||||
|           return { | ||||
|             series, | ||||
|             labels | ||||
|           }; | ||||
|         }), | ||||
|         share() | ||||
|       ); | ||||
| @ -355,11 +306,78 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|         share() | ||||
|       ); | ||||
| 
 | ||||
|       this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$.pipe(startWith(null)), this.currentReserves$.pipe(startWith(null))]) | ||||
|       this.recentPegsList$ = this.auditUpdated$.pipe( | ||||
|         filter(auditUpdated => auditUpdated === true), | ||||
|         throttleTime(40000), | ||||
|         switchMap(_ => this.apiService.recentPegsList$()), | ||||
|         share() | ||||
|       ); | ||||
|    | ||||
|       this.pegsVolume$ = this.auditUpdated$.pipe( | ||||
|         filter(auditUpdated => auditUpdated === true), | ||||
|         throttleTime(40000), | ||||
|         switchMap(_ => this.apiService.pegsVolume$()), | ||||
|         share() | ||||
|       ); | ||||
|    | ||||
|       this.federationAddresses$ = this.auditUpdated$.pipe( | ||||
|         filter(auditUpdated => auditUpdated === true), | ||||
|         throttleTime(40000), | ||||
|         switchMap(_ => this.apiService.federationAddresses$()), | ||||
|         share() | ||||
|       ); | ||||
|    | ||||
|       this.federationAddressesNumber$ = this.auditUpdated$.pipe( | ||||
|         filter(auditUpdated => auditUpdated === true), | ||||
|         throttleTime(40000), | ||||
|         switchMap(_ => this.apiService.federationAddressesNumber$()), | ||||
|         map(count => count.address_count), | ||||
|         share() | ||||
|       ); | ||||
|    | ||||
|       this.federationUtxosNumber$ = this.auditUpdated$.pipe( | ||||
|         filter(auditUpdated => auditUpdated === true), | ||||
|         throttleTime(40000), | ||||
|         switchMap(_ => this.apiService.federationUtxosNumber$()), | ||||
|         map(count => count.utxo_count), | ||||
|         share() | ||||
|       ); | ||||
|    | ||||
|       this.liquidPegsMonth$ = interval(60 * 60 * 1000) | ||||
|         .pipe( | ||||
|           startWith(0), | ||||
|           switchMap(() => this.apiService.listLiquidPegsMonth$()), | ||||
|           map((pegs) => { | ||||
|             const labels = pegs.map(stats => stats.date); | ||||
|             const series = pegs.map(stats => parseFloat(stats.amount) / 100000000); | ||||
|             series.reduce((prev, curr, i) => series[i] = prev + curr, 0); | ||||
|             return { | ||||
|               series, | ||||
|               labels | ||||
|             }; | ||||
|           }), | ||||
|           share(), | ||||
|         ); | ||||
|    | ||||
|       this.liquidReservesMonth$ = interval(60 * 60 * 1000).pipe( | ||||
|         startWith(0), | ||||
|         switchMap(() => this.apiService.listLiquidReservesMonth$()), | ||||
|         map(reserves => { | ||||
|           const labels = reserves.map(stats => stats.date); | ||||
|           const series = reserves.map(stats => parseFloat(stats.amount) / 100000000); | ||||
|           return { | ||||
|             series, | ||||
|             labels | ||||
|           }; | ||||
|         }), | ||||
|         share() | ||||
|       ); | ||||
|    | ||||
|       this.fullHistory$ = combineLatest([this.liquidPegsMonth$, this.currentPeg$, this.liquidReservesMonth$, this.currentReserves$]) | ||||
|         .pipe( | ||||
|           map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => { | ||||
|             liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000; | ||||
| 
 | ||||
|    | ||||
|             if (liquidPegs.series.length === liquidReserves?.series.length) { | ||||
|               liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000; | ||||
|             } else if (liquidPegs.series.length === liquidReserves?.series.length + 1) { | ||||
| @ -371,7 +389,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|                 labels: [] | ||||
|               }; | ||||
|             } | ||||
| 
 | ||||
|    | ||||
|             return { | ||||
|               liquidPegs, | ||||
|               liquidReserves | ||||
| @ -415,17 +433,14 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | ||||
|       this.incomingGraphHeight = 300; | ||||
|       this.goggleResolution = 82; | ||||
|       this.lbtcPegGraphHeight = 320; | ||||
|       this.nbFeaturedAssets = 6; | ||||
|     } else if (window.innerWidth >= 768) { | ||||
|       this.incomingGraphHeight = 215; | ||||
|       this.goggleResolution = 80; | ||||
|       this.lbtcPegGraphHeight = 230; | ||||
|       this.nbFeaturedAssets = 4; | ||||
|     } else { | ||||
|       this.incomingGraphHeight = 180; | ||||
|       this.goggleResolution = 86; | ||||
|       this.lbtcPegGraphHeight = 220; | ||||
|       this.nbFeaturedAssets = 4; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -12,6 +12,13 @@ import { FeeDistributionGraphComponent } from '../components/fee-distribution-gr | ||||
| import { IncomingTransactionsGraphComponent } from '../components/incoming-transactions-graph/incoming-transactions-graph.component'; | ||||
| import { MempoolGraphComponent } from '../components/mempool-graph/mempool-graph.component'; | ||||
| import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs-graph.component'; | ||||
| import { ReservesSupplyStatsComponent } from '../components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component'; | ||||
| import { ReservesRatioStatsComponent } from '../components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component'; | ||||
| import { ReservesRatioComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component'; | ||||
| import { RecentPegsStatsComponent } from '../components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component'; | ||||
| import { RecentPegsListComponent } from '../components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component'; | ||||
| import { FederationAddressesStatsComponent } from '../components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component'; | ||||
| import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component'; | ||||
| import { GraphsComponent } from '../components/graphs/graphs.component'; | ||||
| import { StatisticsComponent } from '../components/statistics/statistics.component'; | ||||
| import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component'; | ||||
| @ -48,6 +55,13 @@ import { CommonModule } from '@angular/common'; | ||||
|     IncomingTransactionsGraphComponent, | ||||
|     MempoolGraphComponent, | ||||
|     LbtcPegsGraphComponent, | ||||
|     ReservesSupplyStatsComponent, | ||||
|     ReservesRatioStatsComponent, | ||||
|     ReservesRatioComponent, | ||||
|     RecentPegsStatsComponent, | ||||
|     RecentPegsListComponent, | ||||
|     FederationAddressesStatsComponent, | ||||
|     FederationAddressesListComponent, | ||||
|     HashrateChartComponent, | ||||
|     HashrateChartPoolsComponent, | ||||
|     BlockHealthGraphComponent, | ||||
|  | ||||
| @ -15,17 +15,10 @@ import { AssetsComponent } from '../components/assets/assets.component'; | ||||
| import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component' | ||||
| import { AssetComponent } from '../components/asset/asset.component'; | ||||
| import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component'; | ||||
| import { ReservesAuditDashboardComponent } from '../components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component'; | ||||
| import { ReservesSupplyStatsComponent } from '../components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component'; | ||||
| import { RecentPegsStatsComponent } from '../components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component'; | ||||
| import { RecentPegsListComponent } from '../components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component'; | ||||
| import { FederationWalletComponent } from '../components/liquid-reserves-audit/federation-wallet/federation-wallet.component'; | ||||
| import { FederationUtxosListComponent } from '../components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component'; | ||||
| import { FederationAddressesStatsComponent } from '../components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component'; | ||||
| import { FederationAddressesListComponent } from '../components/liquid-reserves-audit/federation-addresses-list/federation-addresses-list.component'; | ||||
| import { ReservesRatioComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component'; | ||||
| import { ReservesRatioStatsComponent } from '../components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component'; | ||||
| import { ReservesRatioGraphComponent } from '../components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|   { | ||||
| @ -77,18 +70,6 @@ const routes: Routes = [ | ||||
|         data: { preload: true, networkSpecific: true }, | ||||
|         loadChildren: () => import('../components/block/block.module').then(m => m.BlockModule), | ||||
|       }, | ||||
|       { | ||||
|         path: 'audit', | ||||
|         data: { networks: ['liquid'] }, | ||||
|         component: StartComponent, | ||||
|         children: [ | ||||
|           { | ||||
|             path: '', | ||||
|             data: { networks: ['liquid'] }, | ||||
|             component: ReservesAuditDashboardComponent, | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         path: 'audit/wallet', | ||||
|         data: { networks: ['liquid'] }, | ||||
| @ -180,17 +161,8 @@ export class LiquidRoutingModule { } | ||||
|   ], | ||||
|   declarations: [ | ||||
|     LiquidMasterPageComponent, | ||||
|     ReservesAuditDashboardComponent, | ||||
|     ReservesSupplyStatsComponent, | ||||
|     RecentPegsStatsComponent, | ||||
|     RecentPegsListComponent, | ||||
|     FederationWalletComponent, | ||||
|     FederationUtxosListComponent, | ||||
|     FederationAddressesStatsComponent, | ||||
|     FederationAddressesListComponent, | ||||
|     ReservesRatioComponent, | ||||
|     ReservesRatioStatsComponent, | ||||
|     ReservesRatioGraphComponent, | ||||
|   ] | ||||
| }) | ||||
| export class LiquidMasterPageModule { } | ||||
| @ -200,16 +200,20 @@ export class ApiService { | ||||
|     return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos'); | ||||
|   } | ||||
| 
 | ||||
|   recentPegOuts$(): Observable<RecentPeg[]> { | ||||
|     return this.httpClient.get<RecentPeg[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegouts'); | ||||
|   recentPegsList$(count: number = 0): Observable<RecentPeg[]> { | ||||
|     return this.httpClient.get<RecentPeg[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/list/' + count); | ||||
|   } | ||||
| 
 | ||||
|   federationAddressesOneMonthAgo$(): Observable<any> { | ||||
|     return this.httpClient.get<FederationAddress[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses/previous-month'); | ||||
|   pegsCount$(): Observable<any> { | ||||
|     return this.httpClient.get<number>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/count'); | ||||
|   } | ||||
| 
 | ||||
|   federationUtxosOneMonthAgo$(): Observable<any> { | ||||
|     return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos/previous-month'); | ||||
|   federationAddressesNumber$(): Observable<any> { | ||||
|     return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses/total'); | ||||
|   } | ||||
| 
 | ||||
|   federationUtxosNumber$(): Observable<any> { | ||||
|     return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos/total'); | ||||
|   } | ||||
| 
 | ||||
|   listFeaturedAssets$(): Observable<any[]> { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user