mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
Merge pull request #6370 from mempool/knorrium/fix_fiat
Fix fiat precision across the app
This commit is contained in:
commit
5ec48f7f68
4 changed files with 339 additions and 25 deletions
|
|
@ -83,22 +83,28 @@ describe('Calculator', () => {
|
|||
});
|
||||
|
||||
describe('bitcoin input updates fiat and sats', () => {
|
||||
it('updates fiat and sats when entering 0.5 BTC', () => {
|
||||
const expectedFiat = Math.round(MOCK_BTC_PRICE_USD * 0.5 * 100) / 100;
|
||||
cy.get('input[formControlName="bitcoin"]').clear().type('0.5');
|
||||
cy.get('input[formControlName="satoshis"]').invoke('val').should('equal', '50000000');
|
||||
it('updates fiat and sats when entering 0.33 BTC', () => {
|
||||
const expectedFiat = Math.round(MOCK_BTC_PRICE_USD * 0.33 * 100) / 100; // 40740.48 USD
|
||||
cy.get('input[formControlName="bitcoin"]').clear().type('0.33');
|
||||
cy.get('input[formControlName="satoshis"]').invoke('val').should('equal', '33000000');
|
||||
cy.get('input[formControlName="fiat"]').invoke('val').then((fiatVal) => {
|
||||
const fiat = parseFloat(String(fiatVal).replace(/,/g, ''));
|
||||
expect(fiat).to.equal(expectedFiat);
|
||||
});
|
||||
cy.get('.fiat-text').invoke('text').then((text) => {
|
||||
expect(text.trim()).to.equal('$40,740.48');
|
||||
});
|
||||
});
|
||||
|
||||
it('updates fiat and sats when entering 1 sat (0.00000001 BTC)', () => {
|
||||
const expectedFiat = (MOCK_BTC_PRICE_USD / 100_000_000 * 100 / 100).toFixed(8);
|
||||
const expectedFiat = (MOCK_BTC_PRICE_USD / 100_000_000 * 100 / 100);
|
||||
cy.get('input[formControlName="bitcoin"]').clear().type('0.00000001');
|
||||
cy.get('input[formControlName="satoshis"]').invoke('val').should('equal', '1');
|
||||
cy.get('input[formControlName="fiat"]').invoke('val').then((fiatVal) => {
|
||||
expect(String(fiatVal)).to.equal(expectedFiat);
|
||||
expect(String(fiatVal)).to.equal(String(expectedFiat));
|
||||
});
|
||||
cy.get('.fiat-text').invoke('text').then((text) => {
|
||||
expect(text.trim()).to.equal('$0.00');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -220,6 +226,9 @@ describe('Calculator', () => {
|
|||
cy.get('.symbol').invoke('text').then((text) => {
|
||||
expect(text.replace(/,/g, '')).to.include(String(MOCK_BTC_PRICE_JPY));
|
||||
});
|
||||
cy.get('.fiat-text').invoke('text').then((text) => {
|
||||
expect(text.trim()).to.equal('¥11,057,757');
|
||||
});
|
||||
});
|
||||
|
||||
it('displays 1 BTC with correct sats and fiat in JPY', () => {
|
||||
|
|
@ -234,13 +243,17 @@ describe('Calculator', () => {
|
|||
});
|
||||
|
||||
it('updates fiat and sats when entering 0.5 BTC in JPY', () => {
|
||||
const expectedFiat = Math.round(MOCK_BTC_PRICE_JPY * 0.5 * 100) / 100;
|
||||
const expectedFiat = Math.round(MOCK_BTC_PRICE_JPY * 0.5)
|
||||
cy.get('input[formControlName="bitcoin"]').clear().type('0.5');
|
||||
cy.get('input[formControlName="satoshis"]').invoke('val').should('equal', '50000000');
|
||||
cy.get('input[formControlName="fiat"]').invoke('val').then((fiatVal) => {
|
||||
const fiat = parseFloat(String(fiatVal).replace(/,/g, ''));
|
||||
expect(fiat).to.equal(expectedFiat);
|
||||
});
|
||||
cy.get('.fiat-text').invoke('text').then((text) => {
|
||||
expect(text.trim()).to.equal('¥5,528,879');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('updates BTC and sats when entering fiat value in JPY', () => {
|
||||
|
|
@ -258,13 +271,16 @@ describe('Calculator', () => {
|
|||
|
||||
it('updates BTC and fiat when entering 10000 sats in JPY', () => {
|
||||
const satsAmount = 10000;
|
||||
const expectedFiat = Math.round((satsAmount / 100_000_000) * MOCK_BTC_PRICE_JPY * 100) / 100;
|
||||
const expectedFiat = Math.round((satsAmount / 100_000_000) * MOCK_BTC_PRICE_JPY);
|
||||
cy.get('input[formControlName="satoshis"]').clear().type(String(satsAmount));
|
||||
cy.get('input[formControlName="bitcoin"]').invoke('val').should('equal', '0.00010000');
|
||||
cy.get('input[formControlName="fiat"]').invoke('val').then((fiatVal) => {
|
||||
const fiat = parseFloat(String(fiatVal).replace(/,/g, ''));
|
||||
expect(fiat).to.equal(expectedFiat);
|
||||
});
|
||||
cy.get('.fiat-text').invoke('text').then((text) => {
|
||||
expect(text.trim()).to.equal('¥1,106');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
275
frontend/cypress/e2e/mainnet/fiat-currency-formatting.spec.ts
Normal file
275
frontend/cypress/e2e/mainnet/fiat-currency-formatting.spec.ts
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
const baseModule = Cypress.env('BASE_MODULE');
|
||||
|
||||
// Helper to select "Fiat" from the BTC/sats/Fiat amount selector dropdown
|
||||
const selectFiatMode = () => {
|
||||
cy.get('app-amount-selector').first().scrollIntoView();
|
||||
cy.get('app-amount-selector select').first().select('fiat');
|
||||
};
|
||||
|
||||
// Helper to select currency from the fiat selector dropdown
|
||||
const selectCurrency = (currency: 'USD' | 'JPY') => {
|
||||
cy.get('app-fiat-selector').first().scrollIntoView();
|
||||
cy.get('app-fiat-selector').first().click();
|
||||
cy.get('app-fiat-selector select').first().select(currency);
|
||||
};
|
||||
|
||||
describe('Fiat Currency Formatting', () => {
|
||||
|
||||
if (baseModule === 'mempool') {
|
||||
|
||||
describe('Dashboard', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/');
|
||||
cy.waitForSkeletonGone();
|
||||
selectFiatMode();
|
||||
cy.get('.latest-transactions', { timeout: 10000 }).should('exist');
|
||||
cy.get('.table-cell-fiat', { timeout: 10000 }).should('have.length.at.least', 1);
|
||||
});
|
||||
|
||||
describe('USD formatting', () => {
|
||||
beforeEach(() => {
|
||||
selectCurrency('USD');
|
||||
});
|
||||
|
||||
it('displays USD values with correct currency symbol', () => {
|
||||
cy.get('.table-cell-fiat').eq(1).invoke('text').then((text) => {
|
||||
const trimmedText = text.trim();
|
||||
expect(trimmedText).to.include('$');
|
||||
});
|
||||
});
|
||||
|
||||
it('displays USD values with proper decimal format', () => {
|
||||
cy.get('.table-cell-fiat').eq(1).invoke('text').then((text) => {
|
||||
const trimmedText = text.trim();
|
||||
expect(trimmedText).to.match(/^\$[\d,]+(\.\d{2})?$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('JPY formatting', () => {
|
||||
beforeEach(() => {
|
||||
selectCurrency('JPY');
|
||||
cy.get('.table-cell-fiat').eq(1).should(($el) => {
|
||||
expect($el.text()).to.include('¥');
|
||||
});
|
||||
});
|
||||
|
||||
it('displays JPY values with yen symbol', () => {
|
||||
cy.get('.table-cell-fiat').eq(1).invoke('text').then((text) => {
|
||||
const trimmedText = text.trim();
|
||||
expect(trimmedText).to.include('¥');
|
||||
});
|
||||
});
|
||||
|
||||
it('displays JPY values without decimal places', () => {
|
||||
cy.get('.table-cell-fiat').eq(1).invoke('text').then((text) => {
|
||||
const trimmedText = text.trim();
|
||||
expect(trimmedText).to.not.match(/\.\d+$/);
|
||||
});
|
||||
});
|
||||
|
||||
it('formats all JPY values correctly without decimals', () => {
|
||||
cy.get('.table-cell-fiat').each(($el) => {
|
||||
const text = $el.text().trim();
|
||||
if (text.includes('¥')) {
|
||||
expect(text).to.not.match(/\.\d+/);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('currency switching', () => {
|
||||
it('correctly formats when switching from USD to JPY', () => {
|
||||
selectCurrency('USD');
|
||||
cy.get('.table-cell-fiat').eq(1).invoke('text').then((usdText) => {
|
||||
expect(usdText.trim()).to.include('$');
|
||||
});
|
||||
|
||||
selectCurrency('JPY');
|
||||
cy.get('.table-cell-fiat').eq(1).should(($el) => {
|
||||
const text = $el.text().trim();
|
||||
expect(text).to.include('¥');
|
||||
expect(text).to.not.match(/\.\d+$/);
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly formats when switching from JPY back to USD', () => {
|
||||
selectCurrency('JPY');
|
||||
cy.get('.table-cell-fiat').eq(1).should(($el) => {
|
||||
expect($el.text()).to.include('¥');
|
||||
});
|
||||
|
||||
selectCurrency('USD');
|
||||
cy.get('.table-cell-fiat').eq(1).should(($el) => {
|
||||
expect($el.text().trim()).to.include('$');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Transaction Page', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/tx/dd0faea1e9acd5bd812e5130c0912b62d6b63d04bac4558f2e07270ac613a8f2');
|
||||
cy.waitForSkeletonGone();
|
||||
});
|
||||
|
||||
it('displays USD fiat values with dollar symbol', () => {
|
||||
selectCurrency('USD');
|
||||
selectFiatMode();
|
||||
|
||||
cy.get('app-transaction-details .fiat').invoke('text').then((text) => {
|
||||
expect(text.trim()).to.include('$');
|
||||
});
|
||||
|
||||
cy.get('app-transactions-list .fiat', { timeout: 10000 }).first().invoke('text').then((text) => {
|
||||
expect(text.trim()).to.include('$');
|
||||
});
|
||||
});
|
||||
|
||||
it('displays JPY fiat values without decimals', () => {
|
||||
selectCurrency('JPY');
|
||||
selectFiatMode();
|
||||
|
||||
|
||||
cy.get('app-transaction-details .fiat').invoke('text').then((text) => {
|
||||
const trimmedText = text.trim();
|
||||
expect(trimmedText).to.include('¥');
|
||||
expect(trimmedText).to.not.match(/\.\d+/);
|
||||
});
|
||||
|
||||
cy.get('app-transactions-list .fiat').first().should(($el) => {
|
||||
const text = $el.text().trim();
|
||||
expect(text).to.include('¥');
|
||||
expect(text).to.not.match(/\.\d+/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Block Page', () => {
|
||||
beforeEach(() => {
|
||||
cy.viewport(1080, 1920); // force taller screen to avoid scrollbars
|
||||
cy.intercept('/api/v1/blocks/0').as('blocks-0');
|
||||
cy.intercept('/api/v1/blocks/10').as('blocks-10');
|
||||
cy.intercept('/api/v1/blocks/20').as('blocks-20');
|
||||
cy.intercept('/api/txs/outspends*').as('outspends');
|
||||
cy.visit('/block/100000');
|
||||
});
|
||||
|
||||
it('displays USD fiat values in block reward with dollar symbol', () => {
|
||||
cy.wait('@outspends').then(() => {
|
||||
cy.waitUntil(() => cy.get('.fiat').each(($el) => $el.is(':visible') && $el.text().trim().includes('$')));
|
||||
selectCurrency('USD');
|
||||
selectFiatMode();
|
||||
cy.get('.fiat').each(($el) => {
|
||||
expect($el.text().trim()).to.include('$');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('displays JPY fiat values without decimals', () => {
|
||||
cy.wait('@outspends').then(() => {
|
||||
cy.waitUntil(() => cy.get('.fiat').each(($el) => $el.is(':visible') && $el.text().trim().includes('$')));
|
||||
selectCurrency('JPY');
|
||||
selectFiatMode();
|
||||
cy.get('.fiat').each(($el) => {
|
||||
expect($el.text().trim()).to.include('¥');
|
||||
expect($el.text().trim()).to.not.match(/\.\d+/);
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('Address Page', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/address/1wizaAB16Wrua9V58uNvqktyq2LBLtYso');
|
||||
});
|
||||
|
||||
it('displays USD fiat values with dollar symbol', () => {
|
||||
selectCurrency('USD');
|
||||
selectFiatMode();
|
||||
cy.get('app-amount .fiat', { timeout: 10000 }).each(($el) => {
|
||||
expect($el.text().trim()).to.include('$');
|
||||
});
|
||||
});
|
||||
|
||||
it('displays JPY fiat values without decimals', () => {
|
||||
selectCurrency('JPY');
|
||||
selectFiatMode();
|
||||
cy.get('app-amount .fiat').first().should(($el) => {
|
||||
const text = $el.text().trim();
|
||||
expect(text).to.include('¥');
|
||||
expect(text).to.not.match(/\.\d+/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Calculator Page', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/tools/calculator');
|
||||
cy.waitForSkeletonGone();
|
||||
cy.get('input[formControlName="bitcoin"]', { timeout: 10000 }).should('be.visible');
|
||||
});
|
||||
|
||||
it('displays USD price with dollar symbol', () => {
|
||||
selectCurrency('USD');
|
||||
cy.get('.symbol').invoke('text').then((text) => {
|
||||
expect(text.trim()).to.include('$');
|
||||
});
|
||||
});
|
||||
|
||||
it('displays JPY price without decimals after switching currency', () => {
|
||||
selectCurrency('JPY');
|
||||
cy.get('.symbol').should(($el) => {
|
||||
const text = $el.text().trim();
|
||||
expect(text).to.include('¥');
|
||||
});
|
||||
|
||||
// Enter a value in BTC to see the fiat conversion
|
||||
cy.get('input[formControlName="bitcoin"]').clear().type('1');
|
||||
|
||||
// Check that the fiat output doesn't have decimals for JPY
|
||||
cy.get('input[formControlName="fiat"]').should(($el) => {
|
||||
const value = $el.val() as string;
|
||||
// JPY should not have decimal places in large values
|
||||
expect(value).to.not.match(/\.\d+$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mempool Block Tooltip', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/');
|
||||
cy.waitForSkeletonGone();
|
||||
selectFiatMode();
|
||||
});
|
||||
|
||||
it('displays USD fiat values in mempool block', () => {
|
||||
selectCurrency('USD');
|
||||
cy.get('#mempool-block-0').scrollIntoView();
|
||||
cy.get('#mempool-block-0').click();
|
||||
|
||||
cy.waitForSkeletonGone();
|
||||
|
||||
cy.get('app-mempool-block .fiat').each(($el) => {
|
||||
expect($el.text().trim()).to.include('$');
|
||||
});
|
||||
});
|
||||
|
||||
it('displays JPY fiat values in mempool block', () => {
|
||||
selectCurrency('JPY');
|
||||
cy.get('#mempool-block-0').scrollIntoView();
|
||||
cy.get('#mempool-block-0').click();
|
||||
|
||||
cy.waitForSkeletonGone();
|
||||
|
||||
cy.get('app-mempool-block .fiat').invoke('text').then((text) => {
|
||||
expect(text.trim()).to.include('¥');
|
||||
expect(text).to.not.match(/\.\d+$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
|
|
@ -20,12 +20,15 @@ export class CalculatorComponent implements OnInit {
|
|||
form: FormGroup;
|
||||
currentPrice = 0;
|
||||
isMaxSupply = false;
|
||||
currentCurrency = 'USD';
|
||||
currencyDecimals = 2;
|
||||
|
||||
currency$ = this.stateService.fiatCurrency$;
|
||||
price$: Observable<number>;
|
||||
lastFiatPrice$: Observable<number>;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
private stateService: StateService,
|
||||
private formBuilder: FormBuilder,
|
||||
private websocketService: WebsocketService,
|
||||
|
|
@ -47,6 +50,8 @@ export class CalculatorComponent implements OnInit {
|
|||
this.price$ = this.currency$.pipe(
|
||||
switchMap((result) => {
|
||||
currency = result;
|
||||
this.currentCurrency = result;
|
||||
this.updateCurrencyDecimals();
|
||||
return this.stateService.conversions$.asObservable();
|
||||
}),
|
||||
map((conversions) => {
|
||||
|
|
@ -132,8 +137,11 @@ export class CalculatorComponent implements OnInit {
|
|||
if (name === 'bitcoin' && this.countDecimals(sanitizedValue) > 8) {
|
||||
sanitizedValue = this.toFixedWithoutRounding(sanitizedValue, 8);
|
||||
}
|
||||
if (name === 'fiat' && this.countDecimals(sanitizedValue) > 2) {
|
||||
sanitizedValue = this.toFixedWithoutRounding(sanitizedValue, 2);
|
||||
if (name === 'fiat') {
|
||||
const decimals = this.getCurrencyDecimals();
|
||||
if (this.countDecimals(sanitizedValue) > decimals) {
|
||||
sanitizedValue = this.toFixedWithoutRounding(sanitizedValue, decimals);
|
||||
}
|
||||
}
|
||||
if (sanitizedValue === '') {
|
||||
sanitizedValue = '0';
|
||||
|
|
@ -176,12 +184,20 @@ export class CalculatorComponent implements OnInit {
|
|||
}
|
||||
|
||||
formatFiat(num: number): string | number {
|
||||
// Get the number of decimal places for the current currency
|
||||
const decimals = this.getCurrencyDecimals();
|
||||
|
||||
if (decimals === 0) {
|
||||
return Math.round(num);
|
||||
}
|
||||
|
||||
if (Math.abs(num) >= 1000) {
|
||||
// For values >= 1000: show 2 decimals, or 0 if whole number
|
||||
// For values >= 1000: show currency-specific decimals, or 0 if whole number
|
||||
if (num % 1 === 0) {
|
||||
return Math.round(num);
|
||||
}
|
||||
return (Math.round(num * 100) / 100).toFixed(2);
|
||||
const factor = Math.pow(10, decimals);
|
||||
return (Math.round(num * factor) / factor).toFixed(decimals);
|
||||
}
|
||||
if (num % 1 === 0) {
|
||||
return Math.round(num);
|
||||
|
|
@ -190,6 +206,23 @@ export class CalculatorComponent implements OnInit {
|
|||
if (Math.abs(num) < 1 && num !== 0) {
|
||||
return num.toFixed(8);
|
||||
}
|
||||
return (Math.round(num * 100) / 100).toFixed(2);
|
||||
const factor = Math.pow(10, decimals);
|
||||
return (Math.round(num * factor) / factor).toFixed(decimals);
|
||||
}
|
||||
|
||||
private updateCurrencyDecimals(): void {
|
||||
try {
|
||||
const formatter = new Intl.NumberFormat(this.locale, {
|
||||
style: 'currency',
|
||||
currency: this.currentCurrency
|
||||
});
|
||||
this.currencyDecimals = formatter.resolvedOptions().maximumFractionDigits;
|
||||
} catch {
|
||||
this.currencyDecimals = 2; // Default to 2 decimal places
|
||||
}
|
||||
}
|
||||
|
||||
getCurrencyDecimals(): number {
|
||||
return this.currencyDecimals;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,17 +21,7 @@ export class FiatCurrencyPipe implements PipeTransform {
|
|||
}
|
||||
|
||||
transform(num: number, ...args: any[]): unknown {
|
||||
const digits = args[0] || 1;
|
||||
const currency = args[1] || this.currency || 'USD';
|
||||
|
||||
if (Math.abs(num) >= 1000) {
|
||||
// Check if decimals are exactly 0
|
||||
if (num % 1 === 0) {
|
||||
return new Intl.NumberFormat(this.locale, { style: 'currency', currency, maximumFractionDigits: 0 }).format(num);
|
||||
}
|
||||
return new Intl.NumberFormat(this.locale, { style: 'currency', currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(num);
|
||||
} else {
|
||||
return new Intl.NumberFormat(this.locale, { style: 'currency', currency }).format(num);
|
||||
}
|
||||
return new Intl.NumberFormat(this.locale, { style: 'currency', currency }).format(num);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue