jam/src/lib/fidelityBondUtils.ts
Parth 5ae6a18e35
Some checks are pending
Build / build (v26.3.0) (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
Deploy Storybook / deploy (push) Waiting to run
Rework the Fidelity Bond modals (create, renew, unlock) (#1345)
* fix: generate valid past lockdate options in developer mode

* fix: keep dialog scrollbar clear of step content

* feat: allow creating additional fidelity bonds in developer mode

* chore: align jars and improve bond wording

* chore(fb): use Item component in StepIntro

* chore(ui): update Card component

* chore(earn): always show action buttons for expired fidelity bonds

* chore: remove obsolete responsive test for Card component

---------

Co-authored-by: theborakompanioni <theborakompanioni+github@gmail.com>
2026-07-25 02:19:38 +02:00

119 lines
4.2 KiB
TypeScript

import type { FidelityBondUtxo, Utxo } from '@/hooks/useQueryUtxos'
import type { Milliseconds, MM, Seconds, YYYY } from '@/types/global'
export type Lockdate = `${YYYY}-${MM}`
export type YearsRange = {
min: number
max: number
}
export const toYearsRange = (min: number, max: number): YearsRange => {
if (max <= min) {
throw new Error('Invalid values for range of years.')
}
return { min, max }
}
// A maximum of years for a timelock to be accepted.
// This is useful in simple mode - when it should be prevented that users
// lock up their coins for an awful amount of time by accident.
export const DEFAULT_MAX_TIMELOCK_YEARS = 10
export const DEFAULT_TIMELOCK_YEARS_RANGE = toYearsRange(0, DEFAULT_MAX_TIMELOCK_YEARS)
// The months ahead for the initial lock date.
// It is recommended to start locking for a period of between 3 months and 1 years initially.
// This value should be at the lower end of this recommendation.
// See https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/fidelity-bonds.md#what-amount-of-bitcoins-to-lock-up-and-for-how-long
// for more information (last checked on 2022-06-13).
// Exported for tests only!
export const __INITIAL_LOCKDATE_MONTH_AHEAD = 3
export const lockdate = (() => {
const _fromDate = (date: Date): Lockdate => {
return `${date.getUTCFullYear()}-${date.getUTCMonth() >= 9 ? '' : '0'}${1 + date.getUTCMonth()}` as Lockdate
}
const fromTimestamp = (timestamp: Milliseconds): Lockdate => {
if (Number.isNaN(timestamp)) throw new Error('Unsupported input: NaN')
return _fromDate(new Date(timestamp))
}
const toTimestamp = (lockdate: Lockdate): Milliseconds => {
const split = lockdate.split('-')
if (split.length !== 2 || split[0].length !== 4 || split[1].length !== 2) {
throw new TypeError('Unsupported format')
}
const year = Number.parseInt(split[0], 10)
const month = Number.parseInt(split[1], 10)
if (Number.isNaN(year) || Number.isNaN(month)) {
throw new TypeError('Unsupported format')
}
return Date.UTC(year, month - 1, 1)
}
const toDateLabel = (lockdate: Lockdate): string =>
new Date(toTimestamp(lockdate)).toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
})
/**
* Returns a lockdate an initial lockdate in the future.
*
* This method tries to provide a date that is at least
* {@link __INITIAL_LOCKDATE_MONTH_AHEAD} months after {@link now}.
*
* @param now the reference date
* @param range a min/max range of years
* @returns an initial lockdate
*/
const initial = (now: Date, range: YearsRange = DEFAULT_TIMELOCK_YEARS_RANGE): Lockdate => {
const year = now.getUTCFullYear()
const month = now.getUTCMonth()
const minMonthAhead = Math.max(range.min * 12, __INITIAL_LOCKDATE_MONTH_AHEAD + 1)
const initYear = year + Math.floor((month + minMonthAhead) / 12)
const initMonth = (month + minMonthAhead) % 12
return fromTimestamp(Date.UTC(initYear, initMonth, 1))
}
return {
fromTimestamp,
toTimestamp,
toDateLabel,
initial,
}
})()
export const utxo = (() => {
const isEqual = (lhs: Utxo, rhs: Utxo) => lhs.utxo === rhs.utxo
const isInList = (utxo: Utxo, list: Array<Utxo>) => list.some((it) => isEqual(it, utxo))
const utxosToFreeze = (allUtxos: Array<Utxo>, fbUtxos: Array<Utxo>) =>
allUtxos.filter((utxo) => !isInList(utxo, fbUtxos))
const allAreFrozen = (utxos: Array<Utxo>) => utxos.every((utxo) => utxo.frozen)
const isFidelityBond = (utxo: Utxo): utxo is FidelityBondUtxo => !!utxo.locktime
const getLocktime = (utxo: Utxo): Milliseconds | null => {
if (!isFidelityBond(utxo)) return null
const pathAndLocktime = utxo.path.split(':')
if (pathAndLocktime.length !== 2) return null
const locktimeUnixTimestamp: Seconds = Number.parseInt(pathAndLocktime[1], 10)
if (Number.isNaN(locktimeUnixTimestamp)) return null
return locktimeUnixTimestamp * 1_000
}
const isLocked = (utxo: Utxo, refTime: Milliseconds = Date.now()) => {
const locktime = getLocktime(utxo)
return locktime !== null && locktime >= refTime
}
return { isEqual, isInList, utxosToFreeze, allAreFrozen, isFidelityBond, isLocked, getLocktime }
})()