@lifi/widget
Version:
LI.FI Widget for cross-chain bridging and swapping. It will drive your multi-chain strategy and attract new users from everywhere.
191 lines (166 loc) • 5.09 kB
text/typescript
import { formatUnits } from 'viem'
/**
* Format token amount to at least 6 decimals.
* @param amount amount to format.
* @returns formatted amount.
*/
export function formatTokenAmount(
amount: bigint | undefined,
decimals: number
) {
const formattedAmount = amount ? formatUnits(amount, decimals) : '0'
const parsedAmount = Number.parseFloat(formattedAmount)
if (!parsedAmount || Number.isNaN(Number(formattedAmount))) {
return '0'
}
return formattedAmount
}
export function formatSlippage(
slippage = '',
defaultValue = '',
returnInitial = false
): string {
if (!slippage) {
return slippage
}
const parsedSlippage = Number.parseFloat(slippage)
if (Number.isNaN(Number(slippage)) && !Number.isNaN(parsedSlippage)) {
return parsedSlippage.toString()
}
if (Number.isNaN(parsedSlippage)) {
return defaultValue
}
if (parsedSlippage >= 100) {
return '100'
}
if (parsedSlippage < 0) {
return Math.abs(parsedSlippage).toString()
}
if (returnInitial) {
return slippage
}
return parsedSlippage.toString()
}
/**
* Formats a user input amount string, normalizing it and optionally limiting decimal places.
* @param amount - The amount string to format (e.g., '123.45', '1,23', '0..')
* @param decimals - Maximum number of decimal places to allow. If null, no limit is applied.
* @param returnInitial - If true, preserves the input format during typing (e.g., keeps trailing dots,
* preserves leading zeros, keeps negative signs). If false (default), cleans up the
* format on blur (removes leading/trailing zeros, removes negative signs).
* @returns The formatted amount string, or empty string for invalid input.
*/
export function formatInputAmount(
amount: string,
decimals: number | null = null,
returnInitial = false
) {
if (!amount) {
return amount
}
// Replace commas with dots
let formattedAmount = amount.trim().replaceAll(',', '.')
// Keep only the first dot, remove all subsequent dots
const dotIndex = formattedAmount.indexOf('.')
if (dotIndex !== -1) {
formattedAmount = `${formattedAmount.slice(0, dotIndex + 1)}${formattedAmount.slice(dotIndex + 1).replaceAll('.', '')}`
}
// If the amount starts with a dot, prepend 0
if (
(!returnInitial && formattedAmount.startsWith('.')) ||
formattedAmount === '.'
) {
formattedAmount = `0${formattedAmount}`
}
// Parse the valid part of the amount
const parsedAmount = Number.parseFloat(formattedAmount)
if (Number.isNaN(Number(formattedAmount)) && !Number.isNaN(parsedAmount)) {
formattedAmount = parsedAmount.toString()
}
if (Number.isNaN(Math.abs(Number(formattedAmount)))) {
return ''
}
// Split and limit decimals
let [integer, fraction = ''] = formattedAmount.split('.')
if (decimals !== null && fraction.length > decimals) {
fraction = fraction.slice(0, decimals)
}
if (returnInitial) {
if (!fraction) {
return formattedAmount
}
return `${integer}${fraction ? `.${fraction}` : ''}`
}
// Remove leading zeros and minus sign
integer = integer.replace(/^0+|-/, '')
// Remove trailing zeros
fraction = fraction.replace(/(0+)$/, '')
return `${integer || (fraction ? '0' : '')}${fraction ? `.${fraction}` : ''}`
}
export function formatTokenPrice(
amount?: string | bigint,
price?: string,
decimals?: number
) {
if (!amount || !price) {
return 0
}
const formattedAmount =
typeof amount === 'bigint' && decimals !== undefined
? formatUnits(amount, decimals)
: amount.toString()
if (Number.isNaN(Number(formattedAmount)) || Number.isNaN(Number(price))) {
return 0
}
return Number.parseFloat(formattedAmount) * Number.parseFloat(price)
}
/**
* Formatter for price value to token amount
*/
const formatter = new Intl.NumberFormat('en', {
notation: 'standard',
roundingPriority: 'morePrecision',
maximumSignificantDigits: 20,
maximumFractionDigits: 20,
useGrouping: false,
})
/**
* Convert price value to token amount
*/
export function priceToTokenAmount(priceValue: string, priceUSD?: string) {
if (!priceValue || !priceUSD) {
return '0'
}
const numericPriceValue = Number.parseFloat(priceValue)
const numericPrice = Number.parseFloat(priceUSD)
if (
Number.isNaN(numericPriceValue) ||
Number.isNaN(numericPrice) ||
!numericPrice
) {
return '0'
}
return formatter.format(numericPriceValue / numericPrice)
}
const units = [
['day', 86400],
['hour', 3600],
['minute', 60],
['second', 1],
] as const
export function formatDuration(seconds: number, locale: string): string {
const match = units.find(([, v]) => seconds >= v)
const amount = match ? Math.floor(seconds / match[1]) : 0
const unit = match?.[0] ?? 'second'
return amount.toLocaleString(locale, {
style: 'unit',
unit,
unitDisplay: 'narrow',
})
}
export function wrapLongWords(text: string): string {
return text.replace(
/\S{32,}/g,
(word) => `${word.slice(0, 8)}...${word.slice(-4)}`
)
}