mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-19 13:18:30 +02:00
* 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>
119 lines
4.2 KiB
TypeScript
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 }
|
|
})()
|