Merge branch 'master' into mononaut/sliding-difficulty
This commit is contained in:
commit
5f14b32a06
@ -398,39 +398,24 @@ class ElementsParser {
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all of the federation addresses one month ago, most balances first
|
// Get the total number of federation addresses
|
||||||
public async $getFederationAddressesOneMonthAgo(): Promise<any> {
|
public async $getFederationAddressesNumber(): Promise<any> {
|
||||||
const query = `
|
const query = `SELECT COUNT(DISTINCT bitcoinaddress) AS address_count FROM federation_txos WHERE unspent = 1;`;
|
||||||
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;`;
|
|
||||||
const [rows] = await DB.query(query);
|
const [rows] = await DB.query(query);
|
||||||
return rows[0];
|
return rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all of the UTXOs held by the federation one month ago, most recent first
|
// Get the total number of federation utxos
|
||||||
public async $getFederationUtxosOneMonthAgo(): Promise<any> {
|
public async $getFederationUtxosNumber(): Promise<any> {
|
||||||
const query = `
|
const query = `SELECT COUNT(*) AS utxo_count FROM federation_txos WHERE unspent = 1;`;
|
||||||
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;`;
|
|
||||||
const [rows] = await DB.query(query);
|
const [rows] = await DB.query(query);
|
||||||
return rows[0];
|
return rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get recent pegouts from the federation (3 months old)
|
// Get recent pegs in / out
|
||||||
public async $getRecentPegouts(): Promise<any> {
|
public async $getPegsList(count: number = 0): 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 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);
|
const [rows] = await DB.query(query, [count]);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,6 +428,12 @@ class ElementsParser {
|
|||||||
pegOutQuery[0][0]
|
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();
|
export default new ElementsParser();
|
||||||
|
@ -17,14 +17,15 @@ class LiquidRoutes {
|
|||||||
app
|
app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs', this.$getElementsPegs)
|
.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/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/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', this.$getFederationReserves)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/month', this.$getFederationReservesByMonth)
|
.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', 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', 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)
|
.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 {
|
try {
|
||||||
const federationAddresses = await elementsParser.$getFederationAddressesOneMonthAgo();
|
const federationAddresses = await elementsParser.$getFederationAddressesNumber();
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
res.header('Cache-control', '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);
|
res.json(federationAddresses);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : 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 {
|
try {
|
||||||
const federationUtxos = await elementsParser.$getFederationUtxosOneMonthAgo();
|
const federationUtxos = await elementsParser.$getFederationUtxosNumber();
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
res.header('Cache-control', '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);
|
res.json(federationUtxos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : 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 {
|
try {
|
||||||
const recentPegOuts = await elementsParser.$getRecentPegouts();
|
const recentPegs = await elementsParser.$getPegsList(parseInt(req.params?.count));
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
|
||||||
res.json(recentPegOuts);
|
res.json(recentPegs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : 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();
|
export default new LiquidRoutes();
|
||||||
|
@ -285,7 +285,7 @@ class StatisticsApi {
|
|||||||
|
|
||||||
public async $list2H(): Promise<OptimizedStatistic[]> {
|
public async $list2H(): Promise<OptimizedStatistic[]> {
|
||||||
try {
|
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 });
|
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
|
||||||
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
|
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -296,7 +296,7 @@ class StatisticsApi {
|
|||||||
|
|
||||||
public async $list24H(): Promise<OptimizedStatistic[]> {
|
public async $list24H(): Promise<OptimizedStatistic[]> {
|
||||||
try {
|
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 });
|
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
|
||||||
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
|
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -6,6 +6,7 @@ import statisticsApi from './statistics-api';
|
|||||||
|
|
||||||
class Statistics {
|
class Statistics {
|
||||||
protected intervalTimer: NodeJS.Timer | undefined;
|
protected intervalTimer: NodeJS.Timer | undefined;
|
||||||
|
protected lastRun: number = 0;
|
||||||
protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined;
|
protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined;
|
||||||
|
|
||||||
public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) {
|
public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) {
|
||||||
@ -23,15 +24,21 @@ class Statistics {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.runStatistics();
|
this.runStatistics();
|
||||||
this.intervalTimer = setInterval(() => {
|
this.intervalTimer = setInterval(() => {
|
||||||
this.runStatistics();
|
this.runStatistics(true);
|
||||||
}, 1 * 60 * 1000);
|
}, 1 * 60 * 1000);
|
||||||
}, difference);
|
}, difference);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runStatistics(): Promise<void> {
|
public async runStatistics(skipIfRecent = false): Promise<void> {
|
||||||
if (!memPool.isInSync()) {
|
if (!memPool.isInSync()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (skipIfRecent && new Date().getTime() / 1000 - this.lastRun < 30) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastRun = new Date().getTime() / 1000;
|
||||||
const currentMempool = memPool.getMempool();
|
const currentMempool = memPool.getMempool();
|
||||||
const txPerSecond = memPool.getTxPerSecond();
|
const txPerSecond = memPool.getTxPerSecond();
|
||||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||||
|
@ -23,6 +23,7 @@ import priceUpdater from '../tasks/price-updater';
|
|||||||
import { ApiPrice } from '../repositories/PricesRepository';
|
import { ApiPrice } from '../repositories/PricesRepository';
|
||||||
import accelerationApi from './services/acceleration';
|
import accelerationApi from './services/acceleration';
|
||||||
import mempool from './mempool';
|
import mempool from './mempool';
|
||||||
|
import statistics from './statistics/statistics';
|
||||||
|
|
||||||
interface AddressTransactions {
|
interface AddressTransactions {
|
||||||
mempool: MempoolTransactionExtended[],
|
mempool: MempoolTransactionExtended[],
|
||||||
@ -723,6 +724,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.printLogs();
|
this.printLogs();
|
||||||
|
await statistics.runStatistics();
|
||||||
|
|
||||||
const _memPool = memPool.getMempool();
|
const _memPool = memPool.getMempool();
|
||||||
|
|
||||||
@ -1014,6 +1016,8 @@ class WebsocketHandler {
|
|||||||
client.send(this.serializeResponse(response));
|
client.send(this.serializeResponse(response));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await statistics.runStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
// takes a dictionary of JSON serialized values
|
// 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">
|
<div class="become-sponsor community">
|
||||||
<p style="font-weight: 700; font-size: 18px;">If you're an individual...</p>
|
<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>
|
<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> Co-branded instance</p>
|
||||||
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p>
|
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,11 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin: 68px auto;
|
margin: 68px auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#become-sponsor-container.account {
|
||||||
|
margin: 20px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.become-sponsor {
|
.become-sponsor {
|
||||||
|
@ -8,6 +8,7 @@ import { EnterpriseService } from '../../services/enterprise.service';
|
|||||||
})
|
})
|
||||||
export class AboutSponsorsComponent {
|
export class AboutSponsorsComponent {
|
||||||
@Input() host = 'https://mempool.space';
|
@Input() host = 'https://mempool.space';
|
||||||
|
@Input() context = 'about';
|
||||||
|
|
||||||
constructor(private enterpriseService: EnterpriseService) {
|
constructor(private enterpriseService: EnterpriseService) {
|
||||||
}
|
}
|
||||||
|
@ -78,10 +78,7 @@
|
|||||||
<li class="nav-item" routerLinkActive="active" id="btn-assets">
|
<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>
|
<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>
|
||||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-audit">
|
<li class="nav-item mr-2" routerLinkActive="active" id="btn-docs">
|
||||||
<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">
|
|
||||||
<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>
|
<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>
|
||||||
<li class="nav-item" routerLinkActive="active" id="btn-about">
|
<li class="nav-item" routerLinkActive="active" id="btn-about">
|
||||||
|
@ -23,6 +23,11 @@ li.nav-item {
|
|||||||
margin: auto 10px;
|
margin: auto 10px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
@media (max-width: 429px) {
|
||||||
|
margin: auto 5px;
|
||||||
|
padding-left: 6px;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
|
@ -6,11 +6,14 @@
|
|||||||
|
|
||||||
tr, td, th {
|
tr, td, th {
|
||||||
border: 0px;
|
border: 0px;
|
||||||
padding-top: 0.65rem !important;
|
padding-top: 0.65rem;
|
||||||
padding-bottom: 0.6rem !important;
|
padding-bottom: 0.6rem;
|
||||||
padding-right: 2rem !important;
|
padding-right: 2rem;
|
||||||
.widget {
|
.widget &.widget {
|
||||||
padding-right: 1rem !important;
|
padding-right: 1rem;
|
||||||
|
@media (max-width: 510px) {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
<div *ngIf="(federationAddresses$ | async) as federationAddresses; else loadingData">
|
<div *ngIf="(federationWalletStats$ | async) as federationWalletStats; else loadingData">
|
||||||
|
<div class="fee-estimation-container">
|
||||||
<div class="fee-estimation-container">
|
<div class="item">
|
||||||
<div class="item">
|
<a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]">
|
||||||
<a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]">
|
<h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||||
<h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
</a>
|
||||||
</a>
|
<div class="card-text">
|
||||||
<div class="card-text">
|
<div class="fee-text">{{ federationWalletStats.address_count }} <span i18n="shared.addresses">addresses</span></div>
|
||||||
<div class="fee-text">{{ federationAddresses.length }} <span i18n="shared.addresses">addresses</span></div>
|
<div class="fiat">{{ federationWalletStats.utxo_count }} <span i18n="shared.utxos">UTXOs</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #loadingData>
|
<ng-template #loadingData>
|
||||||
@ -30,5 +27,5 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #loadingSkeleton>
|
<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>
|
</ng-template>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
.fee-estimation-container {
|
.fee-estimation-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
@media (min-width: 376px) {
|
@media (min-width: 376px) {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
@ -21,6 +22,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-text {
|
.card-text {
|
||||||
|
padding-top: 9px;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
span {
|
span {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@ -48,10 +50,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-container{
|
|
||||||
min-height: 76px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-text {
|
.card-text {
|
||||||
.skeleton-loader {
|
.skeleton-loader {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, combineLatest, map, of } from 'rxjs';
|
||||||
import { FederationAddress } from '../../../interfaces/node-api.interface';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-federation-addresses-stats',
|
selector: 'app-federation-addresses-stats',
|
||||||
@ -9,12 +8,24 @@ import { FederationAddress } from '../../../interfaces/node-api.interface';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class FederationAddressesStatsComponent implements OnInit {
|
export class FederationAddressesStatsComponent implements OnInit {
|
||||||
@Input() federationAddresses$: Observable<FederationAddress[]>;
|
@Input() federationAddressesNumber$: Observable<number>;
|
||||||
@Input() federationAddressesOneMonthAgo$: Observable<any>;
|
@Input() federationUtxosNumber$: Observable<number>;
|
||||||
|
federationWalletStats$: Observable<any>;
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
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]="{'container-xl': !widget, 'widget': widget}">
|
||||||
<div [ngClass]="{'widget': widget}">
|
|
||||||
|
|
||||||
<div *ngIf="!widget">
|
<div *ngIf="!widget">
|
||||||
<h1 i18n="liquid.recent-pegs">Recent Peg-In / Out's</h1>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
@ -6,11 +6,14 @@
|
|||||||
|
|
||||||
tr, td, th {
|
tr, td, th {
|
||||||
border: 0px;
|
border: 0px;
|
||||||
padding-top: 0.65rem !important;
|
padding-top: 0.65rem;
|
||||||
padding-bottom: 0.6rem !important;
|
padding-bottom: 0.6rem;
|
||||||
padding-right: 2rem !important;
|
padding-right: 2rem;
|
||||||
.widget {
|
.widget &.widget {
|
||||||
padding-right: 1rem !important;
|
padding-right: 1rem;
|
||||||
|
@media (max-width: 510px) {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +96,7 @@ tr, td, th {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 510px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
|
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 { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../../services/api.service';
|
import { ApiService } from '../../../services/api.service';
|
||||||
import { Env, StateService } from '../../../services/state.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 { WebsocketService } from '../../../services/websocket.service';
|
||||||
import { SeoService } from '../../../services/seo.service';
|
import { SeoService } from '../../../services/seo.service';
|
||||||
|
|
||||||
@ -15,21 +15,22 @@ import { SeoService } from '../../../services/seo.service';
|
|||||||
})
|
})
|
||||||
export class RecentPegsListComponent implements OnInit {
|
export class RecentPegsListComponent implements OnInit {
|
||||||
@Input() widget: boolean = false;
|
@Input() widget: boolean = false;
|
||||||
@Input() recentPegIns$: Observable<RecentPeg[]> = of([]);
|
@Input() recentPegsList$: Observable<RecentPeg[]>;
|
||||||
@Input() recentPegOuts$: Observable<RecentPeg[]> = of([]);
|
|
||||||
|
|
||||||
env: Env;
|
env: Env;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
isPegCountLoading = true;
|
||||||
page = 1;
|
page = 1;
|
||||||
pageSize = 15;
|
pageSize = 15;
|
||||||
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
|
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
|
||||||
skeletonLines: number[] = [];
|
skeletonLines: number[] = [];
|
||||||
auditStatus$: Observable<AuditStatus>;
|
auditStatus$: Observable<AuditStatus>;
|
||||||
auditUpdated$: Observable<boolean>;
|
auditUpdated$: Observable<boolean>;
|
||||||
federationUtxos$: Observable<FederationUtxo[]>;
|
|
||||||
recentPegs$: Observable<RecentPeg[]>;
|
|
||||||
lastReservesBlockUpdate: number = 0;
|
lastReservesBlockUpdate: number = 0;
|
||||||
currentPeg$: Observable<CurrentPegs>;
|
currentPeg$: Observable<CurrentPegs>;
|
||||||
|
pegsCount$: Observable<number>;
|
||||||
|
startingIndexSubject: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||||
|
currentIndex: number = 0;
|
||||||
lastPegBlockUpdate: number = 0;
|
lastPegBlockUpdate: number = 0;
|
||||||
lastPegAmount: string = '';
|
lastPegAmount: string = '';
|
||||||
isLoad: boolean = true;
|
isLoad: boolean = true;
|
||||||
@ -93,53 +94,36 @@ export class RecentPegsListComponent implements OnInit {
|
|||||||
share()
|
share()
|
||||||
);
|
);
|
||||||
|
|
||||||
this.federationUtxos$ = this.auditUpdated$.pipe(
|
this.pegsCount$ = this.auditUpdated$.pipe(
|
||||||
filter(auditUpdated => auditUpdated === true),
|
filter(auditUpdated => auditUpdated === true),
|
||||||
throttleTime(40000),
|
tap(() => this.isPegCountLoading = true),
|
||||||
switchMap(_ => this.apiService.federationUtxos$()),
|
switchMap(_ => this.apiService.pegsCount$()),
|
||||||
|
map((data) => data.pegs_count),
|
||||||
|
tap(() => this.isPegCountLoading = false),
|
||||||
share()
|
share()
|
||||||
);
|
);
|
||||||
|
|
||||||
this.recentPegIns$ = this.federationUtxos$.pipe(
|
this.recentPegsList$ = combineLatest([
|
||||||
map(federationUtxos => federationUtxos.filter(utxo => utxo.pegtxid).map(utxo => {
|
this.auditStatus$,
|
||||||
return {
|
this.auditUpdated$,
|
||||||
txid: utxo.pegtxid,
|
this.startingIndexSubject
|
||||||
txindex: utxo.pegindex,
|
]).pipe(
|
||||||
amount: utxo.amount,
|
filter(([auditStatus, auditUpdated, startingIndex]) => {
|
||||||
bitcoinaddress: utxo.bitcoinaddress,
|
const auditStatusCheck = auditStatus.isAuditSynced === true;
|
||||||
bitcointxid: utxo.txid,
|
const auditUpdatedCheck = auditUpdated === true;
|
||||||
bitcoinindex: utxo.txindex,
|
const startingIndexCheck = startingIndex !== this.currentIndex;
|
||||||
blocktime: utxo.pegblocktime,
|
return auditStatusCheck && (auditUpdatedCheck || startingIndexCheck);
|
||||||
}
|
}),
|
||||||
})),
|
tap(([_, __, startingIndex]) => {
|
||||||
share()
|
this.currentIndex = startingIndex;
|
||||||
);
|
this.isLoading = true;
|
||||||
|
}),
|
||||||
this.recentPegOuts$ = this.auditUpdated$.pipe(
|
switchMap(([_, __, startingIndex]) => this.apiService.recentPegsList$(startingIndex)),
|
||||||
filter(auditUpdated => auditUpdated === true),
|
tap(() => this.isLoading = false),
|
||||||
throttleTime(40000),
|
|
||||||
switchMap(_ => this.apiService.recentPegOuts$()),
|
|
||||||
share()
|
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 {
|
ngOnDestroy(): void {
|
||||||
@ -148,6 +132,7 @@ export class RecentPegsListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pageChange(page: number): void {
|
pageChange(page: number): void {
|
||||||
|
this.startingIndexSubject.next((page - 1) * 15);
|
||||||
this.page = page;
|
this.page = page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
.fee-estimation-container {
|
.fee-estimation-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
@media (min-width: 376px) {
|
@media (min-width: 376px) {
|
||||||
flex-direction: row;
|
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>
|
<h5 class="card-title" i18n="liquid.avg-peg-ratio">Avg Peg Ratio</h5>
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
<div class="fee-text" [ngClass]="{'danger' : unbackedMonths.avg < 1, 'correct': unbackedMonths.avg >= 1}">
|
<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>
|
</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 { EChartsOption } from '../../../graphs/echarts';
|
||||||
import { CurrentPegs } from '../../../interfaces/node-api.interface';
|
import { CurrentPegs } from '../../../interfaces/node-api.interface';
|
||||||
|
|
||||||
@ -32,6 +32,10 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges() {
|
ngOnChanges() {
|
||||||
|
this.updateChartOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChartOptions() {
|
||||||
if (!this.currentPeg || !this.currentReserves || this.currentPeg.amount === '0') {
|
if (!this.currentPeg || !this.currentReserves || this.currentPeg.amount === '0') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -46,13 +50,43 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createChartOptions(currentPeg: CurrentPegs, currentReserves: CurrentPegs): EChartsOption {
|
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 {
|
return {
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
type: 'gauge',
|
type: 'gauge',
|
||||||
startAngle: 180,
|
startAngle: 180,
|
||||||
endAngle: 0,
|
endAngle: 0,
|
||||||
center: ['50%', '70%'],
|
center: ['50%', '75%'],
|
||||||
radius: '100%',
|
radius: '100%',
|
||||||
min: 0.999,
|
min: 0.999,
|
||||||
max: 1.001,
|
max: 1.001,
|
||||||
@ -68,13 +102,23 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: 'inherit',
|
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: {
|
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',
|
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%',
|
length: pointerLength,
|
||||||
width: 16,
|
width: pointerWidth,
|
||||||
offsetCenter: [0, '-27%'],
|
offsetCenter: offsetCenter,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: 'auto'
|
color: 'auto'
|
||||||
}
|
}
|
||||||
@ -95,7 +139,7 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
|
|||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
show: true,
|
show: true,
|
||||||
offsetCenter: [0, '-117.5%'],
|
offsetCenter: [0, '-127%'],
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: '#4a68b9',
|
color: '#4a68b9',
|
||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
@ -108,19 +152,24 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
|
|||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
formatter: function (value) {
|
formatter: function (value) {
|
||||||
return (value).toFixed(5);
|
return (value * 100).toFixed(3) + '%';
|
||||||
},
|
},
|
||||||
color: 'inherit'
|
color: 'inherit'
|
||||||
},
|
},
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
value: parseFloat(currentReserves.amount) / parseFloat(currentPeg.amount),
|
value: value,
|
||||||
name: 'Peg-O-Meter'
|
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 class="fee-estimation-container">
|
||||||
<div *ngIf="(currentReserves$ | async) as currentReserves; else loadingData">
|
<div class="item">
|
||||||
<div class="fee-estimation-container">
|
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
|
||||||
<div class="item">
|
<div *ngIf="(currentPeg$ | async) as currentPeg; else loadingData" class="card-text">
|
||||||
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
|
<div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div>
|
||||||
<div class="card-text">
|
<span class="fiat">
|
||||||
<div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div>
|
<span><ng-container i18n="shared.as-of-block">As of block</ng-container> <a [routerLink]="['/block', currentPeg.hash]">{{ currentPeg.lastBlockUpdate }}</a></span>
|
||||||
<span class="fiat">
|
</span>
|
||||||
<span>As of block <a [routerLink]="['/block', currentPeg.hash]">{{ currentPeg.lastBlockUpdate }}</a></span>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
<div class="item">
|
||||||
</div>
|
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
|
||||||
<div class="item">
|
<div *ngIf="(currentReserves$ | async) as currentReserves; else loadingData" class="card-text">
|
||||||
<h5 class="card-title" i18n="dashboard.btc-reserves">BTC Reserves</h5>
|
<div class="fee-text">{{ (+currentReserves.amount) / 100000000 | number: '1.2-2' }} <span style="color: #b86d12;">BTC</span></div>
|
||||||
<div class="card-text">
|
<span class="fiat">
|
||||||
<div class="fee-text">{{ (+currentReserves.amount) / 100000000 | number: '1.2-2' }} <span style="color: #b86d12;">BTC</span></div>
|
<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 class="fiat">
|
</span>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<ng-template #loadingData>
|
<ng-template #loadingData>
|
||||||
<div class="fee-estimation-container loading-container">
|
<div class="card-text">
|
||||||
<div class="item">
|
<div class="skeleton-loader"></div>
|
||||||
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
|
<div class="skeleton-loader"></div>
|
||||||
<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>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
}
|
}
|
||||||
.fee-text{
|
.fee-text{
|
||||||
border-bottom: 1px solid #ffffff1c;
|
border-bottom: 1px solid #ffffff1c;
|
||||||
|
color: #ffffff;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
line-height: 1.45;
|
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">
|
<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="col card-wrapper">
|
||||||
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@ -17,35 +17,26 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card graph-card">
|
<div class="card graph-card">
|
||||||
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
|
<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]">
|
||||||
<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>
|
||||||
<h5 class="card-title d-inline"><span i18n="dashboard.mempool-goggles">Mempool Goggles</span>: {{ goggleCycle[goggleIndex].name }}</h5>
|
<span> </span>
|
||||||
<span> </span>
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
</a>
|
||||||
</a>
|
<div class="quick-filter">
|
||||||
<div class="quick-filter">
|
<div class="btn-group btn-group-toggle">
|
||||||
<div class="btn-group btn-group-toggle">
|
<label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex" *ngFor="let filter of goggleCycle">
|
||||||
<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 }}
|
||||||
<input type="radio" [value]="'3m'" fragment="3m" (click)="setFilter(filter.index)" [attr.data-cy]="'3m'"> {{ filter.name }}
|
</label>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mempool-block-wrapper">
|
</div>
|
||||||
<app-mempool-block-overview
|
<div class="mempool-block-wrapper">
|
||||||
[index]="0"
|
<app-mempool-block-overview
|
||||||
[resolution]="goggleResolution"
|
[index]="0"
|
||||||
[filterFlags]="goggleFlags"
|
[resolution]="goggleResolution"
|
||||||
[filterMode]="goggleMode"
|
[filterFlags]="goggleFlags"
|
||||||
></app-mempool-block-overview>
|
[filterMode]="goggleMode"
|
||||||
</div>
|
></app-mempool-block-overview>
|
||||||
</ng-template>
|
</div>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -53,7 +44,6 @@
|
|||||||
<div class="card graph-card">
|
<div class="card graph-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
<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>
|
<h5 class="card-title mt-3" i18n="dashboard.incoming-transactions">Incoming Transactions</h5>
|
||||||
<div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
|
<div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
|
||||||
<app-incoming-transactions-graph
|
<app-incoming-transactions-graph
|
||||||
@ -64,30 +54,11 @@
|
|||||||
[windowPreferenceOverride]="'2h'"
|
[windowPreferenceOverride]="'2h'"
|
||||||
></app-incoming-transactions-graph>
|
></app-incoming-transactions-graph>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" style="max-height: 410px">
|
<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">
|
<div class="card-body">
|
||||||
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
||||||
<h5 class="card-title d-inline" i18n="dashboard.recent-rbf-replacements">Recent Replacements</h5>
|
<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>
|
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</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-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>
|
<td class="table-cell-fees"><app-fee-rate [fee]="transaction.fee" [weight]="transaction.vsize * 4"></app-fee-rate></td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -184,25 +155,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #loadingAssetsTable>
|
<ng-template #liquidDashboard>
|
||||||
<table class="table table-borderless table-striped asset-table">
|
<div class="container-xl dashboard-container" *ngIf="(auditStatus$ | async)?.isAuditSynced; else auditInProgress">
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let i of getArrayFromNumber(this.nbFeaturedAssets)">
|
<div class="row row-cols-1 row-cols-md-2">
|
||||||
<td class="asset-icon">
|
|
||||||
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
<div class="col">
|
||||||
</td>
|
<div class="card-liquid card">
|
||||||
<td class="asset-title">
|
<div class="card-title card-title-liquid">
|
||||||
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
<app-reserves-supply-stats [currentPeg$]="currentPeg$" [currentReserves$]="currentReserves$"></app-reserves-supply-stats>
|
||||||
</td>
|
</div>
|
||||||
<td class="asset-title d-none d-md-table-cell">
|
<div class="card-body pl-0" style="padding-top: 10px;">
|
||||||
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
<app-lbtc-pegs-graph [data]="fullHistory$ | async" [height]="lbtcPegGraphHeight"></app-lbtc-pegs-graph>
|
||||||
</td>
|
</div>
|
||||||
<td class="asset-title">
|
</div>
|
||||||
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
<div class="col" style="margin-bottom: 1.47rem">
|
||||||
</tbody>
|
<div class="card-liquid card">
|
||||||
</table>
|
<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>
|
||||||
|
|
||||||
<ng-template #replacementsSkeleton>
|
<ng-template #replacementsSkeleton>
|
||||||
@ -283,21 +279,56 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #lbtcPegs let-mempoolInfoData>
|
<ng-template #loadingSkeletonLiquid>
|
||||||
<div class="mempool-info-data">
|
<div class="container-xl dashboard-container">
|
||||||
<div class="item">
|
|
||||||
<h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5>
|
<div class="row row-cols-1 row-cols-md-2">
|
||||||
<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>
|
<div class="col">
|
||||||
</ng-container>
|
<div class="card-liquid card">
|
||||||
</div>
|
<div class="card-title card-title-liquid">
|
||||||
<div class="item">
|
<app-reserves-supply-stats></app-reserves-supply-stats>
|
||||||
<a class="title-link" [routerLink]="['/audit' | relativeUrl]">
|
</div>
|
||||||
<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>
|
<div class="card-body pl-0" style="padding-top: 10px;">
|
||||||
</a>
|
<app-lbtc-pegs-graph [height]="lbtcPegGraphHeight"></app-lbtc-pegs-graph>
|
||||||
<ng-container *ngIf="(currentReserves$ | async) as currentReserves; else loadingTransactions">
|
</div>
|
||||||
<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>
|
</div>
|
||||||
</ng-container>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</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;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
&.lbtc-pegs-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
h5 {
|
h5 {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
@ -409,3 +413,27 @@
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
margin-bottom: 6px;
|
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 { 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 { 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 { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
|
||||||
import { ApiService } from '../services/api.service';
|
import { ApiService } from '../services/api.service';
|
||||||
import { StateService } from '../services/state.service';
|
import { StateService } from '../services/state.service';
|
||||||
@ -33,8 +33,6 @@ interface MempoolStatsData {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||||
featuredAssets$: Observable<any>;
|
|
||||||
nbFeaturedAssets = 6;
|
|
||||||
network$: Observable<string>;
|
network$: Observable<string>;
|
||||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||||
mempoolInfoData$: Observable<MempoolInfoData>;
|
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||||
@ -54,6 +52,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
auditUpdated$: Observable<boolean>;
|
auditUpdated$: Observable<boolean>;
|
||||||
liquidReservesMonth$: Observable<any>;
|
liquidReservesMonth$: Observable<any>;
|
||||||
currentReserves$: Observable<CurrentPegs>;
|
currentReserves$: Observable<CurrentPegs>;
|
||||||
|
recentPegsList$: Observable<RecentPeg[]>;
|
||||||
|
pegsVolume$: Observable<PegsVolume[]>;
|
||||||
|
federationAddresses$: Observable<FederationAddress[]>;
|
||||||
|
federationAddressesNumber$: Observable<number>;
|
||||||
|
federationUtxosNumber$: Observable<number>;
|
||||||
fullHistory$: Observable<any>;
|
fullHistory$: Observable<any>;
|
||||||
isLoad: boolean = true;
|
isLoad: boolean = true;
|
||||||
filterSubscription: Subscription;
|
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$
|
this.transactions$ = this.stateService.transactions$
|
||||||
.pipe(
|
.pipe(
|
||||||
scan((acc, tx) => {
|
scan((acc, tx) => {
|
||||||
@ -269,7 +252,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
share(),
|
share(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
|
if (this.stateService.network === 'liquid') {
|
||||||
this.auditStatus$ = this.stateService.blocks$.pipe(
|
this.auditStatus$ = this.stateService.blocks$.pipe(
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
throttleTime(40000),
|
throttleTime(40000),
|
||||||
@ -279,22 +262,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
shareReplay(1)
|
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(
|
this.currentPeg$ = this.auditStatus$.pipe(
|
||||||
switchMap(_ =>
|
switchMap(_ =>
|
||||||
this.apiService.liquidPegs$().pipe(
|
this.apiService.liquidPegs$().pipe(
|
||||||
@ -307,7 +274,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
share()
|
share()
|
||||||
);
|
);
|
||||||
|
|
||||||
////////// BTC Reserves historical data //////////
|
|
||||||
this.auditUpdated$ = combineLatest([
|
this.auditUpdated$ = combineLatest([
|
||||||
this.auditStatus$,
|
this.auditStatus$,
|
||||||
this.currentPeg$
|
this.currentPeg$
|
||||||
@ -322,21 +288,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
const amountCheck = currentPegAmount !== this.lastPegAmount;
|
const amountCheck = currentPegAmount !== this.lastPegAmount;
|
||||||
this.lastPegAmount = currentPegAmount;
|
this.lastPegAmount = currentPegAmount;
|
||||||
return of(blockAuditCheck || amountCheck);
|
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()
|
share()
|
||||||
);
|
);
|
||||||
@ -355,11 +306,78 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
share()
|
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(
|
.pipe(
|
||||||
map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => {
|
map(([liquidPegs, currentPeg, liquidReserves, currentReserves]) => {
|
||||||
liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000;
|
liquidPegs.series[liquidPegs.series.length - 1] = parseFloat(currentPeg.amount) / 100000000;
|
||||||
|
|
||||||
if (liquidPegs.series.length === liquidReserves?.series.length) {
|
if (liquidPegs.series.length === liquidReserves?.series.length) {
|
||||||
liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000;
|
liquidReserves.series[liquidReserves.series.length - 1] = parseFloat(currentReserves?.amount) / 100000000;
|
||||||
} else if (liquidPegs.series.length === liquidReserves?.series.length + 1) {
|
} else if (liquidPegs.series.length === liquidReserves?.series.length + 1) {
|
||||||
@ -371,7 +389,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
labels: []
|
labels: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
liquidPegs,
|
liquidPegs,
|
||||||
liquidReserves
|
liquidReserves
|
||||||
@ -415,17 +433,14 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
this.incomingGraphHeight = 300;
|
this.incomingGraphHeight = 300;
|
||||||
this.goggleResolution = 82;
|
this.goggleResolution = 82;
|
||||||
this.lbtcPegGraphHeight = 320;
|
this.lbtcPegGraphHeight = 320;
|
||||||
this.nbFeaturedAssets = 6;
|
|
||||||
} else if (window.innerWidth >= 768) {
|
} else if (window.innerWidth >= 768) {
|
||||||
this.incomingGraphHeight = 215;
|
this.incomingGraphHeight = 215;
|
||||||
this.goggleResolution = 80;
|
this.goggleResolution = 80;
|
||||||
this.lbtcPegGraphHeight = 230;
|
this.lbtcPegGraphHeight = 230;
|
||||||
this.nbFeaturedAssets = 4;
|
|
||||||
} else {
|
} else {
|
||||||
this.incomingGraphHeight = 180;
|
this.incomingGraphHeight = 180;
|
||||||
this.goggleResolution = 86;
|
this.goggleResolution = 86;
|
||||||
this.lbtcPegGraphHeight = 220;
|
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 { IncomingTransactionsGraphComponent } from '../components/incoming-transactions-graph/incoming-transactions-graph.component';
|
||||||
import { MempoolGraphComponent } from '../components/mempool-graph/mempool-graph.component';
|
import { MempoolGraphComponent } from '../components/mempool-graph/mempool-graph.component';
|
||||||
import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs-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 { GraphsComponent } from '../components/graphs/graphs.component';
|
||||||
import { StatisticsComponent } from '../components/statistics/statistics.component';
|
import { StatisticsComponent } from '../components/statistics/statistics.component';
|
||||||
import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component';
|
import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component';
|
||||||
@ -48,6 +55,13 @@ import { CommonModule } from '@angular/common';
|
|||||||
IncomingTransactionsGraphComponent,
|
IncomingTransactionsGraphComponent,
|
||||||
MempoolGraphComponent,
|
MempoolGraphComponent,
|
||||||
LbtcPegsGraphComponent,
|
LbtcPegsGraphComponent,
|
||||||
|
ReservesSupplyStatsComponent,
|
||||||
|
ReservesRatioStatsComponent,
|
||||||
|
ReservesRatioComponent,
|
||||||
|
RecentPegsStatsComponent,
|
||||||
|
RecentPegsListComponent,
|
||||||
|
FederationAddressesStatsComponent,
|
||||||
|
FederationAddressesListComponent,
|
||||||
HashrateChartComponent,
|
HashrateChartComponent,
|
||||||
HashrateChartPoolsComponent,
|
HashrateChartPoolsComponent,
|
||||||
BlockHealthGraphComponent,
|
BlockHealthGraphComponent,
|
||||||
|
@ -15,17 +15,10 @@ import { AssetsComponent } from '../components/assets/assets.component';
|
|||||||
import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component'
|
import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component'
|
||||||
import { AssetComponent } from '../components/asset/asset.component';
|
import { AssetComponent } from '../components/asset/asset.component';
|
||||||
import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.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 { 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 { 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 { 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 { 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 = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -77,18 +70,6 @@ const routes: Routes = [
|
|||||||
data: { preload: true, networkSpecific: true },
|
data: { preload: true, networkSpecific: true },
|
||||||
loadChildren: () => import('../components/block/block.module').then(m => m.BlockModule),
|
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',
|
path: 'audit/wallet',
|
||||||
data: { networks: ['liquid'] },
|
data: { networks: ['liquid'] },
|
||||||
@ -180,17 +161,8 @@ export class LiquidRoutingModule { }
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
LiquidMasterPageComponent,
|
LiquidMasterPageComponent,
|
||||||
ReservesAuditDashboardComponent,
|
|
||||||
ReservesSupplyStatsComponent,
|
|
||||||
RecentPegsStatsComponent,
|
|
||||||
RecentPegsListComponent,
|
|
||||||
FederationWalletComponent,
|
FederationWalletComponent,
|
||||||
FederationUtxosListComponent,
|
FederationUtxosListComponent,
|
||||||
FederationAddressesStatsComponent,
|
|
||||||
FederationAddressesListComponent,
|
|
||||||
ReservesRatioComponent,
|
|
||||||
ReservesRatioStatsComponent,
|
|
||||||
ReservesRatioGraphComponent,
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class LiquidMasterPageModule { }
|
export class LiquidMasterPageModule { }
|
@ -200,16 +200,20 @@ export class ApiService {
|
|||||||
return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos');
|
return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos');
|
||||||
}
|
}
|
||||||
|
|
||||||
recentPegOuts$(): Observable<RecentPeg[]> {
|
recentPegsList$(count: number = 0): Observable<RecentPeg[]> {
|
||||||
return this.httpClient.get<RecentPeg[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegouts');
|
return this.httpClient.get<RecentPeg[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/list/' + count);
|
||||||
}
|
}
|
||||||
|
|
||||||
federationAddressesOneMonthAgo$(): Observable<any> {
|
pegsCount$(): Observable<any> {
|
||||||
return this.httpClient.get<FederationAddress[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses/previous-month');
|
return this.httpClient.get<number>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/count');
|
||||||
}
|
}
|
||||||
|
|
||||||
federationUtxosOneMonthAgo$(): Observable<any> {
|
federationAddressesNumber$(): Observable<any> {
|
||||||
return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos/previous-month');
|
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[]> {
|
listFeaturedAssets$(): Observable<any[]> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user