diff --git a/package-lock.json b/package-lock.json index 9d7bd37f69f9c164cbdb66cb6e21d8e229fa882b..091156df35957967c6d615e691cf8fbb685cbdf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "apollo-link-serialize": "~4.0.0", "apollo3-cache-persist": "~0.14.1", "bs58": "^5.0.0", + "chart.js": "^4.4.7", "graphql-tag": "~2.12.6", "graphql-ws": "~5.16.0", "ionicons": "~7.4.0", @@ -63,6 +64,7 @@ "moment": "^2.30.1", "moment-timezone": "^0.5.46", "ng-qrcode": "^17.0.0", + "ng2-charts": "^6.0.1", "ngx-color-picker": "^16.0.0", "ngx-jdenticon": "^2.0.0", "ngx-markdown": "^17.1.1", @@ -593,6 +595,23 @@ "@angular/core": "17.3.12" } }, + "node_modules/@angular/cdk": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.1.1.tgz", + "integrity": "sha512-MmfNB9iANuDN1TS+HL8uKqA3/7pdVeCRN+HdAcfqFrcqZmSUUSlYWy8PXqymmyeXxoSwt9p4I/6R0By03VoCMw==", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^19.0.0 || ^20.0.0", + "@angular/core": "^19.0.0 || ^20.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/cli": { "version": "17.3.11", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.11.tgz", @@ -6161,6 +6180,11 @@ "integrity": "sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==", "dev": true }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -12103,6 +12127,17 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/chart.js": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chevrotain": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.1.1.tgz", @@ -14583,7 +14618,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.12" }, @@ -19561,8 +19596,7 @@ "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "optional": true + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, "node_modules/lodash._baseassign": { "version": "3.2.0", @@ -21305,6 +21339,23 @@ "@angular/core": ">=17 <18" } }, + "node_modules/ng2-charts": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-6.0.1.tgz", + "integrity": "sha512-pO7evbvHqjiKB7zqE12tCKWQI9gmQ8DVOEaWBBLlxJabc4fLGk7o9t4jC4+Q9pJiQrTtQkugm0dIPQ4PFHUaWA==", + "dependencies": { + "lodash-es": "^4.17.15", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": ">=17.0.0", + "@angular/common": ">=17.0.0", + "@angular/core": ">=17.0.0", + "@angular/platform-browser": ">=17.0.0", + "chart.js": "^3.4.0 || ^4.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/ngx-color-picker": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-16.0.0.tgz", @@ -22726,7 +22777,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dev": true, + "devOptional": true, "dependencies": { "entities": "^4.4.0" }, diff --git a/package.json b/package.json index 995d6e65f8895dd1177a8244929d49f8b3283443..d61c3434f172ce067435510216a0a862af4392d8 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "apollo-link-serialize": "~4.0.0", "apollo3-cache-persist": "~0.14.1", "bs58": "^5.0.0", + "chart.js": "^4.4.7", "graphql-tag": "~2.12.6", "graphql-ws": "~5.16.0", "ionicons": "~7.4.0", @@ -135,6 +136,7 @@ "moment": "^2.30.1", "moment-timezone": "^0.5.46", "ng-qrcode": "^17.0.0", + "ng2-charts": "^6.0.1", "ngx-color-picker": "^16.0.0", "ngx-jdenticon": "^2.0.0", "ngx-markdown": "^17.1.1", diff --git a/src/app/currency/currency-history.model.ts b/src/app/currency/currency-history.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e89f0d626364f372f12aaba4c591b7ab43b9751 --- /dev/null +++ b/src/app/currency/currency-history.model.ts @@ -0,0 +1,40 @@ +import { Moment } from 'moment/moment'; + +export class CurrencyHistory { + constructor(membersCount?: number, blockNumber?: number, timestamp?: Date, monetaryMass?: number) { + this.membersCount = membersCount; + this.blockNumber = blockNumber; + this.timestamp = timestamp; + this.monetaryMass = monetaryMass; + } + + membersCount: number; + blockNumber: number; + timestamp: Date; + monetaryMass: number; +} + +export interface CurrencyHistoryFilter { + before?: Moment; + after?: Moment; +} + +export class ChartMembers { + constructor(dataPoints: DataChart[], label: string) { + this.dataPoints = dataPoints; + this.label = label; + } + type: string; + dataPoints: DataChart[]; + label: string; +} + +export class DataChart { + constructor(x: string, y: string) { + this.x = x; + this.y = y; + } + + x: string; + y: string; +} diff --git a/src/app/currency/currency.converter.ts b/src/app/currency/currency.converter.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6caa045e9ba5f3ebe8c8027e69f760db240402c --- /dev/null +++ b/src/app/currency/currency.converter.ts @@ -0,0 +1,23 @@ +import { LightUniversalDividendFragment, UniversalDividendConnection } from '../network/indexer/indexer-types.generated'; +import { CurrencyHistory } from '@app/currency/currency-history.model'; + +export class CurrencyConverter { + static squidConnectionToCurrency(universalDividendConnection: UniversalDividendConnection, debug?: boolean): CurrencyHistory[] { + const inputs = universalDividendConnection.edges?.map((edge) => { + return edge.node; + }) as CurrencyHistory[]; + const results = (inputs || []).map(this.squidToCurrency); + if (debug) console.debug('Results -> ', results); + return results; + } + + static squidToCurrency(input: LightUniversalDividendFragment): CurrencyHistory { + if (!input) return undefined; + return <CurrencyHistory>{ + membersCount: input.membersCount, + blockNumber: input.blockNumber, + timestamp: input.timestamp, + monetaryMass: input.monetaryMass, + }; + } +} diff --git a/src/app/currency/currency.module.ts b/src/app/currency/currency.module.ts index dc578232ae3e14dda473d2824a6ab7793fb309bb..ed7647465f73f4ef4d13584d99fc857e14e7a249 100644 --- a/src/app/currency/currency.module.ts +++ b/src/app/currency/currency.module.ts @@ -3,11 +3,14 @@ import { NgModule } from '@angular/core'; import { AppSharedModule } from '@app/shared/shared.module'; import { TranslateModule } from '@ngx-translate/core'; import { CurrencyPage } from './currency.page'; +import { BaseChartDirective, provideCharts, withDefaultRegisterables } from 'ng2-charts'; @NgModule({ - imports: [AppSharedModule, TranslateModule.forChild()], + imports: [AppSharedModule, TranslateModule.forChild(), BaseChartDirective], declarations: [CurrencyPage], exports: [CurrencyPage], + providers: [provideCharts(withDefaultRegisterables())], + bootstrap: [CurrencyPage], }) export class AppCurrencyModule { constructor() { diff --git a/src/app/currency/currency.page.html b/src/app/currency/currency.page.html index 94f24c4650eece85ad54270889cfc32551bdb165..17b4c2441e24f3e6c0c5ce77e598cb3d8da88e5f 100644 --- a/src/app/currency/currency.page.html +++ b/src/app/currency/currency.page.html @@ -17,14 +17,13 @@ <div id="container"> <ion-grid class="ion-no-padding"> <ion-row> - <ion-col size="0" size-md="2" size-lg="3"></ion-col> - <ion-col size="12" size-md="8" size-lg="6"> + <ion-col size="12" size-sm="6"> <ion-list *rxIf="loaded$; else listSkeleton" lines="none"> <ion-item-divider translate>CURRENCY.VIEW.TITLE</ion-item-divider> <ion-item> <ion-icon slot="start" name="bookmark"></ion-icon> - <ion-label color="dark" translate>CURRENCY.VIEW.CURRENCY_NAME</ion-label> + <ion-label translate>CURRENCY.VIEW.CURRENCY_NAME</ion-label> <ion-badge color="light"> {{ params.currencyNetwork }} ( <span [innerHtml]="params.currencyName"></span> @@ -34,7 +33,7 @@ <ion-item> <ion-icon slot="start" name="pie-chart"></ion-icon> - <ion-label color="dark" translate>CURRENCY.VIEW.SHARE</ion-label> + <ion-label translate>CURRENCY.VIEW.SHARE</ion-label> <ion-badge color="secondary"> {{ params.monetaryMass / params.members | numberFormat }} <span [innerHtml]="params.currencySymbol"></span> @@ -43,22 +42,53 @@ <ion-item> <ion-icon slot="start" name="ellipse"></ion-icon> - <ion-label color="dark" translate>CURRENCY.VIEW.MASS</ion-label> + <ion-label translate>CURRENCY.VIEW.MASS</ion-label> <ion-badge color="warning"> {{ params.monetaryMass | numberFormat }} <span [innerHtml]="params.currencySymbol"></span> </ion-badge> </ion-item> + <div style="position: relative"> + <ion-button fill="clear" class="sub-menu-chart" id="click-trigger"> + <ion-icon slot="icon-only" name="menu"></ion-icon> + </ion-button> + <ion-popover [dismissOnSelect]="true" trigger="click-trigger" triggerAction="click"> + <ng-template> + <ion-header> + <ion-toolbar> + <ion-title translate>COMMON.BTN_OPTIONS</ion-title> + </ion-toolbar> + </ion-header> + <ion-list [inset]="true"> + <ion-item [button]="true" (click)="isLogarithmic = !isLogarithmic"> + @if (isLogarithmic) { + <ion-icon slot="start" name="checkmark"></ion-icon> + } + <ion-label translate>GRAPH.COMMON.LOGARITHMIC_SCALE</ion-label> + </ion-item> + </ion-list> + </ng-template> + </ion-popover> + <canvas + baseChart + *ngIf="chartReady$ | async" + [datasets]="chartMonetaryMassData$ | async" + [type]="'line'" + [labels]="chartLabels$ | async" + [options]="chartMonetaryMassOptions" + [legend]="false" + ></canvas> + </div> <ion-item> <ion-icon slot="start" name="trending-up"></ion-icon> - <ion-label color="dark" translate>CURRENCY.VIEW.C_ACTUAL</ion-label> + <ion-label translate>CURRENCY.VIEW.C_ACTUAL</ion-label> <ion-badge color="light">[TBD] / day</ion-badge> </ion-item> <ion-item> <ion-icon slot="start" name="reload"></ion-icon> - <ion-label color="dark" translate>CURRENCY.VIEW.UD</ion-label> + <ion-label translate>CURRENCY.VIEW.UD</ion-label> <ion-badge color="tertiary"> {{ params.currentUd }} <span [innerHtml]="params.currencySymbol"></span> @@ -77,38 +107,210 @@ <ion-item> <ion-icon slot="start" name="trending-up"></ion-icon> - <ion-label color="dark" translate>CURRENCY.VIEW.C_RULE</ion-label> + <ion-label translate>CURRENCY.VIEW.C_RULE</ion-label> <ion-badge color="light">{{ params.growthRate | numberFormat: { fixedDecimals: 2 } }}% / 6 months</ion-badge> </ion-item> - @if (showAllRules) { + @if (showAllRulesCurrency) { <ion-item> - <ion-icon slot="start" name="reload"></ion-icon> - <ion-icon slot="start" class="icon-secondary" name="time"></ion-icon> - <ion-label color="dark" translate>CURRENCY.VIEW.DT_REEVAL</ion-label> + <div slot="start" class="icons"> + <ion-icon name="reload" class="icon-primary"></ion-icon> + <ion-icon name="time-outline" class="icon-secondary"></ion-icon> + </div> + <ion-label translate>CURRENCY.VIEW.DT_REEVAL</ion-label> <ion-badge color="light">{{ params.udReevalPeriodMs | duration: 'ms' }}</ion-badge> </ion-item> <ion-item> - <ion-icon slot="start" name="reload"></ion-icon> - <ion-icon slot="start" class="icon-secondary" name="calendar"></ion-icon> - <ion-label color="dark" translate>CURRENCY.VIEW.UD_REEVAL_TIME0</ion-label> - <ion-badge color="light">...</ion-badge> + <div slot="start" class="icons"> + <ion-icon name="reload" class="icon-primary"></ion-icon> + <ion-icon name="calendar-outline" class="icon-secondary"></ion-icon> + </div> + <ion-label translate>CURRENCY.VIEW.UD_REEVAL_TIME0</ion-label> + <ion-badge color="light">{{ params.pastReevals | date: 'dd/MM/YYYY HH:MM' }}</ion-badge> </ion-item> <ion-item> - <ion-icon slot="start" name="reload"></ion-icon> - <ion-icon slot="start" class="icon-secondary" name="calculator"></ion-icon> - <ion-label color="dark" translate>CURRENCY.VIEW.UD_RULE</ion-label> - <ion-badge color="light">...</ion-badge> + <div slot="start" class="icons"> + <ion-icon name="reload" class="icon-primary"></ion-icon> + <ion-icon name="calculator-outline" class="icon-secondary"></ion-icon> + </div> + <ion-label translate>CURRENCY.VIEW.UD_RULE</ion-label> + <ion-badge color="light" style="font-size: 10px"> + {{ 'COMMON.UD' | translate }} + (t + <sub>{{ 'CURRENCY.VIEW.REEVAL_SYMBOL' | translate }}</sub> + - dt + <sub>{{ 'CURRENCY.VIEW.REEVAL_SYMBOL' | translate }}</sub> + ) + c + <sup>2</sup> + * (M/N)(t + <sub>{{ 'CURRENCY.VIEW.REEVAL_SYMBOL' | translate }}</sub> + - dt + <sub> + {{ 'CURRENCY.VIEW.REEVAL_SYMBOL' | translate }} + </sub> + ) / dt + <sub>{{ 'CURRENCY.VIEW.REEVAL_SYMBOL' | translate }}</sub> + </ion-badge> </ion-item> } + <ion-item> - <ion-toggle [(ngModel)]="showAllRules" justify="end"> + <ion-toggle [(ngModel)]="showAllRulesCurrency" justify="end"> <ion-label color="medium" translate>CURRENCY.VIEW.DISPLAY_ALL_RULES</ion-label> </ion-toggle> </ion-item> </ion-list> </ion-col> + <ion-col size="12" size-sm="6"> + <ion-list *rxIf="loaded$; else listSkeleton" lines="none"> + <ion-item-divider translate>CURRENCY.VIEW.WOT_DIVIDER</ion-item-divider> + <ion-item> + <ion-icon slot="start" name="people"></ion-icon> + <ion-label><p translate>CURRENCY.VIEW.MEMBERS</p></ion-label> + <ion-badge color="secondary">{{ params.members | numberFormat }}</ion-badge> + </ion-item> + + <ion-item> + <ion-icon slot="start" name="people"></ion-icon> + <ion-label> + <p>{{ 'CURRENCY.VIEW.MEMBERS_VARIATION' | translate }}(ΔN)</p> + </ion-label> + <ion-badge color="light">{{ membersVariation$ | async }}</ion-badge> + </ion-item> + @if (chartReady$ | async) { + <canvas + baseChart + [datasets]="chartMembersData$ | async" + [type]="'line'" + [labels]="chartLabels$ | async" + [options]="chartMembersOptions" + [legend]="false" + ></canvas> + } + <ion-item-divider translate>CURRENCY.VIEW.WOT_RULES_DIVIDER</ion-item-divider> + + <ion-item> + <ion-icon slot="start" name="ribbon"></ion-icon> + <ion-label color="dark"><p translate>CURRENCY.VIEW.SIG_QTY_RULE</p></ion-label> + <ion-badge color="success"> + {{ wotParams.sigQtyRule | numberFormat }} + </ion-badge> + </ion-item> - <ion-col size="0" size-md="2" size-lg="3"></ion-col> + <ion-item> + <div slot="start" class="icons"> + <ion-icon name="person" class="icon-primary"></ion-icon> + <ion-icon name="time-outline" class="icon-secondary"></ion-icon> + </div> + <ion-label color="dark"><p translate>CURRENCY.VIEW.MS_WINDOW</p></ion-label> + <ion-badge color="danger"> + {{ wotParams.msWindow | duration: 'ms' }} + </ion-badge> + </ion-item> + <ion-item> + <div slot="start" class="icons"> + <ion-icon name="person" class="icon-primary"></ion-icon> + <ion-icon name="calendar-clear-outline" class="icon-secondary"></ion-icon> + </div> + <ion-label color="dark"><p translate>CURRENCY.VIEW.MS_VALIDITY</p></ion-label> + <ion-badge color="success"> + {{ wotParams.msValidity | duration: 'ms' }} + </ion-badge> + </ion-item> + @if (showAllRulesWot) { + <ion-item> + <div slot="start" class="icons"> + <ion-icon name="ribbon" class="icon-primary"></ion-icon> + <ion-icon name="time-outline" class="icon-secondary"></ion-icon> + </div> + <ion-label color="dark"><p translate>CURRENCY.VIEW.SIG_WINDOW</p></ion-label> + <ion-badge color="light"> + {{ wotParams.sigWindow | duration: 'ms' }} + </ion-badge> + </ion-item> + <ion-item> + <div slot="start" class="icons"> + <ion-icon name="ribbon" class="icon-primary"></ion-icon> + <ion-icon name="calendar-clear-outline" class="icon-secondary"></ion-icon> + </div> + <ion-label color="dark"><p translate>CURRENCY.VIEW.SIG_VALIDITY</p></ion-label> + <ion-badge color="success"> + {{ wotParams.sigValidity | duration: 'ms' }} + </ion-badge> + </ion-item> + + <ion-item> + <ion-icon slot="start" name="medal"></ion-icon> + <ion-label color="dark"><p translate>CURRENCY.VIEW.SIG_STOCK</p></ion-label> + <ion-badge color="light"> + {{ wotParams.sigStock }} + </ion-badge> + </ion-item> + <ion-item> + <div slot="start" class="icons"> + <ion-icon name="time-outline" class="icon-primary"></ion-icon> + <ion-icon name="medal" class="icon-secondary" style="left: -10px; top: -10px"></ion-icon> + <ion-icon name="arrow-forward" class="icon-secondary" style="left: 4px; top: -10px"></ion-icon> + <ion-icon name="medal" class="icon-secondary" style="left: 19px; top: -10px"></ion-icon> + </div> + <ion-label color="dark"><p translate>CURRENCY.VIEW.SIG_PERIOD</p></ion-label> + <ion-badge color="light"> + {{ wotParams.sigPeriod | duration: 'ms' }} + </ion-badge> + </ion-item> + + <ion-item> + <div slot="start" class="icons"> + <ion-icon name="logo-steam" class="icon-primary"></ion-icon> + <ion-icon name="person" class="icon-secondary" style="left: 14px"></ion-icon> + </div> + <ion-label color="dark"><p translate>CURRENCY.VIEW.STEP_MAX</p></ion-label> + <ion-badge color="danger"> + {{ wotParams.stepMax }} + </ion-badge> + </ion-item> + + <ion-item> + <div slot="start" class="icons"> + <ion-icon name="ribbon" class="icon-primary"></ion-icon> + <ion-icon name="star" class="icon-secondary" style="color: yellow; font-size: 14px; left: 5px; top: 0px"></ion-icon> + </div> + <ion-label color="dark"><p [innerHTML]="'CURRENCY.VIEW.SENTRIES' | translate"></p></ion-label> + <ion-badge color="light"> + {{ wotParams.sentries }} + </ion-badge> + </ion-item> + + <ion-item> + <div slot="start" class="icons"> + <ion-icon name="ribbon" class="icon-primary"></ion-icon> + <ion-icon name="star" class="icon-secondary" style="color: yellow; font-size: 14px; left: 5px; top: 0px"></ion-icon> + </div> + <ion-label color="dark"><p [innerHTML]="'CURRENCY.VIEW.SENTRIES_FORMULA' | translate"></p></ion-label> + <ion-badge color="light"> + <p class="item-note dark"> + {{ 'CURRENCY.VIEW.MATH_CEILING' | translate }} + </p> + ( N + <sub>t</sub> + <sup>^ (1 / stepMax)</sup> + ) + </ion-badge> + </ion-item> + + <ion-item> + <ion-icon slot="start" name="git-pull-request"></ion-icon> + <ion-label color="dark"><p translate>CURRENCY.VIEW.XPERCENT</p></ion-label> + + <ion-badge color="light">{{ wotParams.xPercent | percent }}</ion-badge> + </ion-item> + } + <ion-item> + <ion-toggle [(ngModel)]="showAllRulesWot" justify="end"> + <ion-label color="medium" translate>CURRENCY.VIEW.DISPLAY_ALL_RULES</ion-label> + </ion-toggle> + </ion-item> + </ion-list> + </ion-col> </ion-row> </ion-grid> </div> diff --git a/src/app/currency/currency.page.scss b/src/app/currency/currency.page.scss index 5fdf1824ec6e13554ebc39e3cb271dff7e57ea7b..4a1406d47edee2588f91401453f700962c33e235 100644 --- a/src/app/currency/currency.page.scss +++ b/src/app/currency/currency.page.scss @@ -1,2 +1,37 @@ #container { } + +.sub-menu-chart { + position: absolute; + top: -6px; + right: 0px; + --ion-text-color: var(--ion-color-dark); + color: var(--ion-text-color); +} + +.icons { + position: relative; +} + +ion-item { + --ion-text-color: var(--ion-color-dark); + ion-icon { + color: var(--ion-text-color); + + &.icon-primary { + font-size: 1.5em; + } + + &.icon-secondary { + position: absolute; + left: 13px; + top: -8px; + font-size: 14px; + } + } + + + p { + font-size: 0.87rem; + } +} diff --git a/src/app/currency/currency.page.ts b/src/app/currency/currency.page.ts index 31743500a95f0b22ba5260d5f7f9801d83b43dac..e17deda9cc65ace591a18f17a8a964e75f6d7a15 100644 --- a/src/app/currency/currency.page.ts +++ b/src/app/currency/currency.page.ts @@ -1,14 +1,21 @@ -import { Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { NetworkService } from '@app/network/network.service'; import { AppPage, AppPageState } from '@app/shared/pages/base-page.class'; import { TranslateService } from '@ngx-translate/core'; import { RxState } from '@rx-angular/state'; -import { RxStateProperty } from '@app/shared/decorator/state.decorator'; +import { RxStateProperty, RxStateSelect } from '@app/shared/decorator/state.decorator'; import { SettingsService } from '@app/settings/settings.service'; import { CurrencyDisplayUnit } from '@app/settings/settings.model'; -import { map } from 'rxjs/operators'; -import { toBoolean, toNumber } from '@app/shared/functions'; +import { map, switchMap } from 'rxjs/operators'; +import { isNotEmptyArray, toBoolean, toNumber } from '@app/shared/functions'; +import { CurrencyHistory } from '@app/currency/currency-history.model'; +import { ChartDataset, ChartOptions } from 'chart.js'; +import { Observable } from 'rxjs'; +import { IndexerService } from '@app/network/indexer/indexer.service'; +import { Currency } from '@app/currency/currency.model'; +import moment from 'moment'; +import { Color } from '@app/shared/colors/graph-colors'; export interface CurrencyParameters { currencyName: string; @@ -21,14 +28,39 @@ export interface CurrencyParameters { udCreationPeriodMs: number; udReevalPeriodMs: number; growthRate: number; + pastReevals: number; +} + +export interface WotParameters { + sigQtyRule: number; + sigWindow: number; + sigValidity: number; + sigStock: number; + sigPeriod: number; + msWindow: number; + msValidity: number; + stepMax: number; + sentries: number; + xPercent: number; } export interface CurrencyPageState extends AppPageState { + currency: Currency; params: CurrencyParameters; + wotParams: WotParameters; paramsByUnit: Map<CurrencyDisplayUnit, CurrencyParameters>; useRelativeUnit: boolean; displayUnit: CurrencyDisplayUnit; - showAllRules: boolean; + showAllRulesCurrency: boolean; + showAllRulesWot: boolean; + currencyHistory: CurrencyHistory[]; + chartMembersData: ChartDataset<'line'>[]; + chartLabels: string[]; + chartLoaded: boolean; + chartMonetaryMassData: ChartDataset<'line'>[]; + isLogarithmic: boolean; + chartReady: boolean; + membersVariation: number; } @Component({ @@ -36,18 +68,92 @@ export interface CurrencyPageState extends AppPageState { templateUrl: './currency.page.html', styleUrls: ['./currency.page.scss'], providers: [RxState], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CurrencyPage extends AppPage<CurrencyPageState> { + @RxStateSelect() protected chartMembersData$: Observable<ChartDataset<'line'>[]>; + @RxStateSelect() protected chartMonetaryMassData$: Observable<ChartDataset<'line'>[]>; + @RxStateSelect() protected chartLabels$: Observable<string[]>; + @RxStateSelect() protected chartReady$: Observable<string[]>; + @RxStateSelect() protected membersVariation$: Observable<number>; + @RxStateProperty() protected params: CurrencyParameters; + @RxStateProperty() protected wotParams: WotParameters; @RxStateProperty() protected useRelativeUnit: boolean; + @RxStateProperty() protected currencyHistory: CurrencyHistory[]; + @RxStateProperty() protected chartMonetaryMassData: CurrencyHistory[]; + @RxStateProperty() protected chartMembersData: ChartDataset<'line'>[]; + @RxStateProperty() protected chartLabels: string[]; + @RxStateProperty() protected chartLoaded: boolean; + @RxStateProperty() protected chartReady: boolean; + @RxStateProperty() protected membersVariation: number; - @Input() @RxStateProperty() showAllRules: boolean; + @Input() @RxStateProperty() showAllRulesCurrency: boolean; + @Input() @RxStateProperty() showAllRulesWot: boolean; @Input() @RxStateProperty() displayUnit: CurrencyDisplayUnit; + @Input() @RxStateProperty() isLogarithmic: boolean = false; + + public chartMembersOptions = <ChartOptions<'line'>>{ + responsive: true, + maintainAspectRatio: true, + interaction: { + intersect: false, + mode: 'index', + }, + plugins: { + title: { + text: this.translate.instant('GRAPH.CURRENCY.MEMBERS_COUNT_TITLE'), + display: true, + }, + }, + scales: { + x: { + position: 'bottom', + }, + y: { + id: 'y-axis-1', + ticks: { + beginAtZero: false, + }, + }, + }, + color: Color.get('primary').rgba(1), + }; + public chartMonetaryMassOptions = <ChartOptions<'line'>>{ + responsive: true, + plugins: { + title: { + text: this.translate.instant('GRAPH.CURRENCY.MONETARY_MASS_TITLE'), + display: true, + }, + }, + interaction: { + intersect: false, + mode: 'index', + }, + scales: { + x: { + position: 'bottom', + }, + y: { + id: 'y-axis-1', + ticks: { + beginAtZero: false, + }, + }, + }, + borderColor: Color.get('primary').rgba(1), + }; + + protected powBase: number; + protected ud: number; constructor( protected networkService: NetworkService, protected translate: TranslateService, + protected indexer: IndexerService, protected settings: SettingsService, + protected cd: ChangeDetectorRef, private sanitizer: DomSanitizer ) { super({ name: 'currency-service' }); @@ -61,49 +167,212 @@ export class CurrencyPage extends AppPage<CurrencyPageState> { 'params', this._state.select(['paramsByUnit', 'displayUnit'], (res) => res).pipe(map(({ paramsByUnit, displayUnit }) => paramsByUnit.get(displayUnit))) ); + + this._state.connect('currencyHistory', this._state.select('currency').pipe(switchMap(() => this.indexer.loadCurrencyHistory()))); + this._state.connect('chartMembersData', this._state.select('currencyHistory').pipe(map((value) => this.prepareCurrencyMemberHistory(value)))); + this._state.connect( + 'chartMonetaryMassData', + this._state + .select(['currencyHistory', 'displayUnit', 'isLogarithmic'], (res) => res) + .pipe(map(({ currencyHistory, displayUnit, isLogarithmic }) => this.prepareMonetaryMassHistory(currencyHistory, displayUnit, isLogarithmic))) + ); + this._state.connect('membersVariation', this._state.select('currencyHistory').pipe(map((value) => this.getMembersVariation(value)))); + this._state.connect('chartLabels', this._state.select('currencyHistory').pipe(map((value) => this.prepareLineChartLabel(value)))); + this._state.connect( + 'chartReady', + this._state + .select(['chartLabels', 'chartMembersData', 'chartMonetaryMassData'], (res) => res) + .pipe( + map( + ({ chartLabels, chartMembersData, chartMonetaryMassData }) => + isNotEmptyArray(chartLabels) && isNotEmptyArray(chartMembersData) && isNotEmptyArray(chartMonetaryMassData) + ) + ) + ); } protected async ngOnLoad(): Promise<Partial<CurrencyPageState>> { - const showAllRules = toBoolean(this.showAllRules, false); + const showAllRulesCurrency = toBoolean(this.showAllRulesCurrency, false); + const showAllRulesWot = toBoolean(this.showAllRulesWot, false); + const useRelativeUnit = toBoolean(this.useRelativeUnit, false); const network = await this.networkService.ready(); const api = network.api; const currency = network.currency; - const powBase = Math.pow(10, currency.decimals); - const [monetaryMassFractions, currentUd, members] = await Promise.all([ + this.powBase = Math.pow(10, currency.decimals); + const msPerBlock = toNumber(api.consts.babe.expectedBlockTime); + const ud0 = toNumber(api.consts.universalDividend.unitsPerUd) / this.powBase; + const [monetaryMassFractions, currentUd, members, pastReevals] = await Promise.all([ api.query.universalDividend.monetaryMass(), api.query.universalDividend.currentUd(), api.query.membership.counterForMembership(), + api.query.universalDividend.pastReevals(), ]); - const ud0 = toNumber(api.consts.universalDividend.unitsPerUd) / powBase; - const ud = toNumber(currentUd) / powBase; + this.ud = toNumber(currentUd) / this.powBase; const growthRate = Math.sqrt(toNumber(api.consts.universalDividend.squareMoneyGrowthRate)); + + const sigQtyRule = toNumber(api.consts.wot.minCertForMembership); + const msWindow = toNumber(api.consts.distance.evaluationPeriod) * msPerBlock * 3; + const msValidity = toNumber(api.consts.membership.membershipPeriod) * msPerBlock; + const sigWindow = toNumber(api.consts.membership.membershipRenewalPeriod) * msPerBlock; + const sigValidity = toNumber(api.consts.certification.validityPeriod) * msPerBlock; + const sigStock = toNumber(api.consts.certification.maxByIssuer); + const sigPeriod = toNumber(api.consts.certification.certPeriod) * msPerBlock; + const stepMax = toNumber(api.consts.distance.maxRefereeDistance); + const sentries = toNumber(api.consts.smithMembers.minCertForMembership); + const xPercent = toNumber(api.consts.distance.minAccessibleReferees) / 1000000000; + const paramsByUnit = new Map<CurrencyDisplayUnit, CurrencyParameters>(); paramsByUnit.set('base', { currencyName: currency.displayName, currencySymbol: currency.symbol, currencyNetwork: currency.network, - monetaryMass: toNumber(monetaryMassFractions) / powBase, + monetaryMass: toNumber(monetaryMassFractions) / this.powBase, members: toNumber(members), - currentUd: ud, + currentUd: this.ud, ud0, udCreationPeriodMs: toNumber(api.consts.universalDividend.udCreationPeriod), udReevalPeriodMs: toNumber(api.consts.universalDividend.udReevalPeriod), growthRate, + pastReevals: toNumber(pastReevals[0][1]), }); paramsByUnit.set('du', { currencyName: currency.displayName, currencySymbol: this.sanitizer.bypassSecurityTrustHtml(`${this.translate.instant('COMMON.UD')}<sub>${currency.symbol}</sub>`), currencyNetwork: currency.network, - monetaryMass: toNumber(monetaryMassFractions) / powBase / ud, + monetaryMass: toNumber(monetaryMassFractions) / this.powBase / this.ud, members: toNumber(members), currentUd: 1, ud0, udCreationPeriodMs: toNumber(api.consts.universalDividend.udCreationPeriod), udReevalPeriodMs: toNumber(api.consts.universalDividend.udReevalPeriod), growthRate, + pastReevals: toNumber(pastReevals[0][1]), }); - return { paramsByUnit, showAllRules, useRelativeUnit }; + const wotParams = <WotParameters>{ + sigQtyRule, + sigWindow, + sigValidity, + sigStock, + sigPeriod, + msWindow, + msValidity, + stepMax, + sentries, + xPercent, + }; + return { currency, paramsByUnit, showAllRulesCurrency, showAllRulesWot, useRelativeUnit, wotParams }; + } + + private prepareCurrencyMemberHistory(histories: CurrencyHistory[]): ChartDataset<'line'>[] { + const dataset: ChartDataset<'line'> = { + data: histories.map((data) => { + return data.membersCount; + }), + label: this.translate.instant('GRAPH.CURRENCY.MEMBERS_COUNT_LABEL'), + fill: true, + tension: 0, + borderWidth: 2, + pointRadius: 0, + pointHitRadius: 4, + pointHoverRadius: 3, + backgroundColor: Color.get('secondary').rgba(0.4), + }; + this.chartMonetaryMassOptions.plugins = { + title: { + text: this.translate.instant('GRAPH.CURRENCY.MONETARY_MASS_TITLE'), + display: true, + }, + }; + + if (this._debug) console.debug(`${this._logPrefix} number of member history generated: ${dataset.data.length}`); + return <ChartDataset<'line'>[]>[dataset]; + } + + private prepareMonetaryMassHistory(histories: CurrencyHistory[], displayUnit: CurrencyDisplayUnit, isLogarithmic: boolean): ChartDataset<'line'>[] { + const dataset: ChartDataset<'line'>[] = [ + { + data: histories.map((data) => { + if (displayUnit == 'base') { + return data.monetaryMass / this.powBase; + } else { + return data.monetaryMass / this.powBase / this.ud; + } + }), + + label: this.translate.instant('GRAPH.CURRENCY.MONETARY_MASS_LABEL'), + fill: true, + tension: 0, + borderWidth: 2, + pointRadius: 0, + pointHitRadius: 4, + pointHoverRadius: 3, + backgroundColor: Color.get('secondary').rgba(0.4), + borderColor: Color.get('secondary').rgba(1), + }, + { + data: histories.map((data) => { + if (displayUnit == 'base') { + return data.monetaryMass / this.powBase / data.membersCount; + } else { + return data.monetaryMass / this.powBase / data.membersCount / this.ud; + } + }), + label: this.translate.instant('GRAPH.CURRENCY.MONETARY_MASS_SHARE_LABEL'), + borderColor: Color.parseRgba('rgb(255,255,0)').rgba(1), + tension: 0, + borderWidth: 2, + pointRadius: 0, + pointHitRadius: 4, + pointHoverRadius: 3, + }, + ]; + + if (isLogarithmic) { + this.chartMonetaryMassOptions = <ChartOptions<'line'>>{ + ...this.chartMonetaryMassOptions, + scales: { + y: { + id: 'y-axis-1', + type: 'logarithmic', + ticks: { + beginAtZero: false, + }, + }, + }, + }; + } else { + this.chartMonetaryMassOptions = <ChartOptions<'line'>>{ + ...this.chartMonetaryMassOptions, + scales: { + y: { + id: 'y-axis-1', + type: 'linear', + ticks: { + beginAtZero: false, + }, + }, + }, + }; + } + return dataset; + } + + private prepareLineChartLabel(histories: CurrencyHistory[]): string[] { + const labels = histories.map((data) => { + return moment(data.timestamp).format('MM/YYYY'); + }); + + if (this._debug) console.debug(`${this._logPrefix} number of label generated: ${labels.length}`); + + this.cd.markForCheck(); + return labels; + } + + private getMembersVariation(histories: CurrencyHistory[]): number { + const membersCountCurrent = histories[histories.length - 1]; + const membersCountPast = histories[histories.length - 2]; + return membersCountCurrent.membersCount - membersCountPast.membersCount; } } diff --git a/src/app/currency/currency.service.ts b/src/app/currency/currency.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..e41923b0f749ffcfdecffe72a5aad45de52da6a5 --- /dev/null +++ b/src/app/currency/currency.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { SettingsService } from '@app/settings/settings.service'; +import { StartableService } from '@app/shared/services/startable-service.class'; +import { Promise } from '@rx-angular/cdk/zone-less/browser'; +import { IndexerService } from '@app/network/indexer/indexer.service'; + +@Injectable({ providedIn: 'root' }) +export class CurrencyService extends StartableService { + constructor( + private indexer: IndexerService, + private settings: SettingsService + ) { + super(settings); + } + + protected ngOnStart(): Promise<void> { + return Promise.resolve(undefined); + } +} diff --git a/src/app/network/indexer/indexer-currency.gql b/src/app/network/indexer/indexer-currency.gql new file mode 100644 index 0000000000000000000000000000000000000000..77dc3eb4cebb9d2909909a0fc51fd9196e92060f --- /dev/null +++ b/src/app/network/indexer/indexer-currency.gql @@ -0,0 +1,26 @@ +fragment LightUniversalDividend on UniversalDividend{ + membersCount + blockNumber + timestamp + monetaryMass +} + +fragment UniversalDividend on UniversalDividendConnection{ + edges { + node { + ...LightUniversalDividend + } + } +} + +query CurrencyHistory($before: String, $after: String, $distinctOn :[UniversalDividendSelectColumn!], $orderBy: [UniversalDividendOrderBy!] ){ + universalDividendConnection( + distinctOn: $distinctOn + before: $before + after: $after + orderBy: $orderBy + where: { _and: [{ id: {_isNull: false}}, {eventId:{_isNull:false}}]} + ) { + ...UniversalDividend + } +} diff --git a/src/app/network/indexer/indexer-types.generated.ts b/src/app/network/indexer/indexer-types.generated.ts index 340aa2df2e226bb410cce42eda03d8becbbfee1f..d6dae77458386c2e9a4d8769294ff44b7cea5673 100644 --- a/src/app/network/indexer/indexer-types.generated.ts +++ b/src/app/network/indexer/indexer-types.generated.ts @@ -4749,6 +4749,20 @@ export type CertsConnectionByReceiverQueryVariables = Exact<{ export type CertsConnectionByReceiverQuery = { __typename?: 'query_root', identityConnection: { __typename?: 'IdentityConnection', edges: Array<{ __typename?: 'IdentityEdge', node: { __typename?: 'Identity', aggregate: { __typename?: 'CertAggregate', aggregate?: { __typename?: 'CertAggregateFields', count: number } | null }, connection: { __typename?: 'CertConnection', edges: Array<{ __typename?: 'CertEdge', node: { __typename: 'Cert', id: string, expireOn: number, createdOn: number, updatedOn: number, issuer?: { __typename?: 'Identity', id: string, index: number, name: string, accountId?: string | null, status?: IdentityStatusEnum | null, isMember: boolean, createdOn: number, membershipHistory: Array<{ __typename: 'MembershipEvent', id: string, eventType?: EventTypeEnum | null }> } | null } }>, pageInfo: { __typename?: 'PageInfo', endCursor: string, hasNextPage: boolean } } } }> } }; +export type LightUniversalDividendFragment = { __typename?: 'UniversalDividend', membersCount: number, blockNumber: number, timestamp: any, monetaryMass: any }; + +export type UniversalDividendFragment = { __typename?: 'UniversalDividendConnection', edges: Array<{ __typename?: 'UniversalDividendEdge', node: { __typename?: 'UniversalDividend', membersCount: number, blockNumber: number, timestamp: any, monetaryMass: any } }> }; + +export type CurrencyHistoryQueryVariables = Exact<{ + before?: InputMaybe<Scalars['String']['input']>; + after?: InputMaybe<Scalars['String']['input']>; + distinctOn?: InputMaybe<Array<UniversalDividendSelectColumn> | UniversalDividendSelectColumn>; + orderBy?: InputMaybe<Array<UniversalDividendOrderBy> | UniversalDividendOrderBy>; +}>; + + +export type CurrencyHistoryQuery = { __typename?: 'query_root', universalDividendConnection: { __typename?: 'UniversalDividendConnection', edges: Array<{ __typename?: 'UniversalDividendEdge', node: { __typename?: 'UniversalDividend', membersCount: number, blockNumber: number, timestamp: any, monetaryMass: any } }> } }; + export type TransferFragment = { __typename: 'Transfer', id: string, amount: any, timestamp: any, blockNumber: number, from?: { __typename?: 'Account', id: string, identity?: { __typename?: 'Identity', id: string, index: number, name: string, accountId?: string | null, status?: IdentityStatusEnum | null, isMember: boolean, createdOn: number, membershipHistory: Array<{ __typename: 'MembershipEvent', id: string, eventType?: EventTypeEnum | null }> } | null } | null, to?: { __typename?: 'Account', id: string, identity?: { __typename?: 'Identity', id: string, index: number, name: string, accountId?: string | null, status?: IdentityStatusEnum | null, isMember: boolean, createdOn: number, membershipHistory: Array<{ __typename: 'MembershipEvent', id: string, eventType?: EventTypeEnum | null }> } | null } | null }; export type TransferConnectionFragment = { __typename?: 'TransferConnection', pageInfo: { __typename?: 'PageInfo', endCursor: string, hasNextPage: boolean }, edges: Array<{ __typename?: 'TransferEdge', node: { __typename: 'Transfer', id: string, amount: any, timestamp: any, blockNumber: number, from?: { __typename?: 'Account', id: string, identity?: { __typename?: 'Identity', id: string, index: number, name: string, accountId?: string | null, status?: IdentityStatusEnum | null, isMember: boolean, createdOn: number, membershipHistory: Array<{ __typename: 'MembershipEvent', id: string, eventType?: EventTypeEnum | null }> } | null } | null, to?: { __typename?: 'Account', id: string, identity?: { __typename?: 'Identity', id: string, index: number, name: string, accountId?: string | null, status?: IdentityStatusEnum | null, isMember: boolean, createdOn: number, membershipHistory: Array<{ __typename: 'MembershipEvent', id: string, eventType?: EventTypeEnum | null }> } | null } | null } }> }; @@ -4933,6 +4947,23 @@ export const CertIssuedConnectionFragmentDoc = gql` } ${LightCertFragmentDoc} ${LightIdentityFragmentDoc}`; +export const LightUniversalDividendFragmentDoc = gql` + fragment LightUniversalDividend on UniversalDividend { + membersCount + blockNumber + timestamp + monetaryMass +} + `; +export const UniversalDividendFragmentDoc = gql` + fragment UniversalDividend on UniversalDividendConnection { + edges { + node { + ...LightUniversalDividend + } + } +} + ${LightUniversalDividendFragmentDoc}`; export const TransferFragmentDoc = gql` fragment Transfer on Transfer { id @@ -5083,6 +5114,30 @@ export const CertsConnectionByReceiverDocument = gql` super(apollo); } } +export const CurrencyHistoryDocument = gql` + query CurrencyHistory($before: String, $after: String, $distinctOn: [UniversalDividendSelectColumn!], $orderBy: [UniversalDividendOrderBy!]) { + universalDividendConnection( + distinctOn: $distinctOn + before: $before + after: $after + orderBy: $orderBy + where: {_and: [{id: {_isNull: false}}, {eventId: {_isNull: false}}]} + ) { + ...UniversalDividend + } +} + ${UniversalDividendFragmentDoc}`; + + @Injectable({ + providedIn: 'root' + }) + export class CurrencyHistoryGQL extends Apollo.Query<CurrencyHistoryQuery, CurrencyHistoryQueryVariables> { + document = CurrencyHistoryDocument; + client = 'indexer'; + constructor(apollo: Apollo.Apollo) { + super(apollo); + } + } export const TransferConnectionByAddressDocument = gql` query TransferConnectionByAddress($address: String!, $first: Int!, $orderBy: [TransferOrderBy!]!, $after: String) { transferConnection( @@ -5213,6 +5268,7 @@ export const WotSearchByUidDocument = gql` private lastBlockGql: LastBlockGQL, private certsConnectionByIssuerGql: CertsConnectionByIssuerGQL, private certsConnectionByReceiverGql: CertsConnectionByReceiverGQL, + private currencyHistoryGql: CurrencyHistoryGQL, private transferConnectionByAddressGql: TransferConnectionByAddressGQL, private wotSearchByTextGql: WotSearchByTextGQL, private wotSearchByAddressGql: WotSearchByAddressGQL, @@ -5260,6 +5316,14 @@ export const WotSearchByUidDocument = gql` return this.certsConnectionByReceiverGql.watch(variables, options) } + currencyHistory(variables?: CurrencyHistoryQueryVariables, options?: QueryOptionsAlone<CurrencyHistoryQueryVariables>) { + return this.currencyHistoryGql.fetch(variables, options) + } + + currencyHistoryWatch(variables?: CurrencyHistoryQueryVariables, options?: WatchQueryOptionsAlone<CurrencyHistoryQueryVariables>) { + return this.currencyHistoryGql.watch(variables, options) + } + transferConnectionByAddress(variables: TransferConnectionByAddressQueryVariables, options?: QueryOptionsAlone<TransferConnectionByAddressQueryVariables>) { return this.transferConnectionByAddressGql.fetch(variables, options) } diff --git a/src/app/network/indexer/indexer.service.ts b/src/app/network/indexer/indexer.service.ts index dd9de3182b4bf3215bc93f9d787d74faaa112d0d..7ea83d4fe535bdb4294c04f81e451bb3d2b86a37 100644 --- a/src/app/network/indexer/indexer.service.ts +++ b/src/app/network/indexer/indexer.service.ts @@ -21,6 +21,7 @@ import { LightAccountConnectionFragment, OrderBy, TransferFragment, + UniversalDividendConnection, } from './indexer-types.generated'; import { firstValueFrom, mergeMap, Observable } from 'rxjs'; import { filter, map } from 'rxjs/operators'; @@ -39,6 +40,8 @@ import { CertificationSearchFilterUtils, } from '@app/certification/history/cert-history.model'; import { AccountConverter } from '@app/account/account.converter'; +import { CurrencyHistory, CurrencyHistoryFilter } from '@app/currency/currency-history.model'; +import { CurrencyConverter } from '@app/currency/currency.converter'; export interface IndexerState extends GraphqlServiceState { currency: Currency; @@ -305,6 +308,33 @@ export class IndexerService extends GraphqlService<IndexerState> { return this.blockSearch({ height }, { first: 1, after: null }).pipe(map(firstArrayValue)); } + loadCurrencyHistory( + filter?: CurrencyHistoryFilter, + options?: { + fetchPolicy?: FetchPolicy; + } + ): Observable<CurrencyHistory[]> { + const now = Date.now(); + + console.debug(`${this._logPrefix} Fetching currency history...`, filter); + const variables = { + orderBy: { blockNumber: OrderBy.Asc }, + }; + + if (isNotNil(filter?.after) && isNotNil(filter?.before)) { + // variables.after = filter.after?.toISOString(); + // variables.before = filter.before?.toISOString(); + } + return this.graphqlService.currencyHistory(variables, { fetchPolicy: options?.fetchPolicy || 'cache-first' }).pipe( + map(({ data }) => { + const res = CurrencyConverter.squidConnectionToCurrency(data.universalDividendConnection as UniversalDividendConnection); + const duration = Date.now() - now; + if (duration > 10) console.info(`${this._logPrefix}${res.length} currency history loaded in ${duration}ms`); + return res; + }) + ); + } + protected async ngOnStart(): Promise<IndexerState> { // Wait settings and storage const [settings, currency] = await Promise.all([this.settings.ready(), firstNotNilPromise(this.currency$)]); diff --git a/src/app/network/pod/pod-schema.graphql b/src/app/network/pod/pod-schema.graphql index 8273276d3ca437130e48ed19b0e410b3594f5e28..f6999a41940a3c06686fc0141bdf5eccd7f865de 100644 --- a/src/app/network/pod/pod-schema.graphql +++ b/src/app/network/pod/pod-schema.graphql @@ -57,14 +57,14 @@ type profiles { geoloc: point "CID of the latest index request that modified this document" index_request_cid: String! - "base58 pubkey of profile owner" + "ss58 address of profile owner" pubkey: String! socials( "JSON select path" path: String ): jsonb "timestamp of the latest index request that modified this document" - time: timestamptz! + time: timestamp! "title of c+ profile" title: String } @@ -92,10 +92,10 @@ type profiles_max_fields { description: String "CID of the latest index request that modified this document" index_request_cid: String - "base58 pubkey of profile owner" + "ss58 address of profile owner" pubkey: String "timestamp of the latest index request that modified this document" - time: timestamptz + time: timestamp "title of c+ profile" title: String } @@ -110,10 +110,10 @@ type profiles_min_fields { description: String "CID of the latest index request that modified this document" index_request_cid: String - "base58 pubkey of profile owner" + "ss58 address of profile owner" pubkey: String "timestamp of the latest index request that modified this document" - time: timestamptz + time: timestamp "title of c+ profile" title: String } @@ -147,7 +147,7 @@ type query_root { ): profiles_aggregate! "fetch data from the table: \"profiles\" using primary key columns" profiles_by_pk( - "base58 pubkey of profile owner" + "ss58 address of profile owner" pubkey: String! ): profiles } @@ -181,7 +181,7 @@ type subscription_root { ): profiles_aggregate! "fetch data from the table: \"profiles\" using primary key columns" profiles_by_pk( - "base58 pubkey of profile owner" + "ss58 address of profile owner" pubkey: String! ): profiles "fetch data from the table in a streaming manner: \"profiles\"" @@ -247,7 +247,7 @@ scalar jsonb scalar point -scalar timestamptz +scalar timestamp input GeolocInput { latitude: Float! @@ -346,7 +346,7 @@ input profiles_bool_exp { index_request_cid: String_comparison_exp pubkey: String_comparison_exp socials: jsonb_comparison_exp - time: timestamptz_comparison_exp + time: timestamp_comparison_exp title: String_comparison_exp } @@ -383,24 +383,24 @@ input profiles_stream_cursor_value_input { geoloc: point "CID of the latest index request that modified this document" index_request_cid: String - "base58 pubkey of profile owner" + "ss58 address of profile owner" pubkey: String socials: jsonb "timestamp of the latest index request that modified this document" - time: timestamptz + time: timestamp "title of c+ profile" title: String } -"Boolean expression to compare columns of type \"timestamptz\". All fields are combined with logical 'AND'." -input timestamptz_comparison_exp { - _eq: timestamptz - _gt: timestamptz - _gte: timestamptz - _in: [timestamptz!] +"Boolean expression to compare columns of type \"timestamp\". All fields are combined with logical 'AND'." +input timestamp_comparison_exp { + _eq: timestamp + _gt: timestamp + _gte: timestamp + _in: [timestamp!] _is_null: Boolean - _lt: timestamptz - _lte: timestamptz - _neq: timestamptz - _nin: [timestamptz!] + _lt: timestamp + _lte: timestamp + _neq: timestamp + _nin: [timestamp!] } diff --git a/src/app/network/pod/pod-types.generated.ts b/src/app/network/pod/pod-types.generated.ts index 07fedc0c06c572c2ccda81b532b42df86fc4ffd2..5cead1f404898578c8393c467de36bd4830044e8 100644 --- a/src/app/network/pod/pod-types.generated.ts +++ b/src/app/network/pod/pod-types.generated.ts @@ -20,7 +20,7 @@ export type Scalars = { Float: { input: number; output: number; } jsonb: { input: any; output: any; } point: { input: any; output: any; } - timestamptz: { input: any; output: any; } + timestamp: { input: any; output: any; } }; export type AddTransactionResponse = { @@ -221,11 +221,11 @@ export type Profiles = { geoloc?: Maybe<Scalars['point']['output']>; /** CID of the latest index request that modified this document */ index_request_cid: Scalars['String']['output']; - /** base58 pubkey of profile owner */ + /** ss58 address of profile owner */ pubkey: Scalars['String']['output']; socials?: Maybe<Scalars['jsonb']['output']>; /** timestamp of the latest index request that modified this document */ - time: Scalars['timestamptz']['output']; + time: Scalars['timestamp']['output']; /** title of c+ profile */ title?: Maybe<Scalars['String']['output']>; }; @@ -271,7 +271,7 @@ export type Profiles_Bool_Exp = { index_request_cid?: InputMaybe<String_Comparison_Exp>; pubkey?: InputMaybe<String_Comparison_Exp>; socials?: InputMaybe<Jsonb_Comparison_Exp>; - time?: InputMaybe<Timestamptz_Comparison_Exp>; + time?: InputMaybe<Timestamp_Comparison_Exp>; title?: InputMaybe<String_Comparison_Exp>; }; @@ -286,10 +286,10 @@ export type Profiles_Max_Fields = { description?: Maybe<Scalars['String']['output']>; /** CID of the latest index request that modified this document */ index_request_cid?: Maybe<Scalars['String']['output']>; - /** base58 pubkey of profile owner */ + /** ss58 address of profile owner */ pubkey?: Maybe<Scalars['String']['output']>; /** timestamp of the latest index request that modified this document */ - time?: Maybe<Scalars['timestamptz']['output']>; + time?: Maybe<Scalars['timestamp']['output']>; /** title of c+ profile */ title?: Maybe<Scalars['String']['output']>; }; @@ -305,10 +305,10 @@ export type Profiles_Min_Fields = { description?: Maybe<Scalars['String']['output']>; /** CID of the latest index request that modified this document */ index_request_cid?: Maybe<Scalars['String']['output']>; - /** base58 pubkey of profile owner */ + /** ss58 address of profile owner */ pubkey?: Maybe<Scalars['String']['output']>; /** timestamp of the latest index request that modified this document */ - time?: Maybe<Scalars['timestamptz']['output']>; + time?: Maybe<Scalars['timestamp']['output']>; /** title of c+ profile */ title?: Maybe<Scalars['String']['output']>; }; @@ -370,11 +370,11 @@ export type Profiles_Stream_Cursor_Value_Input = { geoloc?: InputMaybe<Scalars['point']['input']>; /** CID of the latest index request that modified this document */ index_request_cid?: InputMaybe<Scalars['String']['input']>; - /** base58 pubkey of profile owner */ + /** ss58 address of profile owner */ pubkey?: InputMaybe<Scalars['String']['input']>; socials?: InputMaybe<Scalars['jsonb']['input']>; /** timestamp of the latest index request that modified this document */ - time?: InputMaybe<Scalars['timestamptz']['input']>; + time?: InputMaybe<Scalars['timestamp']['input']>; /** title of c+ profile */ title?: InputMaybe<Scalars['String']['input']>; }; @@ -454,17 +454,17 @@ export type Subscription_RootProfiles_StreamArgs = { where?: InputMaybe<Profiles_Bool_Exp>; }; -/** Boolean expression to compare columns of type "timestamptz". All fields are combined with logical 'AND'. */ -export type Timestamptz_Comparison_Exp = { - _eq?: InputMaybe<Scalars['timestamptz']['input']>; - _gt?: InputMaybe<Scalars['timestamptz']['input']>; - _gte?: InputMaybe<Scalars['timestamptz']['input']>; - _in?: InputMaybe<Array<Scalars['timestamptz']['input']>>; +/** Boolean expression to compare columns of type "timestamp". All fields are combined with logical 'AND'. */ +export type Timestamp_Comparison_Exp = { + _eq?: InputMaybe<Scalars['timestamp']['input']>; + _gt?: InputMaybe<Scalars['timestamp']['input']>; + _gte?: InputMaybe<Scalars['timestamp']['input']>; + _in?: InputMaybe<Array<Scalars['timestamp']['input']>>; _is_null?: InputMaybe<Scalars['Boolean']['input']>; - _lt?: InputMaybe<Scalars['timestamptz']['input']>; - _lte?: InputMaybe<Scalars['timestamptz']['input']>; - _neq?: InputMaybe<Scalars['timestamptz']['input']>; - _nin?: InputMaybe<Array<Scalars['timestamptz']['input']>>; + _lt?: InputMaybe<Scalars['timestamp']['input']>; + _lte?: InputMaybe<Scalars['timestamp']['input']>; + _neq?: InputMaybe<Scalars['timestamp']['input']>; + _nin?: InputMaybe<Array<Scalars['timestamp']['input']>>; }; export type LightProfileFragment = { __typename: 'profiles', title?: string | null, time: any, id?: string | null, address: string, avatar_cid?: string | null }; diff --git a/src/app/shared/functions.ts b/src/app/shared/functions.ts index 53484d0b5d434e386efffa6f4f462b0a9a1d4f95..e38d4387dfda64384ac734aa0b04bf58fdb54bc2 100644 --- a/src/app/shared/functions.ts +++ b/src/app/shared/functions.ts @@ -82,7 +82,8 @@ export function toBoolean(obj: boolean | null | undefined | string, defaultValue } export function toNumber(obj: number | u32 | u64 | Codec | null | undefined, defaultValue?: number): number { if (obj instanceof u32) return obj.toNumber(); - if (obj instanceof u64) return obj.toNumber(); + if (obj instanceof u64) return Number(obj); + return obj !== undefined && obj !== null ? +obj : defaultValue; } export function toFloat(obj: string | null | undefined, defaultValue?: number): number | null { diff --git a/src/app/shared/pages/base-page.class.ts b/src/app/shared/pages/base-page.class.ts index 04a0696164750d178a22d705f3b12803babf4803..0f5b84be0d43c80d3eaaad68ee9a18e0e20f8614 100644 --- a/src/app/shared/pages/base-page.class.ts +++ b/src/app/shared/pages/base-page.class.ts @@ -135,6 +135,7 @@ export abstract class AppPage<S extends AppPageState = AppPageState, O extends A this.markAsLoaded(); } catch (err) { this.setError(err); + console.error(this._logPrefix + 'error while loading', err); this.markAsLoaded(); } } diff --git a/src/app/transfer/send/transfer.controller.ts b/src/app/transfer/send/transfer.controller.ts index dfed4ca468d65289fbb500fc44fa2e14c1b00ee8..79760ea7ea1c2291209c97e87d9a51561d970b5f 100644 --- a/src/app/transfer/send/transfer.controller.ts +++ b/src/app/transfer/send/transfer.controller.ts @@ -23,7 +23,7 @@ export class TransferController implements ITransferController { if (opts?.account?.address) { await this.navController.navigateForward(['transfer', 'from', opts.account.address], { state: { - to: '5H7L4V5qMLEcqAsRMmyRYU42q8XWxgk1HroC5QsQTDZpY7hx', + to: opts.recipient.address, }, }); } else if (opts?.recipient?.address) { diff --git a/src/assets/i18n/ca.json b/src/assets/i18n/ca.json index 363b957965512af23998a9da8d103ecc20c7d6a3..018a690f19268798d2c687644f75c529d7e0892f 100644 --- a/src/assets/i18n/ca.json +++ b/src/assets/i18n/ca.json @@ -1012,5 +1012,18 @@ "TITLE": "{{'COMMON.APP_NAME'|translate}} - Pago en línia", "TITLE_SHORT": "Pago en línia" } + }, + "GRAPH": { + "COMMON": { + "LOGARITHMIC_SCALE" : "Escala logarítmica" + }, + "CURRENCY": { + "MONETARY_MASS_TITLE": "Evolución de la masa monetaria", + "MONETARY_MASS_LABEL": "Masa monetaria", + "MONETARY_MASS_SHARE_LABEL": "Promedio miembro", + "UD_TITLE": "Evolución del dividendo universales", + "MEMBERS_COUNT_TITLE": "Evolución del número de miembros", + "MEMBERS_COUNT_LABEL": "Número de miembros" + } } } diff --git a/src/assets/i18n/en-GB.json b/src/assets/i18n/en-GB.json index a4fa9e5d77cc0060886b2b6576b859247e87c911..898c2a5ea9b09a0094c350a6a42a408a19858d67 100644 --- a/src/assets/i18n/en-GB.json +++ b/src/assets/i18n/en-GB.json @@ -1021,5 +1021,22 @@ "EXAMPLE_BUTTON_ICON_G1_BLACK": "Ğ1 logo (outline)" } } + }, + "GRAPH": { + "COMMON": { + "LOGARITHMIC_SCALE": "Logarithmic scale" + }, + "CURRENCY": { + "MONETARY_MASS_TITLE": "Evolution of the monetary mass", + "MONETARY_MASS_LABEL": "Monetary mass", + "MONETARY_MASS_SHARE_LABEL": "Average per member", + "UD_TITLE": "Evolution of the universal dividend", + "MEMBERS_COUNT_TITLE": "Evolution of the number of members", + "MEMBERS_COUNT_LABEL": "Number of members", + "MEMBERS_DELTA_TITLE": "variation in the number of members", + "IS_MEMBER_DELTA_LABEL": "Validated memberships", + "WAS_MEMBER_DELTA_LABEL": "Membership losses", + "PENDING_DELTA_LABEL": "Membership requests" + } } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index fcdf4511ce70cc6d7e73ffb1442846fb91506df6..34953d6aca08b1d73c5ba51e9380fbc69970bcca 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -949,5 +949,22 @@ "END_NOT_LOGIN": "This guided visit has <b>ended</b>.<br/><br/>If you wish to join the currency {{currency|capitalize}}, simply click <b>{{'LOGIN.CREATE_FREE_ACCOUNT'|translate}}</b> below.", "END_READONLY": "This guided visit has <b>ended</b>.<br/><br/>{{'MODE.READONLY.INSTALL_HELP'|translate}}." } + }, + "GRAPH": { + "COMMON": { + "LOGARITHMIC_SCALE": "Logarithmic scale" + }, + "CURRENCY": { + "MONETARY_MASS_TITLE": "Evolution of the monetary mass", + "MONETARY_MASS_LABEL": "Monetary mass", + "MONETARY_MASS_SHARE_LABEL": "Average per member", + "UD_TITLE": "Evolution of the universal dividend", + "MEMBERS_COUNT_TITLE": "Evolution of the number of members", + "MEMBERS_COUNT_LABEL": "Number of members", + "MEMBERS_DELTA_TITLE": "variation in the number of members", + "IS_MEMBER_DELTA_LABEL": "Validated memberships", + "WAS_MEMBER_DELTA_LABEL": "Membership losses", + "PENDING_DELTA_LABEL": "Membership requests" + } } } diff --git a/src/assets/i18n/eo-EO.json b/src/assets/i18n/eo-EO.json index 963cb589ba6ec417cfb39f4114c5c65621984b15..931fbd430c497c46c6437ba140165d6a332972d1 100644 --- a/src/assets/i18n/eo-EO.json +++ b/src/assets/i18n/eo-EO.json @@ -1020,5 +1020,22 @@ "EXAMPLE_BUTTON_ICON_G1_BLACK": "Insigno Ğ1 (nigra)" } } + }, + "GRAPH": { + "COMMON": { + "LOGARITHMIC_SCALE": "Logaritma skalo" + }, + "CURRENCY": { + "MONETARY_MASS_TITLE": "Evoluo de la mona maso", + "MONETARY_MASS_LABEL": "Mona maso", + "MONETARY_MASS_SHARE_LABEL": "Mezumo por membro", + "UD_TITLE": "Evoluo de la universala dividendo", + "MEMBERS_COUNT_TITLE": "Evoluo de la nombro de membroj", + "MEMBERS_COUNT_LABEL": "Nombro de membroj", + "MEMBERS_DELTA_TITLE": "Variado de la nombro de membroj", + "IS_MEMBER_DELTA_LABEL": "Novaj aliĝoj", + "WAS_MEMBER_DELTA_LABEL": "Aliĝo-perdoj", + "PENDING_DELTA_LABEL": "Aliĝo-petoj" + } } } diff --git a/src/assets/i18n/es-ES.json b/src/assets/i18n/es-ES.json index c1a3eb41496cbdef74fcc43348da50a6040ccc8f..3d5baed52a719f186d17bd739ecd694c7af391cb 100644 --- a/src/assets/i18n/es-ES.json +++ b/src/assets/i18n/es-ES.json @@ -996,5 +996,18 @@ "END_LOGIN": "¡La visita guiada ha <b>terminado</b>!<br/><br/>¡Buena suerte en este nuevo mundo de la <b>economía libre</b> !", "END_NOT_LOGIN": "¡La visita guiada ha <b>terminado</b>!<br/><br/>Si quiere utilizar la moneda {{currency|capitalize}}, tiene que hacer un clic en <b>{{'LOGIN.CREATE_ACCOUNT'|translate}}</b> más abajo." } + }, + "GRAPH": { + "COMMON": { + "LOGARITHMIC_SCALE": "Escala logarítmica" + }, + "CURRENCY": { + "MONETARY_MASS_TITLE": "Evolución de la masa monetaria", + "MONETARY_MASS_LABEL": "Masa monetaria", + "MONETARY_MASS_SHARE_LABEL": "Promedio miembro", + "UD_TITLE": "Evolución del dividendo universales", + "MEMBERS_COUNT_TITLE": "Evolución del número de miembros", + "MEMBERS_COUNT_LABEL": "Número de miembros" + } } } diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index 22e74e6be3a70592aec42e13cc0bbfa12d5c9b77..d592908946c22b177522b8d8b2b35ffc97a284f8 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -984,5 +984,22 @@ "END_NOT_LOGIN": "Cette visite guidée est <b>terminée</b> !<br/><br/>Si vous souhaitez rejoindre la monnaie {{currency|capitalize}}, il vous suffira de cliquer sur <b>{{'LOGIN.CREATE_FREE_ACCOUNT'|translate}}</b> ci-dessous.", "END_READONLY": "Cette visite guidée est <b>terminée</b>.<br/><br/>{{'MODE.READONLY.INSTALL_HELP'|translate}}." } + }, + "GRAPH": { + "COMMON": { + "LOGARITHMIC_SCALE" : "Echelle logarithmique" + }, + "CURRENCY": { + "MONETARY_MASS_TITLE": "Evolution de la masse monétaire", + "MONETARY_MASS_LABEL": "Masse monétaire", + "MONETARY_MASS_SHARE_LABEL": "Moyenne par membre", + "UD_TITLE": "Evolution du dividende universel", + "MEMBERS_COUNT_TITLE": "Evolution du nombre de membres", + "MEMBERS_COUNT_LABEL": "Nombre de membres", + "MEMBERS_DELTA_TITLE": "Variation du nombre de membres", + "IS_MEMBER_DELTA_LABEL": "Prises en compte d'adhésion", + "WAS_MEMBER_DELTA_LABEL": "Pertes d'adhésion", + "PENDING_DELTA_LABEL": "Demandes d'adhésion" + } } } diff --git a/src/assets/i18n/it-IT.json b/src/assets/i18n/it-IT.json index 74727b91dfa998e962618ccdd9ffbb6b8005c97b..9be3d6e706f23f437cdc6cdfe23c188267466a5a 100644 --- a/src/assets/i18n/it-IT.json +++ b/src/assets/i18n/it-IT.json @@ -932,5 +932,18 @@ "EXAMPLE_BUTTON_ICON_G1_BLACK": "Logo Ğ1 (nero)" } } + }, + "GRAPH": { + "COMMON": { + "LOGARITHMIC_SCALE": "Scala logaritmica" + }, + "CURRENCY": { + "MONETARY_MASS_TITLE": "Evoluzione della massa monetaria", + "MONETARY_MASS_LABEL": "Massa monetaria", + "MONETARY_MASS_SHARE_LABEL": "Media a membro", + "UD_TITLE": "Evoluzione del Dividendo Universale", + "MEMBERS_COUNT_TITLE": "Evoluzione del numero di membri", + "MEMBERS_COUNT_LABEL": "Numero di membri" + } } } diff --git a/src/assets/i18n/nl-NL.json b/src/assets/i18n/nl-NL.json index 55edee2dffdc571f4b85627db6caca05155fa4f2..5c707d903c5373fdfdd423eb0cf1ef77cb67173d 100644 --- a/src/assets/i18n/nl-NL.json +++ b/src/assets/i18n/nl-NL.json @@ -577,5 +577,18 @@ "END_LOGIN": "This guided visit has <b>ended</b>.<br/><br/>Welcome to the <b>free economy</b>!", "END_NOT_LOGIN": "This guided visit has <b>ended</b>.<br/><br/>If you wish to join the currency {{currency|capitalize}}, simply click <b>{{'LOGIN.CREATE_ACCOUNT'|translate}}</b> below." } + }, + "GRAPH": { + "COMMON": { + "LOGARITHMIC_SCALE": "Logaritmische schaal" + }, + "CURRENCY": { + "MONETARY_MASS_TITLE": "Evolutie van de monetaire massa", + "MONETARY_MASS_LABEL": "Monetaire massa", + "MONETARY_MASS_SHARE_LABEL": "Gemiddelde leden", + "UD_TITLE": "Ontwikkeling van de universele dividend", + "MEMBERS_COUNT_TITLE": "Evolutie van het aantal leden", + "MEMBERS_COUNT_LABEL": "Aantal leden" + } } }