-
diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.scss b/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.scss
index 46f9ffa4d..1116f8d85 100644
--- a/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.scss
+++ b/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.scss
@@ -10,19 +10,8 @@
background-color: #1d1f31;
}
-.graph-card {
- height: 100%;
- @media (min-width: 992px) {
- height: 385px;
- }
-}
-
.card-title {
- font-size: 1rem;
- color: #4a68b9;
-}
-.card-title > a {
- color: #4a68b9;
+ padding-top: 20px;
}
.card-body.pool-ranking {
@@ -48,17 +37,6 @@
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
}
-.main-title {
- position: relative;
- color: #ffffff91;
- margin-top: -13px;
- font-size: 11px;
- text-transform: uppercase;
- font-weight: 500;
- text-align: center;
- padding-bottom: 3px;
-}
-
.in-progress-message {
position: relative;
color: #ffffff91;
diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.ts b/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.ts
index 95de4fdca..c3fa6321d 100644
--- a/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.ts
+++ b/frontend/src/app/components/liquid-reserves-audit/reserves-audit-dashboard/reserves-audit-dashboard.component.ts
@@ -2,9 +2,9 @@ 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, concat, delay, filter, share, skip, switchMap, tap, throttleTime } from 'rxjs';
+import { Observable, combineLatest, concat, delay, filter, interval, map, mergeMap, of, share, skip, startWith, switchMap, tap, throttleTime } from 'rxjs';
import { ApiService } from '../../../services/api.service';
-import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo } from '../../../interfaces/node-api.interface';
+import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo, LiquidPegs } from '../../../interfaces/node-api.interface';
@Component({
selector: 'app-reserves-audit-dashboard',
@@ -14,10 +14,16 @@ import { AuditStatus, CurrentPegs, FederationAddress, FederationUtxo } from '../
})
export class ReservesAuditDashboardComponent implements OnInit {
auditStatus$: Observable
;
+ auditUpdated$: Observable;
currentPeg$: Observable;
currentReserves$: Observable;
federationUtxos$: Observable;
+ federationUtxosOneMonthAgo$: Observable;
federationAddresses$: Observable;
+ federationAddressesOneMonthAgo$: Observable;
+ liquidPegsMonth$: Observable;
+ liquidReservesMonth$: Observable;
+ fullHistory$: Observable;
private lastPegBlockUpdate: number = 0;
private lastReservesBlockUpdate: number = 0;
@@ -45,8 +51,16 @@ export class ReservesAuditDashboardComponent implements OnInit {
)
);
- this.currentReserves$ = this.auditStatus$.pipe(
+ this.auditUpdated$ = this.auditStatus$.pipe(
filter(auditStatus => auditStatus.isAuditSynced === true),
+ map(auditStatus => auditStatus.lastBlockAudit),
+ switchMap((lastBlockAudit) => {
+ return lastBlockAudit > this.lastReservesBlockUpdate ? of(true) : of(false);
+ }),
+ );
+
+ this.currentReserves$ = this.auditUpdated$.pipe(
+ filter(auditUpdated => auditUpdated === true),
switchMap(_ =>
this.apiService.liquidReserves$().pipe(
filter((currentReserves) => currentReserves.lastBlockUpdate > this.lastReservesBlockUpdate),
@@ -71,17 +85,84 @@ export class ReservesAuditDashboardComponent implements OnInit {
share()
);
- this.federationUtxos$ = this.auditStatus$.pipe(
- filter(auditStatus => auditStatus.isAuditSynced === true),
+ this.federationUtxos$ = this.auditUpdated$.pipe(
+ filter(auditUpdated => auditUpdated === true),
switchMap(_ => this.apiService.federationUtxos$()),
share()
);
- this.federationAddresses$ = this.auditStatus$.pipe(
- filter(auditStatus => auditStatus.isAuditSynced === true),
+ this.federationAddresses$ = this.auditUpdated$.pipe(
+ filter(auditUpdated => auditUpdated === true),
switchMap(_ => this.apiService.federationAddresses$()),
share()
);
+
+ this.federationUtxosOneMonthAgo$ = interval(60 * 60 * 1000)
+ .pipe(
+ startWith(0),
+ switchMap(() => this.apiService.federationUtxosOneMonthAgo$())
+ );
+
+ 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()
+ );
}
}
diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html
new file mode 100644
index 000000000..04d82f37b
--- /dev/null
+++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html
@@ -0,0 +1,42 @@
+
+
+
+
+
Unpeg
+
+
0, 'correct': unbackedMonths.total === 0}">
+ {{ unbackedMonths.total }} Unpeg Event
+
+
+
+
+
+
Avg Peg Ratio
+
+
= 1}">
+ {{ unbackedMonths.avg.toFixed(5) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.scss b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.scss
new file mode 100644
index 000000000..72aa390e4
--- /dev/null
+++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.scss
@@ -0,0 +1,63 @@
+.fee-estimation-container {
+ display: flex;
+ justify-content: space-between;
+ @media (min-width: 376px) {
+ flex-direction: row;
+ }
+ .item {
+ max-width: 300px;
+ margin: 0;
+ width: -webkit-fill-available;
+ @media (min-width: 376px) {
+ margin: 0 auto 0px;
+ }
+
+ .card-title {
+ margin-bottom: 4px;
+ color: #4a68b9;
+ font-size: 10px;
+ font-size: 1rem;
+ white-space: nowrap;
+ }
+
+ .card-text {
+ font-size: 22px;
+ span {
+ font-size: 11px;
+ position: relative;
+ top: -2px;
+ }
+ .danger {
+ color: #D81B60;
+ }
+ .correct {
+ color: #7CB342;
+ }
+ }
+
+ .card-text span {
+ color: #ffffff66;
+ font-size: 12px;
+ top: 0px;
+ }
+ .fee-text{
+ width: fit-content;
+ margin: auto;
+ line-height: 1.45;
+ padding: 0px 2px;
+ }
+ }
+}
+
+.loading-container{
+ min-height: 76px;
+}
+
+.card-text {
+ .skeleton-loader {
+ width: 100%;
+ display: block;
+ max-width: 90px;
+ margin: 15px auto 3px;
+ }
+}
diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.ts b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.ts
new file mode 100644
index 000000000..45a114c1f
--- /dev/null
+++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.ts
@@ -0,0 +1,51 @@
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
+import { Observable, map } from 'rxjs';
+
+@Component({
+ selector: 'app-reserves-ratio-stats',
+ templateUrl: './reserves-ratio-stats.component.html',
+ styleUrls: ['./reserves-ratio-stats.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ReservesRatioStatsComponent implements OnInit {
+ @Input() fullHistory$: Observable;
+ unbackedMonths$: Observable
+
+ constructor() { }
+
+ ngOnInit(): void {
+ if (!this.fullHistory$) {
+ return;
+ }
+ this.unbackedMonths$ = this.fullHistory$
+ .pipe(
+ map((fullHistory) => {
+ if (fullHistory.liquidPegs.series.length !== fullHistory.liquidReserves.series.length) {
+ return {
+ historyComplete: false,
+ total: null
+ };
+ }
+ // Only check the last 3 years
+ let ratioSeries = fullHistory.liquidReserves.series.map((value: number, index: number) => value / fullHistory.liquidPegs.series[index]);
+ ratioSeries = ratioSeries.slice(Math.max(ratioSeries.length - 36, 0));
+ let total = 0;
+ let avg = 0;
+ for (let i = 0; i < ratioSeries.length; i++) {
+ avg += ratioSeries[i];
+ if (ratioSeries[i] < 1) {
+ total++;
+ }
+ }
+ avg = avg / ratioSeries.length;
+ return {
+ historyComplete: true,
+ total: total,
+ avg: avg,
+ };
+ })
+ );
+
+ }
+
+}
diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.html b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.html
new file mode 100644
index 000000000..cffb73c06
--- /dev/null
+++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.html
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.scss b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.scss
new file mode 100644
index 000000000..9881148fc
--- /dev/null
+++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.scss
@@ -0,0 +1,6 @@
+.loadingGraphs {
+ position: absolute;
+ top: 50%;
+ left: calc(50% - 16px);
+ z-index: 100;
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.ts b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.ts
new file mode 100644
index 000000000..187a059a1
--- /dev/null
+++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio-graph.component.ts
@@ -0,0 +1,195 @@
+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) => ``;
+ let itemFormatted = '' + params[0].axisValue + '
';
+ 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 += `
+
${colorSpan(item.color)}
+
+
${symbol}${formattedValue}
+
`;
+ return `${itemFormatted}
`;
+ }
+ },
+ 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'
+ }
+ },
+ };
+ }
+}
+
diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.html b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.html
index 561656760..64e68624b 100644
--- a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.html
+++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.html
@@ -1,4 +1,4 @@
-
+
diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.ts b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.ts
index 0a6363257..b53172e97 100644
--- a/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.ts
+++ b/frontend/src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.ts
@@ -12,7 +12,7 @@ import { CurrentPegs } from '../../../interfaces/node-api.interface';
export class ReservesRatioComponent implements OnInit, OnChanges {
@Input() currentPeg: CurrentPegs;
@Input() currentReserves: CurrentPegs;
- pegsChartOptions: EChartsOption;
+ ratioChartOptions: EChartsOption;
height: number | string = '200';
right: number | string = '10';
@@ -21,8 +21,7 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
template: ('widget' | 'advanced') = 'widget';
isLoading = true;
- pegsChartOption: EChartsOption = {};
- pegsChartInitOption = {
+ ratioChartInitOptions = {
renderer: 'svg'
};
@@ -36,7 +35,7 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
if (!this.currentPeg || !this.currentReserves || this.currentPeg.amount === '0') {
return;
}
- this.pegsChartOptions = this.createChartOptions(this.currentPeg, this.currentReserves);
+ this.ratioChartOptions = this.createChartOptions(this.currentPeg, this.currentReserves);
}
rendered() {
diff --git a/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.html b/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.html
index d0a624a4f..6d3897093 100644
--- a/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.html
+++ b/frontend/src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.html
@@ -11,7 +11,7 @@