mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
Merge pull request #6259 from mempool/knorrium/calc_precision
🧮 Calculator improvements
This commit is contained in:
commit
e22902f861
6 changed files with 378 additions and 20 deletions
274
frontend/cypress/e2e/mainnet/calculator.spec.ts
Normal file
274
frontend/cypress/e2e/mainnet/calculator.spec.ts
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
import { emitMempoolInfo, receiveWebSocketMessageFromServer } from '../../support/websocket';
|
||||
|
||||
const calculatorBaseModule = Cypress.env('BASE_MODULE');
|
||||
|
||||
const MOCK_BTC_PRICE_USD = 123456;
|
||||
const MOCK_BTC_PRICE_JPY = 11057757;
|
||||
|
||||
describe('Calculator', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockMempoolSocketV2();
|
||||
cy.visit('/tools/calculator');
|
||||
|
||||
emitMempoolInfo({
|
||||
params: {
|
||||
command: 'init',
|
||||
waitForMempoolBlocks: false
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('input[formControlName="bitcoin"]', { timeout: 15000 }).should('be.visible');
|
||||
|
||||
receiveWebSocketMessageFromServer({
|
||||
params: {
|
||||
message: {
|
||||
contents: `{"conversions": { "time": 1770429602, "USD": ${MOCK_BTC_PRICE_USD}, "EUR": 59711, "GBP": 51810, "CAD": 96567, "CHF": 54834, "AUD": 100897, "JPY": ${MOCK_BTC_PRICE_JPY} }}`
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('.symbol', { timeout: 10000 }).should(($el) => {
|
||||
expect($el.text().replace(/,/g, '')).to.include(String(MOCK_BTC_PRICE_USD));
|
||||
});
|
||||
});
|
||||
|
||||
if (calculatorBaseModule === 'mempool') {
|
||||
|
||||
describe('page load and initial state', () => {
|
||||
it('loads the calculator page with heading and form', () => {
|
||||
cy.get('h2').should('contain', 'Calculator');
|
||||
cy.contains('Waiting for price feed...').should('not.exist');
|
||||
cy.get('input[formControlName="fiat"]').should('be.visible');
|
||||
cy.get('input[formControlName="bitcoin"]').should('be.visible');
|
||||
cy.get('input[formControlName="satoshis"]').should('be.visible');
|
||||
});
|
||||
|
||||
it('displays the mocked conversion rate in .symbol', () => {
|
||||
cy.get('.symbol').invoke('text').then((text) => {
|
||||
expect(text.replace(/,/g, '')).to.include(String(MOCK_BTC_PRICE_USD));
|
||||
});
|
||||
});
|
||||
|
||||
it('shows copy buttons for each input', () => {
|
||||
cy.get('app-clipboard').should('have.length', 3);
|
||||
cy.get('app-clipboard').each(($el) => {
|
||||
cy.wrap($el).should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows fiat price display and bitcoin visual', () => {
|
||||
cy.contains('Fiat price last updated').should('be.visible');
|
||||
cy.get('.bitcoin-satoshis-text').should('be.visible');
|
||||
cy.get('.bitcoin-satoshis-text').should('contain', '₿');
|
||||
cy.get('.fiat-text').should('be.visible');
|
||||
});
|
||||
|
||||
it('shows input labels for currency, BTC, and sats', () => {
|
||||
cy.get('.input-group-text').contains('BTC').should('be.visible');
|
||||
cy.get('.input-group-text').contains('sats').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('default values', () => {
|
||||
it('displays 1 BTC with correct sats and fiat', () => {
|
||||
cy.get('input[formControlName="bitcoin"]').invoke('val').then((btcVal) => {
|
||||
expect(parseFloat(String(btcVal))).to.equal(1);
|
||||
});
|
||||
cy.get('input[formControlName="satoshis"]').invoke('val').should('equal', '100000000');
|
||||
cy.get('input[formControlName="fiat"]').invoke('val').then((fiatVal) => {
|
||||
const fiat = parseFloat(String(fiatVal).replace(/,/g, ''));
|
||||
expect(fiat).to.equal(MOCK_BTC_PRICE_USD);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
cy.get('input[formControlName="fiat"]').invoke('val').then((fiatVal) => {
|
||||
const fiat = parseFloat(String(fiatVal).replace(/,/g, ''));
|
||||
expect(fiat).to.equal(expectedFiat);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fiat input updates BTC and sats', () => {
|
||||
it('updates BTC and sats when entering fiat value', () => {
|
||||
const fiatAmount = 100;
|
||||
const expectedBtc = parseFloat((fiatAmount / MOCK_BTC_PRICE_USD).toFixed(8));
|
||||
const expectedSats = Math.round((fiatAmount / MOCK_BTC_PRICE_USD) * 100_000_000);
|
||||
cy.get('input[formControlName="fiat"]').clear().type(String(fiatAmount));
|
||||
cy.get('input[formControlName="bitcoin"]').invoke('val').then((btcVal) => {
|
||||
expect(parseFloat(String(btcVal))).to.equal(expectedBtc);
|
||||
});
|
||||
cy.get('input[formControlName="satoshis"]').invoke('val').then((satsVal) => {
|
||||
expect(parseInt(String(satsVal), 10)).to.equal(expectedSats);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('satoshis input updates BTC and fiat', () => {
|
||||
it('updates BTC and fiat when entering 10000 sats', () => {
|
||||
const satsAmount = 10000;
|
||||
const expectedFiat = Math.round((satsAmount / 100_000_000) * MOCK_BTC_PRICE_USD * 100) / 100;
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('input sanitization', () => {
|
||||
it('normalizes comma to dot in fiat input', () => {
|
||||
cy.get('input[formControlName="fiat"]').clear().type('1,5');
|
||||
cy.get('input[formControlName="fiat"]').invoke('val').then((val) => {
|
||||
expect(String(val)).to.match(/^1\.5/);
|
||||
});
|
||||
});
|
||||
|
||||
it('limits BTC to 8 decimals', () => {
|
||||
cy.get('input[formControlName="bitcoin"]').clear().type('1.123456789');
|
||||
cy.get('input[formControlName="bitcoin"]').invoke('val').then((val) => {
|
||||
const parts = String(val).split('.');
|
||||
expect(parts.length).to.be.lte(2);
|
||||
if (parts[1]) {
|
||||
expect(parts[1].length).to.be.lte(8);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('strips decimals from satoshis input', () => {
|
||||
cy.get('input[formControlName="satoshis"]').clear().type('10000.99');
|
||||
cy.get('input[formControlName="satoshis"]').invoke('val').then((val) => {
|
||||
expect(String(val)).not.to.include('.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('max supply (21M BTC)', () => {
|
||||
it('shows warning when entering 21M BTC', () => {
|
||||
cy.get('input[formControlName="bitcoin"]').clear().type('21000000');
|
||||
cy.get('.alert.alert-warning').should('be.visible');
|
||||
cy.get('.alert.alert-warning').should('contain', 'Values were capped at the max supply of 21M BTC');
|
||||
cy.get('input[formControlName="bitcoin"]').invoke('val').should('equal', '21000000');
|
||||
cy.get('input[formControlName="satoshis"]').invoke('val').should('equal', '2100000000000000');
|
||||
});
|
||||
|
||||
it('caps values at max supply', () => {
|
||||
cy.get('input[formControlName="bitcoin"]').clear().type('25000000');
|
||||
cy.get('.alert.alert-warning').should('be.visible');
|
||||
cy.get('input[formControlName="bitcoin"]').invoke('val').should('equal', '21000000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clipboard buttons', () => {
|
||||
it('copy buttons exist and are visible', () => {
|
||||
cy.get('app-clipboard').should('have.length', 3);
|
||||
cy.get('app-clipboard button, app-clipboard .btn').each(($btn) => {
|
||||
cy.wrap($btn).should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('responsive viewports', () => {
|
||||
it('calculator is usable on desktop', () => {
|
||||
cy.viewport('macbook-16');
|
||||
cy.get('input[formControlName="bitcoin"]').should('be.visible');
|
||||
cy.get('input[formControlName="bitcoin"]').clear().type('1');
|
||||
cy.get('input[formControlName="bitcoin"]').invoke('val').should('include', '1');
|
||||
});
|
||||
|
||||
it('calculator is usable on mobile', () => {
|
||||
cy.viewport('iphone-6');
|
||||
cy.get('input[formControlName="bitcoin"]').should('be.visible');
|
||||
cy.get('input[formControlName="fiat"]').should('be.visible');
|
||||
cy.get('input[formControlName="satoshis"]').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loading state', () => {
|
||||
it('shows calculator form after price feed loads', () => {
|
||||
cy.contains('Waiting for price feed...').should('not.exist');
|
||||
cy.get('input[formControlName="bitcoin"]').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('JPY currency', () => {
|
||||
beforeEach(() => {
|
||||
cy.get('app-fiat-selector').scrollIntoView();
|
||||
cy.get('app-fiat-selector select').select('JPY');
|
||||
cy.get('.symbol', { timeout: 10000 }).should(($el) => {
|
||||
expect($el.text().replace(/,/g, '')).to.include(String(MOCK_BTC_PRICE_JPY));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('displays JPY conversion rate in .symbol', () => {
|
||||
cy.get('.symbol').invoke('text').then((text) => {
|
||||
expect(text.replace(/,/g, '')).to.include(String(MOCK_BTC_PRICE_JPY));
|
||||
});
|
||||
});
|
||||
|
||||
it('displays 1 BTC with correct sats and fiat in JPY', () => {
|
||||
cy.get('input[formControlName="bitcoin"]').invoke('val').then((btcVal) => {
|
||||
expect(parseFloat(String(btcVal))).to.equal(1);
|
||||
});
|
||||
cy.get('input[formControlName="satoshis"]').invoke('val').should('equal', '100000000');
|
||||
cy.get('input[formControlName="fiat"]').invoke('val').then((fiatVal) => {
|
||||
const fiat = parseFloat(String(fiatVal).replace(/,/g, ''));
|
||||
expect(fiat).to.equal(MOCK_BTC_PRICE_JPY);
|
||||
});
|
||||
});
|
||||
|
||||
it('updates fiat and sats when entering 0.5 BTC in JPY', () => {
|
||||
const expectedFiat = Math.round(MOCK_BTC_PRICE_JPY * 0.5 * 100) / 100;
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
it('updates BTC and sats when entering fiat value in JPY', () => {
|
||||
const fiatAmount = 1000000;
|
||||
const expectedBtc = parseFloat((fiatAmount / MOCK_BTC_PRICE_JPY).toFixed(8));
|
||||
const expectedSats = Math.round((fiatAmount / MOCK_BTC_PRICE_JPY) * 100_000_000);
|
||||
cy.get('input[formControlName="fiat"]').clear().type(String(fiatAmount));
|
||||
cy.get('input[formControlName="bitcoin"]').invoke('val').then((btcVal) => {
|
||||
expect(parseFloat(String(btcVal))).to.equal(expectedBtc);
|
||||
});
|
||||
cy.get('input[formControlName="satoshis"]').invoke('val').then((satsVal) => {
|
||||
expect(parseInt(String(satsVal), 10)).to.equal(expectedSats);
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
it.skip(`Tests cannot be run on the selected BASE_MODULE ${calculatorBaseModule}`);
|
||||
}
|
||||
});
|
||||
|
|
@ -32,7 +32,7 @@ export const mockWebSocketV2 = () => {
|
|||
const winWebSocket = win.WebSocket;
|
||||
cy.stub(win, 'WebSocket').callsFake((url) => {
|
||||
console.log(url);
|
||||
if ((new URL(url).pathname.indexOf('/sockjs-node/') !== 0)) {
|
||||
if ((new URL(url).pathname.indexOf('/sockjs-node/') !== 0) && (new URL(url).pathname.indexOf('/ng-cli-ws') !== 0)) {
|
||||
const { server, websocket } = createMock(url);
|
||||
|
||||
win.mockServer = server;
|
||||
|
|
@ -117,10 +117,16 @@ export const receiveWebSocketMessageFromServer = ({
|
|||
};
|
||||
|
||||
|
||||
const MOCK_SOCKET_WAIT_TIMEOUT_MS = 5000;
|
||||
|
||||
export const emitMempoolInfo = ({
|
||||
params
|
||||
}: { params?: any } = {}) => {
|
||||
cy.window().then((win) => {
|
||||
cy.window({ timeout: MOCK_SOCKET_WAIT_TIMEOUT_MS })
|
||||
.should((win) => {
|
||||
expect(win.mockSocket, 'mockSocket to be set (app should open WebSocket within timeout)').to.not.be.undefined;
|
||||
})
|
||||
.then((win) => {
|
||||
//TODO: Refactor to take into account different parameterized mocking scenarios
|
||||
switch (params.network) {
|
||||
//TODO: Use network specific mocks
|
||||
|
|
@ -133,7 +139,6 @@ export const emitMempoolInfo = ({
|
|||
|
||||
switch (params.command) {
|
||||
case 'init': {
|
||||
win.mockSocket.send('{"conversions":{"USD":32365.338815782445}}');
|
||||
cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'utf-8').then((fixture) => {
|
||||
win.mockSocket.send(JSON.stringify(fixture));
|
||||
});
|
||||
|
|
@ -159,7 +164,11 @@ export const emitMempoolInfo = ({
|
|||
}
|
||||
});
|
||||
cy.waitForSkeletonGone();
|
||||
return cy.get('#mempool-block-0');
|
||||
if (!params.waitForMempoolBlocks) {
|
||||
return
|
||||
} else {
|
||||
return cy.get('#mempool-block-0');
|
||||
}
|
||||
};
|
||||
|
||||
export const dropWebSocket = (() => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">{{ currency$ | async }}</span>
|
||||
</div>
|
||||
<input type="text" inputmode="numeric" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
|
||||
<input type="text" inputmode="decimal" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
|
||||
<app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||
</div>
|
||||
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">BTC</span>
|
||||
</div>
|
||||
<input type="text" inputmode="numeric" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
|
||||
<input type="text" inputmode="decimal" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
|
||||
<app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||
</div>
|
||||
|
||||
|
|
@ -35,6 +35,12 @@
|
|||
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center mt-3" *ngIf="isMaxSupply">
|
||||
<div class="alert alert-warning" role="alert" i18n="calculator.max-supply-warning|Max supply warning message">
|
||||
Values were capped at the max supply of 21M BTC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
|
|
@ -47,13 +53,13 @@
|
|||
|
||||
<div class="row justify-content-center">
|
||||
<div class="fiat-text">
|
||||
<app-fiat [value]="form.get('satoshis').value" digitsInfo="1.0-0"></app-fiat>
|
||||
<app-fiat [value]="form.get('satoshis').value" digitsInfo="1.2-2"></app-fiat>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center mt-3">
|
||||
<div class="symbol">
|
||||
Fiat price last updated <app-time kind="since" [time]="lastFiatPrice$ | async" [fastRender]="true"></app-time>
|
||||
Fiat price last updated <app-time kind="since" [time]="lastFiatPrice$ | async" [fastRender]="true"></app-time>: {{ price$ | async | fiatCurrency : '1.0-0' : (currency$ | async) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import { map, switchMap } from 'rxjs/operators';
|
|||
import { StateService } from '@app/services/state.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
|
||||
const MAX_BTC_SUPPLY = 21000000;
|
||||
const MAX_SATOSHI_SUPPLY = MAX_BTC_SUPPLY * 100_000_000;
|
||||
|
||||
@Component({
|
||||
selector: 'app-calculator',
|
||||
templateUrl: './calculator.component.html',
|
||||
|
|
@ -15,6 +18,8 @@ import { WebsocketService } from '@app/services/websocket.service';
|
|||
export class CalculatorComponent implements OnInit {
|
||||
satoshis = 10000;
|
||||
form: FormGroup;
|
||||
currentPrice = 0;
|
||||
isMaxSupply = false;
|
||||
|
||||
currency$ = this.stateService.fiatCurrency$;
|
||||
price$: Observable<number>;
|
||||
|
|
@ -53,12 +58,23 @@ export class CalculatorComponent implements OnInit {
|
|||
this.price$,
|
||||
this.form.get('fiat').valueChanges
|
||||
]).subscribe(([price, value]) => {
|
||||
const rate = (value / price).toFixed(8);
|
||||
const satsRate = Math.round(value / price * 100_000_000);
|
||||
this.currentPrice = price;
|
||||
const maxFiat = price * MAX_BTC_SUPPLY;
|
||||
const isMaxSupply = value >= maxFiat;
|
||||
this.isMaxSupply = isMaxSupply;
|
||||
if (isMaxSupply) {
|
||||
value = maxFiat;
|
||||
this.form.get('fiat').setValue(this.formatFiat(value), { emitEvent: false });
|
||||
}
|
||||
let rate = parseFloat((value / price).toFixed(8));
|
||||
if (rate >= MAX_BTC_SUPPLY) {
|
||||
rate = MAX_BTC_SUPPLY;
|
||||
}
|
||||
const satsRate = Math.round(rate * 100_000_000);
|
||||
if (isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
this.form.get('bitcoin').setValue(rate, { emitEvent: false });
|
||||
this.form.get('bitcoin').setValue(isMaxSupply ? MAX_BTC_SUPPLY.toString() : rate.toFixed(8), { emitEvent: false });
|
||||
this.form.get('satoshis').setValue(satsRate, { emitEvent: false } );
|
||||
});
|
||||
|
||||
|
|
@ -66,24 +82,36 @@ export class CalculatorComponent implements OnInit {
|
|||
this.price$,
|
||||
this.form.get('bitcoin').valueChanges
|
||||
]).subscribe(([price, value]) => {
|
||||
this.currentPrice = price;
|
||||
const isMaxSupply = parseFloat(value) >= MAX_BTC_SUPPLY;
|
||||
this.isMaxSupply = isMaxSupply;
|
||||
const rate = parseFloat((value * price).toFixed(8));
|
||||
if (isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
this.form.get('fiat').setValue(rate, { emitEvent: false } );
|
||||
this.form.get('satoshis').setValue(Math.round(value * 100_000_000), { emitEvent: false } );
|
||||
this.form.get('fiat').setValue(this.formatFiat(rate), { emitEvent: false } );
|
||||
this.form.get('satoshis').setValue(Math.min(Math.round(value * 100_000_000), MAX_SATOSHI_SUPPLY), { emitEvent: false } );
|
||||
});
|
||||
|
||||
combineLatest([
|
||||
this.price$,
|
||||
this.form.get('satoshis').valueChanges
|
||||
]).subscribe(([price, value]) => {
|
||||
const rate = parseFloat((value / 100_000_000 * price).toFixed(8));
|
||||
const bitcoinRate = (value / 100_000_000).toFixed(8);
|
||||
this.currentPrice = price;
|
||||
let bitcoinValue = value / 100_000_000;
|
||||
const isMaxSupply = bitcoinValue >= MAX_BTC_SUPPLY;
|
||||
this.isMaxSupply = isMaxSupply;
|
||||
if (isMaxSupply) {
|
||||
bitcoinValue = MAX_BTC_SUPPLY;
|
||||
value = MAX_SATOSHI_SUPPLY;
|
||||
this.form.get('satoshis').setValue(value, { emitEvent: false });
|
||||
}
|
||||
const rate = parseFloat((bitcoinValue * price).toFixed(8));
|
||||
const bitcoinRate = isMaxSupply ? MAX_BTC_SUPPLY.toString() : bitcoinValue.toFixed(8);
|
||||
if (isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
this.form.get('fiat').setValue(rate, { emitEvent: false } );
|
||||
this.form.get('fiat').setValue(this.formatFiat(rate), { emitEvent: false } );
|
||||
this.form.get('bitcoin').setValue(bitcoinRate, { emitEvent: false });
|
||||
});
|
||||
|
||||
|
|
@ -104,12 +132,21 @@ 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 (sanitizedValue === '') {
|
||||
sanitizedValue = '0';
|
||||
}
|
||||
if (name === 'satoshis') {
|
||||
sanitizedValue = parseFloat(sanitizedValue).toFixed(0);
|
||||
}
|
||||
if (name === 'bitcoin' && parseFloat(sanitizedValue) >= MAX_BTC_SUPPLY) {
|
||||
sanitizedValue = MAX_BTC_SUPPLY.toString();
|
||||
}
|
||||
if (name === 'satoshis' && parseFloat(sanitizedValue) > MAX_SATOSHI_SUPPLY) {
|
||||
sanitizedValue = MAX_SATOSHI_SUPPLY.toString();
|
||||
}
|
||||
formControl.setValue(sanitizedValue, {emitEvent: true});
|
||||
}
|
||||
|
||||
|
|
@ -137,4 +174,22 @@ export class CalculatorComponent implements OnInit {
|
|||
selectAll(event): void {
|
||||
event.target.select();
|
||||
}
|
||||
|
||||
formatFiat(num: number): string | number {
|
||||
if (Math.abs(num) >= 1000) {
|
||||
// For values >= 1000: show 2 decimals, or 0 if whole number
|
||||
if (num % 1 === 0) {
|
||||
return Math.round(num);
|
||||
}
|
||||
return (Math.round(num * 100) / 100).toFixed(2);
|
||||
}
|
||||
if (num % 1 === 0) {
|
||||
return Math.round(num);
|
||||
}
|
||||
// For small values (< 1), show more precision
|
||||
if (Math.abs(num) < 1 && num !== 0) {
|
||||
return num.toFixed(8);
|
||||
}
|
||||
return (Math.round(num * 100) / 100).toFixed(2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ export class BitcoinsatoshisPipe implements PipeTransform {
|
|||
constructor(private sanitizer: DomSanitizer) { }
|
||||
|
||||
transform(value: string, firstPartClass?: string): SafeHtml {
|
||||
const newValue = this.insertSpaces(parseFloat(value || '0').toFixed(8));
|
||||
const numValue = parseFloat(value || '0');
|
||||
const newValue = this.insertSpaces(numValue.toFixed(8));
|
||||
const position = (newValue || '0').search(/[1-9]/);
|
||||
|
||||
const firstPart = newValue.slice(0, position);
|
||||
|
|
@ -22,8 +23,17 @@ export class BitcoinsatoshisPipe implements PipeTransform {
|
|||
}
|
||||
|
||||
insertSpaces(str: string): string {
|
||||
const length = str.length;
|
||||
return str.slice(0, length - 6) + ' ' + str.slice(length - 6, length - 3) + ' ' + str.slice(length - 3);
|
||||
const [integerPart, decimalPart] = str.split('.');
|
||||
|
||||
// Format integer part with thousand separators (right to left)
|
||||
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||
|
||||
// Format decimal part: first 2 digits, then groups of 3
|
||||
const formattedDecimal = decimalPart.slice(0, 2) + ' ' +
|
||||
decimalPart.slice(2, 5) + ' ' +
|
||||
decimalPart.slice(5);
|
||||
|
||||
return formattedInteger + '.' + formattedDecimal;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,11 @@ export class FiatCurrencyPipe implements PipeTransform {
|
|||
const currency = args[1] || this.currency || 'USD';
|
||||
|
||||
if (Math.abs(num) >= 1000) {
|
||||
return new Intl.NumberFormat(this.locale, { style: 'currency', currency, maximumFractionDigits: 0 }).format(num);
|
||||
// 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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue