Files

409 lines
13 KiB
TypeScript

import BigNumber from 'bignumber.js';
import { settingsStore, fiatStore, unitsStore } from '../stores/Stores';
import {
numberWithCommas,
numberWithDecimals,
SATS_PER_BTC
} from './UnitsUtils';
import type { Units } from './UnitsUtils';
import FeeUtils from './FeeUtils';
import { localeString } from './LocaleUtils';
export interface AmountDisplayResult {
displayAmount: string;
shouldShowRounding: boolean;
}
export interface ValueDisplayProps {
amount: string;
unit: Units;
symbol?: string;
negative?: boolean;
plural?: boolean;
rtl?: boolean;
space?: boolean;
error?: string;
separatorSwap?: boolean;
}
/**
* Processes satoshi amounts for display, respecting the hideMsats setting
* and optional rounding behavior.
*
* @param amount - The amount to process (as string or number)
* @param roundAmount - Whether to apply rounding logic (default: false)
* @param forceMsats - Force showing millisatoshi decimals regardless of setting (e.g. for fees)
* @returns Object containing the display amount and whether to show rounding indicator
*/
export function processSatsAmount(
amount: string | number,
roundAmount: boolean = false,
forceMsats: boolean = false
): AmountDisplayResult {
const amountStr = amount.toString();
const hideMsats = forceMsats ? false : shouldHideMillisatoshiAmounts();
// Remove commas from the amount string for processing
const cleanAmountStr = amountStr.replace(/,/g, '');
// Always check for decimals when hideMsats is enabled
const [, decimalPart] = cleanAmountStr.split('.');
const hasDecimals = decimalPart && Number(decimalPart) > 0;
// Determine if we should round based on hideMsats setting or explicit roundAmount
// When hideMsats is false (showMillisatoshiAmounts is true), never round (respect the setting)
// When hideMsats is true (showMillisatoshiAmounts is false), always round decimals
// Only apply explicit roundAmount when hideMsats is true
const shouldRound =
hasDecimals && (hideMsats || (roundAmount && hideMsats));
let displayAmount: string;
if (shouldRound) {
// Round the number and format with commas
const roundedNumber = Math.round(Number(cleanAmountStr));
displayAmount = numberWithCommas(roundedNumber);
} else {
// Format the original amount with commas
displayAmount = numberWithCommas(cleanAmountStr);
}
// Only show rounding indicator when explicitly requested via roundAmount prop
const shouldShowRounding = Boolean(shouldRound && roundAmount);
return {
displayAmount,
shouldShowRounding
};
}
/**
* Checks if the hideMsats setting is enabled
* @returns true if millisatoshi amounts should be hidden
*/
export function shouldHideMillisatoshiAmounts(): boolean {
return !settingsStore?.settings?.display?.showMillisatoshiAmounts;
}
/**
* Gets the fiat entry from fiatStore for a given currency code
* @param currencyCode - The currency code to look up (e.g., 'USD', 'EUR')
* @returns The fiat entry object or undefined if not found
*/
function getFiatEntry(currencyCode: string): any | undefined {
if (!fiatStore.fiatRates || !currencyCode) {
return undefined;
}
return fiatStore.fiatRates.find(
(entry: any) => entry.code === currencyCode
);
}
/**
* Converts satoshi amounts to unformatted display values based on the current unit setting
* @param sats - The amount in satoshis (string or number)
* @param fixedUnits - Optional unit override ('sats', 'BTC', or 'fiat')
* @returns Object containing the amount string, unit, and display properties
*/
export function getUnformattedAmount({
sats = 0,
fixedUnits
}: {
sats?: string | number;
fixedUnits?: string;
}): ValueDisplayProps {
const { settings } = settingsStore;
const { fiat, display } = settings;
const showAllDecimalPlaces: boolean =
(display && display.showAllDecimalPlaces) || false;
const effectiveUnits = fixedUnits || unitsStore.units;
const satsNumber = Number(sats);
const negative = satsNumber < 0;
const absValueSats = Math.abs(satsNumber);
if (effectiveUnits === 'BTC') {
return {
amount: FeeUtils.toFixed(
absValueSats / SATS_PER_BTC,
showAllDecimalPlaces
),
unit: 'BTC',
negative,
space: false
};
} else if (effectiveUnits === 'sats') {
return {
amount: absValueSats.toString(),
unit: 'sats',
negative,
plural: !(satsNumber === 1 || satsNumber === -1)
};
} else {
const currency = fiat;
// TODO: is this the right place to catch this?
if (!currency) {
return {
amount: localeString('general.disabled'),
unit: 'fiat',
symbol: '$'
};
}
if (fiatStore.fiatRates) {
const fiatEntry = getFiatEntry(fiat);
if (!fiatEntry?.rate) {
return {
amount: localeString('general.disabled'),
unit: 'fiat',
error: localeString('general.fiatRateNotAvailable')
};
}
const rate = (fiatEntry && fiatEntry.rate) || 0;
const { symbol, space, rtl, decimalPlaces, separatorSwap } =
fiatStore.getSymbol();
const decimals = decimalPlaces !== undefined ? decimalPlaces : 2;
const amount = (
FeeUtils.toFixed(absValueSats / SATS_PER_BTC) * rate
).toFixed(decimals);
return {
amount,
unit: 'fiat',
symbol,
negative,
plural: false,
rtl,
space,
separatorSwap
};
} else {
return {
amount: localeString('general.disabled'),
unit: 'fiat',
error: localeString('general.errorFetchingFiatRates')
};
}
}
}
/**
* Converts satoshi amounts to formatted display strings
* @param value - The amount in satoshis (string or number)
* @param fixedUnits - Optional unit override ('sats', 'BTC', or 'fiat')
* @returns Formatted string like "₿0.5", "1,000 sats", or "$50.00"
*/
export function getAmountFromSats(
value: string | number = 0,
fixedUnits?: string
): string | undefined {
const { settings } = settingsStore;
const { fiat } = settings;
const units = fixedUnits || unitsStore.units;
const [wholeSats] = value.toString().split('.');
if (units === 'BTC') {
// handle negative values
const valueToProcess = (wholeSats && wholeSats.toString()) || '0';
if (valueToProcess.includes('-')) {
const processedValue = valueToProcess.split('-')[1];
return `-₿${FeeUtils.toFixed(
Number(processedValue) / SATS_PER_BTC
)}`;
}
return `₿${FeeUtils.toFixed(Number(wholeSats || 0) / SATS_PER_BTC)}`;
} else if (units === 'sats') {
const sats = `${numberWithCommas(wholeSats || value) || 0} ${
Number(value) === 1 || Number(value) === -1 ? 'sat' : 'sats'
}`;
return sats;
} else if (units === 'fiat' && fiat) {
if (fiatStore.fiatRates) {
const fiatEntry = getFiatEntry(fiat);
if (!fiatEntry) {
return localeString('general.notAvailable');
}
const { code } = fiatEntry;
const rate = (fiatEntry && fiatEntry.rate) || 0;
const { symbol, space, rtl, separatorSwap } =
fiatStore.symbolLookup(code);
const amount = (
FeeUtils.toFixed(Number(wholeSats || 0) / SATS_PER_BTC) * rate
).toFixed(2);
const formattedAmount = separatorSwap
? numberWithDecimals(amount)
: numberWithCommas(amount);
if (rtl) {
return `${formattedAmount}${space ? ' ' : ''}${symbol}`;
} else {
return `${symbol}${space ? ' ' : ''}${formattedAmount}`;
}
} else {
return localeString('general.notAvailable');
}
}
}
/**
* Formats amounts for display (expects BTC value directly for BTC unit, not sats)
* @param value - The amount (string or number). For BTC unit, this should be in BTC, not sats.
* @param fixedUnits - Optional unit override ('sats', 'BTC', or 'fiat')
* @returns Formatted string like "₿0.5", "1,000 sats", or "$50.00"
*/
export function getFormattedAmount(
value: string | number = 0,
fixedUnits?: string
): string | undefined {
const { settings } = settingsStore;
const { fiat } = settings;
const units = fixedUnits || unitsStore.units;
if (units === 'BTC') {
// handle negative values
const valueToProcess = value.toString() || '0';
if (valueToProcess.includes('-')) {
const processedValue = valueToProcess.split('-')[1];
return `-₿${FeeUtils.toFixed(Number(processedValue))}`;
}
return `₿${FeeUtils.toFixed(Number(value || 0))}`;
} else if (units === 'sats') {
const [wholeSats] = value.toString().split('.');
const sats = `${numberWithCommas(wholeSats || value) || 0} ${
Number(value) === 1 || Number(value) === -1 ? 'sat' : 'sats'
}`;
return sats;
} else if (units === 'fiat' && fiat) {
if (fiatStore.fiatRates) {
const fiatEntry = getFiatEntry(fiat);
if (!fiatEntry) {
return localeString('general.notAvailable');
}
const { code } = fiatEntry;
const { symbol, space, rtl, separatorSwap } =
fiatStore.symbolLookup(code);
// handle amounts passed in with commas
const amount = Number(value.toString().replace(/,/g, '.')).toFixed(
2
);
const formattedAmount = separatorSwap
? numberWithDecimals(amount)
: numberWithCommas(amount);
if (rtl) {
return `${formattedAmount}${space ? ' ' : ''}${symbol}`;
} else {
return `${symbol}${space ? ' ' : ''}${formattedAmount}`;
}
} else {
return localeString('general.notAvailable');
}
}
}
/**
* Calculates the fee as a percentage of the payment amount.
* Returns a string like "1.5%" or empty string if not applicable.
*
* @param fee - The fee amount (string or number)
* @param amount - The payment amount (string or number)
* @returns Formatted percentage string (e.g., "1.5%") or empty string
*/
export function getFeePercentage(
fee: string | number | undefined,
amount: string | number | undefined
): string {
if (!fee || !amount || fee == '0' || amount == '0') return '';
return (
Number(new BigNumber(fee).div(amount).times(100).toFixed(3))
.toString()
.replace(/-/g, '') + '%'
);
}
/**
* Converts satoshis to millisatoshis
* @param sats - Amount in satoshis
* @returns Amount in millisatoshis
*/
export function satsToMillisats(sats: number): number {
return Math.floor(sats * 1000);
}
/**
* Converts millisatoshis to satoshis
* @param millisats - Amount in millisatoshis
* @returns Amount in satoshis
*/
export function millisatsToSats(millisats: number): number {
return Math.floor(millisats / 1000);
}
/**
* Converts an amount in the current display unit (sats, BTC, or fiat) to satoshis
* @param amount - The amount to convert (as string or number)
* @param forceUnit - Optional unit override ('sats', 'BTC', or 'fiat')
* @returns The amount in satoshis
*/
export function getSatAmount(
amount: string | number,
forceUnit?: string
): number {
const { fiatRates } = fiatStore;
const { settings } = settingsStore;
const { fiat } = settings;
const { units } = unitsStore;
const effectiveUnits = forceUnit || units;
const value = amount ? amount.toString().replace(/,/g, '.') : 0;
const fiatEntry =
fiat && fiatRates
? fiatRates.find((entry: any) => entry.code === fiat)
: null;
const rate = fiat && fiatRates && fiatEntry ? fiatEntry.rate : 0;
let satAmount: number = 0;
switch (effectiveUnits) {
case 'sats':
satAmount = Number(value);
break;
case 'BTC':
satAmount = new BigNumber(value || 0)
.multipliedBy(SATS_PER_BTC)
.toNumber();
break;
case 'fiat':
satAmount =
rate && value
? Number(
new BigNumber(value)
.dividedBy(rate)
.multipliedBy(SATS_PER_BTC)
.toNumber()
.toFixed(0)
)
: 0;
break;
default:
satAmount = 0;
break;
}
return satAmount;
}