jam/src/utils.ts
2024-10-31 14:23:53 +01:00

151 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { JM_DUST_THRESHOLD } from './constants/jm'
import type { AccountBalances } from './context/BalanceSummary'
import type { AmountSats, OfferType, WalletFileName } from './libs/JmWalletApi'
const BTC_FORMATTER = new Intl.NumberFormat('en-US', {
minimumIntegerDigits: 1,
minimumFractionDigits: 8,
maximumFractionDigits: 8,
})
const SATS_FORMATTER = new Intl.NumberFormat('en-US', {
minimumIntegerDigits: 1,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})
export const BTC: Unit = 'BTC'
export const SATS: Unit = 'sats'
export const JM_WALLET_FILE_EXTENSION = '.jmdat'
export const DUMMY_MNEMONIC_PHRASE: MnemonicPhrase =
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'.split(' ')
export const SEGWIT_ACTIVATION_BLOCK = 481_824 // https://github.com/bitcoin/bitcoin/blob/v25.0/src/kernel/chainparams.cpp#L86
export const sanitizeWalletName = (name: string) => name.replace(JM_WALLET_FILE_EXTENSION, '')
export const walletDisplayNameToFileName = (name: string) => (name + JM_WALLET_FILE_EXTENSION) as WalletFileName
export const walletDisplayName = (fileName: WalletFileName) => sanitizeWalletName(fileName)
export const displayDate = (string: string) => new Date(string).toLocaleString()
export const btcToSats = (value: string) => Math.round(parseFloat(value) * 100000000)
export const satsToBtc = (value: string) => parseInt(value, 10) / 100000000
export const formatBtc = (value: number) => BTC_FORMATTER.format(value)
export const BITCOIN_SYMBOL = '\u20BF' // Bitcoin Currency Symbol `₿`
const HORIZONTAL_ELLIPSIS = '\u2026' // Horizontal Ellipsis `…`
const NNBSP = '\u202F' // Narrow No-Break Space ``
const NBSP = '\u00A0' // No-Break Space ` `
export const formatBtcDisplayValue = (sats: AmountSats, { withSymbol = false }: { withSymbol?: boolean } = {}) => {
const formattedBtc = formatBtc(satsToBtc(String(sats)))
const pointIndex = formattedBtc.indexOf('.')
return (
(withSymbol ? `${BITCOIN_SYMBOL}${NNBSP}` : '') +
(formattedBtc.substring(0, pointIndex + 3) +
NBSP +
formattedBtc.substring(pointIndex + 3, pointIndex + 6) +
NBSP +
formattedBtc.substring(pointIndex + 6))
)
}
export const formatSats = (value: number) => SATS_FORMATTER.format(value)
export const shortenStringMiddle = (value: string, chars = 8, separator = HORIZONTAL_ELLIPSIS) => {
const prefixLength = Math.max(Math.floor(chars / 2), 1)
if (value.length <= prefixLength * 2) {
return `${value}`
}
return `${value.substring(0, prefixLength)}${separator}${value.substring(value.length - prefixLength)}`
}
export const percentageToFactor = (val: number, precision = 6) => {
// Value cannot just be divided
// e.g. ✗ 0.0027 / 100 == 0.000027000000000000002
// but: ✓ Number((0.0027 / 100).toFixed(6)) = 0.000027
return Number((val / 100).toFixed(precision))
}
export const factorToPercentage = (val: number, precision = 6) => {
// Value cannot just be multiplied
// e.g. ✗ 0.000027 * 100 == 0.0026999999999999997
// but: ✓ Number((0.000027 * 100).toFixed(6)) = 0.0027
return Number((val * 100).toFixed(precision))
}
// can be any of ['sw0reloffer', 'swreloffer', 'reloffer']
export const isRelativeOffer = (offertype: OfferType) => offertype.includes('reloffer')
// can be any of ['sw0absoffer', 'swabsoffer', 'absoffer']
export const isAbsoluteOffer = (offertype: OfferType) => offertype.includes('absoffer')
export const isValidNumber = (val: number | undefined | null) => typeof val === 'number' && !isNaN(val)
export const UNKNOWN_VERSION: SemVer = { major: 0, minor: 0, patch: 0, raw: 'unknown' }
const versionRegex = new RegExp(/^v?(\d+)\.(\d+)\.(\d+).*$/)
export const toSemVer = (raw?: string): SemVer => {
const arr = versionRegex.exec(raw || '')
if (!arr || arr.length < 4) {
return UNKNOWN_VERSION
}
return {
major: parseInt(arr[1], 10),
minor: parseInt(arr[2], 10),
patch: parseInt(arr[3], 10),
raw,
}
}
/**
* Scrolls to the top of the page.
*
* Hint: There is a small delay before the scrolling is initiated,
* in order to mitigate some weird browser behaviour, where it
* did not properly work without a timeout.
*/
export const scrollToTop = (options?: ScrollOptions) => {
setTimeout(() => window.scrollTo({ behavior: 'smooth', ...options, top: 0, left: 0 }), 21)
}
export const noop = () => {}
export const setIntervalDebounced = (
callback: () => Promise<void>,
delay: Milliseconds,
onTimerIdChanged: (timerId: NodeJS.Timeout) => void,
onError: (error: any, loop: () => void) => void = (_, loop) => loop(),
) => {
;(function loop() {
onTimerIdChanged(
setTimeout(async () => {
try {
await callback()
loop()
} catch (e: any) {
onError(e, loop)
}
}, delay),
)
})()
}
const calcMaxAvailableBalanceInJar = (accountBalances: AccountBalances) => {
return Math.max(0, Math.max(...Object.values(accountBalances || []).map((it) => it.calculatedAvailableBalanceInSats)))
}
export const calcOfferMinsizeMax = (
accountBalances: AccountBalances,
minBufferAmount: AmountSats = JM_DUST_THRESHOLD,
) => {
const maxAvailableBalanceInJar = calcMaxAvailableBalanceInJar(accountBalances)
return Math.max(0, maxAvailableBalanceInJar - minBufferAmount)
}