diff --git a/src/app/block/block.page.html b/src/app/block/block.page.html index 276435bbcdda23becc810a52d5df52a900e1590c..a162a5832890bfcc735bf037085dc8089fa17a25 100644 --- a/src/app/block/block.page.html +++ b/src/app/block/block.page.html @@ -15,7 +15,7 @@ <ion-header collapse="condense"> <ion-toolbar> <ion-title size="large"> - {{ 'BLOCKCHAIN.VIEW.TITLE' | translate: { number: height$ | push | numberFormat } }} + {{ 'BLOCKCHAIN.VIEW.TITLE' | translate: { number: height$ | push | blockNumber } }} </ion-title> </ion-toolbar> </ion-header> @@ -37,13 +37,13 @@ @if (mobile) { <ion-label> <h2 translate>BLOCKCHAIN.VIEW.HASH</h2> - <p class="ion-text-wrap">{{ block.height }}-{{ block.hash }}</p> + <p class="ion-text-wrap">{{ block.height | blockNumber: { allowSuffix: false, useGrouping: false } }}-{{ block.hash }}</p> </ion-label> } @else { <ion-label> <h2 translate>BLOCKCHAIN.VIEW.HASH</h2> + <p class="ion-text-wrap">{{ block.height | blockNumber: { allowSuffix: false, useGrouping: false } }}-{{ block.hash }}</p> </ion-label> - <ion-note slot="end" class="ion-text-wrap">{{ block.height }}-{{ block.hash }}</ion-note> } </ion-item> </ion-list> diff --git a/src/app/certification/history/cert-history.page.html b/src/app/certification/history/cert-history.page.html index 1130238d4be2f92062ce5ce0a9562e1cbb15908a..1d6c0f413f868b2b2ea7133bae5b426cd2ece451 100644 --- a/src/app/certification/history/cert-history.page.html +++ b/src/app/certification/history/cert-history.page.html @@ -62,7 +62,10 @@ <ion-icon name="key"></ion-icon> {{ item.account.address | addressToPubkeyV1 | pubkeyFormat: true }} | <a [routerLink]="['/block', item.createdOn]" routerDirection="forward" (click)="$event.preventDefault()" class="tx-timestamp"> - {{ item.updatedOn | blockTime | dateFormat }} | {{ 'COMMON.BLOCK' | translate }} #{{ item.updatedOn | blockNumber }} + @if (item.updatedOn | blockTime | dateFormat; as blockTime) { + {{ blockTime }} | + } + {{ 'COMMON.BLOCK' | translate }} #{{ item.updatedOn | blockNumber }} </a> </p> </ion-label> diff --git a/src/app/currency/currency.page.html b/src/app/currency/currency.page.html index 458158a3db41bb823cf1d9c1b430dc244796986f..94f24c4650eece85ad54270889cfc32551bdb165 100644 --- a/src/app/currency/currency.page.html +++ b/src/app/currency/currency.page.html @@ -18,7 +18,7 @@ <ion-grid class="ion-no-padding"> <ion-row> <ion-col size="0" size-md="2" size-lg="3"></ion-col> - <ion-col> + <ion-col size="12" size-md="8" size-lg="6"> <ion-list *rxIf="loaded$; else listSkeleton" lines="none"> <ion-item-divider translate>CURRENCY.VIEW.TITLE</ion-item-divider> @@ -60,7 +60,7 @@ <ion-icon slot="start" name="reload"></ion-icon> <ion-label color="dark" translate>CURRENCY.VIEW.UD</ion-label> <ion-badge color="tertiary"> - {{ params.unitsPerUd }} + {{ params.currentUd }} <span [innerHtml]="params.currencySymbol"></span> / {{ params.udCreationPeriodMs | duration: 'ms' }} diff --git a/src/app/currency/currency.page.ts b/src/app/currency/currency.page.ts index 74434ad3374b71ad6e4c72ac58d8bd1b573db25a..31743500a95f0b22ba5260d5f7f9801d83b43dac 100644 --- a/src/app/currency/currency.page.ts +++ b/src/app/currency/currency.page.ts @@ -3,13 +3,12 @@ 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 { u32, u64 } from '@polkadot/types'; import { RxState } from '@rx-angular/state'; import { RxStateProperty } 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 } from '@app/shared/functions'; +import { toBoolean, toNumber } from '@app/shared/functions'; export interface CurrencyParameters { currencyName: string; @@ -17,7 +16,8 @@ export interface CurrencyParameters { currencySymbol: SafeHtml; members: number; monetaryMass: number; - unitsPerUd: number; + currentUd: number; + ud0: number; udCreationPeriodMs: number; udReevalPeriodMs: number; growthRate: number; @@ -69,34 +69,38 @@ export class CurrencyPage extends AppPage<CurrencyPageState> { const network = await this.networkService.ready(); const api = network.api; const currency = network.currency; - const fractionsPerUnit = Math.pow(10, currency.decimals); - const [monetaryMassFractions, members] = await Promise.all([ + const powBase = Math.pow(10, currency.decimals); + const [monetaryMassFractions, currentUd, members] = await Promise.all([ api.query.universalDividend.monetaryMass(), + api.query.universalDividend.currentUd(), api.query.membership.counterForMembership(), ]); - const duValue = (api.consts.universalDividend.unitsPerUd as u64).toNumber() / fractionsPerUnit; - const growthRate = Math.sqrt((api.consts.universalDividend.squareMoneyGrowthRate as u64).toNumber()); + const ud0 = toNumber(api.consts.universalDividend.unitsPerUd) / powBase; + const ud = toNumber(currentUd) / powBase; + const growthRate = Math.sqrt(toNumber(api.consts.universalDividend.squareMoneyGrowthRate)); const paramsByUnit = new Map<CurrencyDisplayUnit, CurrencyParameters>(); paramsByUnit.set('base', { currencyName: currency.displayName, currencySymbol: currency.symbol, currencyNetwork: currency.network, - monetaryMass: (monetaryMassFractions as u64).toNumber() / fractionsPerUnit, - members: (members as u32).toNumber(), - unitsPerUd: duValue, - udCreationPeriodMs: (api.consts.universalDividend.udCreationPeriod as u64).toNumber(), - udReevalPeriodMs: (api.consts.universalDividend.udReevalPeriod as u64).toNumber(), + monetaryMass: toNumber(monetaryMassFractions) / powBase, + members: toNumber(members), + currentUd: ud, + ud0, + udCreationPeriodMs: toNumber(api.consts.universalDividend.udCreationPeriod), + udReevalPeriodMs: toNumber(api.consts.universalDividend.udReevalPeriod), growthRate, }); paramsByUnit.set('du', { currencyName: currency.displayName, currencySymbol: this.sanitizer.bypassSecurityTrustHtml(`${this.translate.instant('COMMON.UD')}<sub>${currency.symbol}</sub>`), currencyNetwork: currency.network, - monetaryMass: (monetaryMassFractions as u64).toNumber() / fractionsPerUnit / duValue, - members: (members as u32).toNumber(), - unitsPerUd: 1, - udCreationPeriodMs: (api.consts.universalDividend.udCreationPeriod as u64).toNumber(), - udReevalPeriodMs: (api.consts.universalDividend.udReevalPeriod as u64).toNumber(), + monetaryMass: toNumber(monetaryMassFractions) / powBase / ud, + members: toNumber(members), + currentUd: 1, + ud0, + udCreationPeriodMs: toNumber(api.consts.universalDividend.udCreationPeriod), + udReevalPeriodMs: toNumber(api.consts.universalDividend.udReevalPeriod), growthRate, }); diff --git a/src/app/network/network.service.ts b/src/app/network/network.service.ts index 57be88c1e7db80a203f2f54d2d4dcdaa6bce924d..1b026570f9fe2b6869a6c117e8de859094795f29 100644 --- a/src/app/network/network.service.ts +++ b/src/app/network/network.service.ts @@ -6,9 +6,9 @@ import { abbreviate, WELL_KNOWN_CURRENCIES } from '@app/shared/currencies'; import { Currency } from '../currency/currency.model'; import { RxStartableService } from '@app/shared/services/rx-startable-service.class'; import { RxStateProperty, RxStateSelect } from '@app/shared/decorator/state.decorator'; -import { Observable } from 'rxjs'; +import { mergeMap, Observable, tap } from 'rxjs'; import { filter, map } from 'rxjs/operators'; -import { arrayRandomPick, isNotNilOrBlank } from '@app/shared/functions'; +import { arrayRandomPick, isNotNilOrBlank, toNumber } from '@app/shared/functions'; import { IndexerService } from './indexer.service'; import { fromDateISOString } from '@app/shared/dates'; import { ContextService } from '@app/shared/services/storage/context.service'; @@ -17,6 +17,7 @@ export interface NetworkState { peer: Peer; currency: Currency; currencySymbol: string; + currentUd?: number; api: ApiPromise; } @@ -27,10 +28,12 @@ export class NetworkService extends RxStartableService<NetworkState> { @RxStateProperty() peer: Peer; @RxStateProperty() currency: Currency; @RxStateProperty() currencySymbol: string; + @RxStateProperty() currentUd: number; @RxStateProperty() api: ApiPromise; @RxStateSelect() peer$: Observable<Peer>; @RxStateSelect() currency$: Observable<Currency>; + @RxStateSelect() currentUd$: Observable<number>; constructor( private settings: SettingsService, @@ -53,6 +56,17 @@ export class NetworkService extends RxStartableService<NetworkState> { ); this.hold(this.currency$, (currency) => (this.context.currency = currency)); + + this.connect( + 'currentUd', + this.select('currency').pipe( + mergeMap(async (currency) => { + const ud = await this.api.query.universalDividend.currentUd(); + return toNumber(ud) / currency.powBase; + }), + tap((currentUd) => console.info(`${this._logPrefix}Current UD: ${currentUd}`)) + ) + ); } protected async ngOnStart(): Promise<NetworkState> { @@ -131,14 +145,17 @@ export class NetworkService extends RxStartableService<NetworkState> { const lastHeader = await api.rpc.chain.getHeader(); console.info(`${this._logPrefix}Last block: #${lastHeader.number} - hash ${lastHeader.hash}`); + const ud0 = toNumber(api.consts.universalDividend.unitsPerUd) / currency.powBase; + this.indexer.currency = currency; await this.indexer.start(); return { + api, peer, currency, currencySymbol: currency?.symbol, - api, + currentUd: ud0, }; } diff --git a/src/app/settings/settings.service.ts b/src/app/settings/settings.service.ts index 0afe7c674456df332ed53aff92394a38637fb8cc..ae9578bacdeaf9fbad1fc6c761ce1950519e6f81 100644 --- a/src/app/settings/settings.service.ts +++ b/src/app/settings/settings.service.ts @@ -2,29 +2,39 @@ import { Inject, Injectable, Optional } from '@angular/core'; import { CurrencyDisplayUnit, Settings } from './settings.model'; import { environment } from '@environments/environment'; import { Platform } from '@ionic/angular'; -import { Observable, Subject } from 'rxjs'; +import { debounceTime, Observable, Subject } from 'rxjs'; import { APP_STORAGE, IStorage } from '@app/shared/services/storage/storage.utils'; import { RxStartableService } from '@app/shared/services/rx-startable-service.class'; import { setTimeout } from '@rx-angular/cdk/zone-less/browser'; import { arrayDistinct } from '@app/shared/functions'; import { RxStateProperty, RxStateSelect } from '@app/shared/decorator/state.decorator'; import { isMobile } from '@app/shared/platforms'; +import { distinctUntilChanged, map } from 'rxjs/operators'; const SETTINGS_STORAGE_KEY = 'settings'; +export interface SettingsState extends Settings { + localesArgument?: Intl.LocalesArgument; + numberFormatOptions?: Intl.NumberFormatOptions; +} + @Injectable({ providedIn: 'root' }) -export class SettingsService extends RxStartableService<Settings> { - changes = new Subject<Settings>(); +export class SettingsService extends RxStartableService<SettingsState> { + changes = new Subject<SettingsState>(); get mobile() { return this.get('mobile'); } + @RxStateSelect() locale$: Observable<string>; @RxStateSelect() darkMode$: Observable<boolean>; @RxStateSelect() peer$: Observable<string>; @RxStateSelect() indexer$: Observable<string>; @RxStateSelect() displayUnit$: Observable<CurrencyDisplayUnit>; + @RxStateProperty() locale: string; + @RxStateProperty() localesArgument: Intl.LocalesArgument; + @RxStateProperty() numberFormatOptions: Intl.NumberFormatOptions; @RxStateProperty() darkMode: boolean; @RxStateProperty() peer: string; @RxStateProperty() indexer: string; @@ -43,7 +53,13 @@ export class SettingsService extends RxStartableService<Settings> { }); // Emit changes event - this.hold(this.$, (value) => this.changes.next(value)); + this.hold(this.$.pipe(debounceTime(100), distinctUntilChanged()), (value) => this.changes.next(value)); + + // Compute number options + this.connect('numberFormatOptions', this.locale$.pipe(map((locale) => this.getNumberFormatOptions(locale)))); + + // Compute number options + //this.connect('localesArgument', this.locale$.pipe(map((locale) => this.getLocalesArgument(locale)))); } protected async ngOnStart(): Promise<Settings> { @@ -100,4 +116,15 @@ export class SettingsService extends RxStartableService<Settings> { const data = this.clone(); await this.storage.set('settings', data); } + + private getNumberFormatOptions(locale: string): Intl.NumberFormatOptions { + const defaultOptions: Intl.NumberFormatOptions = { useGrouping: true, maximumFractionDigits: 3 }; + switch (locale) { + case 'fr-FR': + return <Intl.NumberFormatOptions>{ ...defaultOptions }; + case 'en-US': + case 'en-GB': + return defaultOptions; + } + } } diff --git a/src/app/shared/functions.ts b/src/app/shared/functions.ts index ae7956658e0c6ab7aaa5543b240a83d849d282e4..a61324f76a7dbc08940986ea3b4589a8abbb4eeb 100644 --- a/src/app/shared/functions.ts +++ b/src/app/shared/functions.ts @@ -1,5 +1,7 @@ import { KeysEnum, KeyValueType } from '@app/shared/types'; import { setTimeout } from '@rx-angular/cdk/zone-less/browser'; +import { u32, u64 } from '@polkadot/types-codec'; +import { Codec } from '@polkadot/types-codec/types'; export function isNil<T>(obj: T | null | undefined): boolean { return obj === undefined || obj === null; @@ -77,7 +79,9 @@ export function trimEmptyToNull(str: string | null | undefined): string | null { export function toBoolean(obj: boolean | null | undefined | string, defaultValue?: boolean): boolean { return obj !== undefined && obj !== null ? (obj !== 'false' ? !!obj : false) : defaultValue; } -export function toNumber(obj: number | null | undefined, defaultValue?: number): number { +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(); return obj !== undefined && obj !== null ? +obj : defaultValue; } export function toFloat(obj: string | null | undefined, defaultValue?: number): number | null { diff --git a/src/app/shared/pipes/amount.pipe.ts b/src/app/shared/pipes/amount.pipe.ts index d91c857f23c156f4f5d0410e9e464e039457f771..a32905ee4703736c0ed4595c0ecc332bc1b90de6 100644 --- a/src/app/shared/pipes/amount.pipe.ts +++ b/src/app/shared/pipes/amount.pipe.ts @@ -1,10 +1,9 @@ import { Pipe, PipeTransform } from '@angular/core'; import { NumberFormatPipe } from '@app/shared/pipes/number-format.pipe'; import { NetworkService } from '@app/network/network.service'; -import { isNil } from '@app/shared/functions'; +import { isNilOrNaN } from '@app/shared/functions'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { SettingsService } from '@app/settings/settings.service'; -import { u64 } from '@polkadot/types'; import { TranslateService } from '@ngx-translate/core'; @Pipe({ @@ -14,7 +13,7 @@ export class AmountFormatPipe extends NumberFormatPipe implements PipeTransform private currencySymbol = this.networkService.currency?.symbol; private powBase = this.networkService.currency?.powBase; private decimals = this.networkService.currency?.decimals; - private udValue: number; + private currentUd: number = this.networkService.currentUd; constructor( private networkService: NetworkService, private settings: SettingsService, @@ -22,26 +21,25 @@ export class AmountFormatPipe extends NumberFormatPipe implements PipeTransform private sanitizer: DomSanitizer ) { super(); - this.udValue = (networkService.api.consts.universalDividend.unitsPerUd as u64).toNumber(); } - transform(val: number, opts?: Intl.NumberFormatOptions & { fixedDecimals?: number; html?: boolean }): SafeHtml { - if (isNil(val)) return ''; + transform(amount: number, opts?: Intl.NumberFormatOptions & { fixedDecimals?: number; html?: boolean }): SafeHtml { + if (isNilOrNaN(amount)) return ''; switch (this.settings.displayUnit) { case 'du': { if (opts?.html === false) { return ( - super.transform(val / this.udValue / this.powBase, { fixedDecimals: this.decimals, ...opts }) + + super.transform((amount / this.powBase) | this.currentUd, { fixedDecimals: this.decimals + 1, ...opts }) + ` ${this.translate.instant('COMMON.UD')}(${this.currencySymbol})` ); } return this.sanitizer.bypassSecurityTrustHtml( - super.transform(val / this.udValue / this.powBase, { fixedDecimals: this.decimals, ...opts }) + + super.transform(amount / this.powBase / this.currentUd, { fixedDecimals: this.decimals + 1, ...opts }) + ` ${this.translate.instant('COMMON.UD')}<sub>${this.currencySymbol}</sub>` ); } default: - return super.transform(val / this.powBase, { fixedDecimals: this.decimals, ...opts }) + (' ' + this.currencySymbol); + return super.transform(amount / this.powBase, { fixedDecimals: this.decimals, ...opts }) + (' ' + this.currencySymbol); } } } diff --git a/src/app/shared/pipes/block-number.pipe.ts b/src/app/shared/pipes/block-number.pipe.ts index e2a910482bf7432c73e48ecbe857c45d8b96b7ad..6d6cd1f40fa10e6f6ebf1649528dab965513c696 100644 --- a/src/app/shared/pipes/block-number.pipe.ts +++ b/src/app/shared/pipes/block-number.pipe.ts @@ -1,24 +1,28 @@ import { Pipe, PipeTransform } from '@angular/core'; import { IndexerService } from '@app/network/indexer.service'; import { isNil } from '@app/shared/functions'; +import { SettingsService } from '@app/settings/settings.service'; @Pipe({ name: 'blockNumber', }) export class BlockNumberPipe implements PipeTransform { - constructor(private indexer: IndexerService) {} + constructor( + private indexer: IndexerService, + private settings: SettingsService + ) {} - transform(blockNumber: number, suffixV1 = true): string { + transform(blockNumber: number, opts?: { allowSuffix: boolean } & Intl.NumberFormatOptions): string { if (isNil(blockNumber)) return null; // Convert V1 block number (cf CRR https://pad.p2p.legal/Visio_2024-04-29) if (blockNumber < 0 && this.indexer.minBlockHeight) { blockNumber = -1 * this.indexer.minBlockHeight + blockNumber; - if (suffixV1) { - return `${blockNumber.toString()} (v1)`; + if (opts?.allowSuffix !== false) { + return `${blockNumber.toLocaleString(this.settings.locale, { ...this.settings.numberFormatOptions, ...opts })} (v1)`; } } - return blockNumber.toString(); + return blockNumber.toLocaleString(this.settings.locale, { ...this.settings.numberFormatOptions, ...opts }); } } diff --git a/src/app/shared/pipes/block-timestamp.pipe.ts b/src/app/shared/pipes/block-timestamp.pipe.ts index 0465eba94461b52072d9f94f22867a2872e40330..5b4009245e537e6709400a30302301a372d31f86 100644 --- a/src/app/shared/pipes/block-timestamp.pipe.ts +++ b/src/app/shared/pipes/block-timestamp.pipe.ts @@ -14,8 +14,8 @@ export class BlockTimePipe implements PipeTransform { private indexer: IndexerService ) {} - transform(blockNumber: number): Moment { - if (isNil(blockNumber)) return null; + transform(blockNumber: number, defaultValue?: Moment): Moment { + if (isNil(blockNumber)) return defaultValue; const startTime = DateUtils.fromDateISOString(this.networkService.currency.startTime); @@ -30,6 +30,8 @@ export class BlockTimePipe implements PipeTransform { return null; } else { + // TODO: estimate only for future date + // TODO: get from network service const blockDuration = 6; const duration = DateUtils.toDuration(blockNumber * blockDuration, 'seconds');