mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-19 13:18:30 +02:00
chore(lint): enable eslint recommendedTypeChecked WIP
This commit is contained in:
parent
e4fc0daae1
commit
ea0b0b3fa6
44 changed files with 184 additions and 180 deletions
|
|
@ -76,11 +76,11 @@ export default function MainWalletPage({ walletFileName }: MainWalletPageProps)
|
|||
)}
|
||||
</div>
|
||||
<div className="mt-2 flex w-full justify-center gap-4">
|
||||
<Button size="xxl" className="flex-1" variant="default" onClick={() => navigate(routes.receive)}>
|
||||
<Button size="xxl" className="flex-1" variant="default" onClick={() => void navigate(routes.receive)}>
|
||||
<DownloadIcon />
|
||||
{t('current_wallet.button_deposit')}
|
||||
</Button>
|
||||
<Button size="xxl" className="flex-1" variant="outline" onClick={() => navigate(routes.send)}>
|
||||
<Button size="xxl" className="flex-1" variant="outline" onClick={() => void navigate(routes.send)}>
|
||||
<UploadIcon />
|
||||
{t('current_wallet.button_withdraw')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ const SwitchWalletPage = ({ walletFileName }: SwitchWalletPageProps) => {
|
|||
{listWalletsFetching ? (
|
||||
<Spinner className="size-6" />
|
||||
) : (
|
||||
<WalletIcon className="text-primary h-6 w-6" onClick={async () => await listWalletsRefetch()} />
|
||||
<WalletIcon className="text-primary h-6 w-6" onClick={() => void listWalletsRefetch()} />
|
||||
)}
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold">{t('settings.button_switch_wallet')}</CardTitle>
|
||||
|
|
@ -132,7 +132,7 @@ const SwitchWalletPage = ({ walletFileName }: SwitchWalletPageProps) => {
|
|||
<AlertTitle>{listWalletsErrorAlert.message}</AlertTitle>
|
||||
<AlertDescription>{listWalletsErrorAlert.error_description}</AlertDescription>
|
||||
</Alert>
|
||||
<Button variant="ghost" size="sm" onClick={async () => await listWalletsRefetch()}>
|
||||
<Button variant="ghost" size="sm" onClick={() => void listWalletsRefetch()}>
|
||||
<RefreshCwIcon className="h-4 w-4" /> {t('global.retry')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
|
@ -177,7 +177,7 @@ const SwitchWalletPage = ({ walletFileName }: SwitchWalletPageProps) => {
|
|||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(routes.home)}
|
||||
onClick={() => void navigate(routes.home)}
|
||||
className="flex-1"
|
||||
>
|
||||
<WalletIcon className="mr-2 h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export const CreateStepConfirm = ({ walletFileName, password, seedphrase, onConf
|
|||
</div>
|
||||
|
||||
<Button
|
||||
onClick={async () => await onConfirm()}
|
||||
onClick={() => void onConfirm()}
|
||||
className="w-full"
|
||||
size="xxl"
|
||||
disabled={!backupConfirmed || !revealSensitiveInfo.dirty}
|
||||
|
|
|
|||
|
|
@ -83,8 +83,10 @@ export const CreateWalletForm = ({
|
|||
resolver: yupResolver(schema),
|
||||
})
|
||||
|
||||
const doOnSubmit = handleSubmit(onSubmit)
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={cn('flex flex-col gap-4', className)} noValidate>
|
||||
<form onSubmit={(event) => void doOnSubmit(event)} className={cn('flex flex-col gap-4', className)} noValidate>
|
||||
<div className="space-y-2">
|
||||
<Field data-invalid={errors.walletName !== undefined}>
|
||||
<FieldLabel htmlFor="create-wallet-name">{t('create_wallet.label_wallet_name')}</FieldLabel>
|
||||
|
|
|
|||
|
|
@ -158,8 +158,10 @@ export function EarnForm({
|
|||
const values = useWatch({ control })
|
||||
const watchOfferType = useWatch({ control, name: 'offerType' })
|
||||
|
||||
const doOnSubmit = handleSubmit(onSubmit)
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={cn('flex flex-col gap-4', className)} noValidate>
|
||||
<form onSubmit={(event) => void doOnSubmit(event)} className={cn('flex flex-col gap-4', className)} noValidate>
|
||||
<OfferTypeInput
|
||||
disabled={disabled}
|
||||
defaultValue={FORM_INPUT_DEFAULT_VALUES.offerType}
|
||||
|
|
|
|||
|
|
@ -49,16 +49,23 @@ function ErrorView({ title, subtitle, reason, stacktrace }: ErrorViewProps) {
|
|||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function UnknownError({ error }: { error: any }) {
|
||||
function UnknownError({ error }: { error: unknown }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const title = t('error_page.unknown_error.title')
|
||||
const subtitle = t('error_page.unknown_error.subtitle')
|
||||
if (!error || typeof error !== 'object') {
|
||||
return <ErrorView title={title} subtitle={subtitle} reason={t('global.errors.reason_unknown')} />
|
||||
}
|
||||
|
||||
const reason = 'message' in error && error.message ? String(error.message) : undefined
|
||||
const stacktrace = 'stack' in error && error.stack ? String(error.stack) : undefined
|
||||
return (
|
||||
<ErrorView
|
||||
title={t('error_page.unknown_error.title')}
|
||||
subtitle={t('error_page.unknown_error.subtitle')}
|
||||
reason={error.message || t('global.errors.reason_unknown')}
|
||||
stacktrace={error.stack}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
reason={reason || t('global.errors.reason_unknown')}
|
||||
stacktrace={stacktrace}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ export function AppNavbar({
|
|||
className="light:text-green-600 text-green-300"
|
||||
variant="ghost-navbar"
|
||||
size="icon"
|
||||
onClick={() => navigate(rescanningRoute)}
|
||||
onClick={() => void navigate(rescanningRoute)}
|
||||
aria-label={t('navbar.text_rescan_in_progress')}
|
||||
title={t('navbar.text_rescan_in_progress')}
|
||||
>
|
||||
|
|
@ -213,7 +213,7 @@ export function AppNavbar({
|
|||
className="light:text-green-600 text-green-300"
|
||||
variant="ghost-navbar"
|
||||
size="icon"
|
||||
onClick={() => navigate(joiningRoute)}
|
||||
onClick={() => void navigate(joiningRoute)}
|
||||
aria-label={t('navbar.joining_in_progress')}
|
||||
title={t('navbar.joining_in_progress')}
|
||||
>
|
||||
|
|
@ -224,7 +224,7 @@ export function AppNavbar({
|
|||
<Button
|
||||
variant="ghost-navbar"
|
||||
size="icon"
|
||||
onClick={() => navigate(routes.settings)}
|
||||
onClick={() => void navigate(routes.settings)}
|
||||
aria-label={t('navbar.menu_mobile_settings')}
|
||||
title={t('navbar.menu_mobile_settings')}
|
||||
>
|
||||
|
|
@ -234,7 +234,7 @@ export function AppNavbar({
|
|||
className="hidden sm:flex"
|
||||
variant="ghost-navbar"
|
||||
size="icon"
|
||||
onClick={doOnLockWallet}
|
||||
onClick={() => void doOnLockWallet()}
|
||||
aria-label={t('settings.button_lock_wallet')}
|
||||
title={t('settings.button_lock_wallet')}
|
||||
disabled={isLockingWallet}
|
||||
|
|
@ -245,7 +245,7 @@ export function AppNavbar({
|
|||
className="hidden sm:flex"
|
||||
variant="ghost-navbar"
|
||||
size="icon"
|
||||
onClick={async () => await onLogout(navigate)}
|
||||
onClick={() => void onLogout(navigate)}
|
||||
aria-label={/* TODO: i18n */ 'Logout'}
|
||||
title={/* TODO: i18n */ 'Logout'}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function AppSidebar({ side }: Pick<React.ComponentProps<typeof Sidebar>,
|
|||
|
||||
const isDeveloperMode = useStore(jamSettingsStore, (state) => state.state.developerMode)
|
||||
|
||||
const { isLogsEnabled } = useFeatures()
|
||||
const { isFeatureEnabled } = useFeatures()
|
||||
const mainItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
|
@ -96,7 +96,7 @@ export function AppSidebar({ side }: Pick<React.ComponentProps<typeof Sidebar>,
|
|||
url: routes.rescan,
|
||||
icon: PackageSearchIcon,
|
||||
},
|
||||
...(!isLogsEnabled
|
||||
...(!isFeatureEnabled('logs')
|
||||
? []
|
||||
: [
|
||||
{
|
||||
|
|
@ -106,7 +106,7 @@ export function AppSidebar({ side }: Pick<React.ComponentProps<typeof Sidebar>,
|
|||
},
|
||||
]),
|
||||
],
|
||||
[t, isLogsEnabled],
|
||||
[t, isFeatureEnabled],
|
||||
)
|
||||
|
||||
const devItems = useMemo(
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export function LayoutInner({ onLogout, onLockWallet, children }: LayoutInnerPro
|
|||
const cheatsheet = useCheatsheet()
|
||||
const [isOrderbookOverlayOpen, setIsOrderbookOverlayOpen] = useState(false)
|
||||
const [isLogsOverlayOpen, setIsLogsOverlayOpen] = useState(false)
|
||||
const { isLogsEnabled } = useFeatures()
|
||||
const { isFeatureEnabled } = useFeatures()
|
||||
|
||||
// Adds a keyboard shortcut to toggle the logs overlay.
|
||||
useEffect(() => {
|
||||
|
|
@ -91,7 +91,7 @@ export function LayoutInner({ onLogout, onLockWallet, children }: LayoutInnerPro
|
|||
joinmarketVersion={joinmarketVersion}
|
||||
onClickCheatsheet={() => cheatsheet.onOpenChange(true)}
|
||||
onClickOrderbook={() => setIsOrderbookOverlayOpen(true)}
|
||||
onClickLogs={isLogsEnabled ? () => setIsLogsOverlayOpen(true) : undefined}
|
||||
onClickLogs={isFeatureEnabled('logs') ? () => setIsLogsOverlayOpen(true) : undefined}
|
||||
/>
|
||||
|
||||
<Cheatsheet open={cheatsheet.open} onOpenChange={cheatsheet.onOpenChange} />
|
||||
|
|
|
|||
|
|
@ -92,8 +92,10 @@ export const LoginFormComponent = ({
|
|||
|
||||
const values = useWatch({ control })
|
||||
|
||||
const doOnSubmit = handleSubmit(onSubmit)
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={cn('flex flex-col gap-4', className)} noValidate>
|
||||
<form onSubmit={(event) => void doOnSubmit(event)} className={cn('flex flex-col gap-4', className)} noValidate>
|
||||
<div className="space-y-2">
|
||||
<Field data-invalid={errors.walletFileName !== undefined}>
|
||||
<FieldLabel>{/* TODO: i18n */}Wallet</FieldLabel>
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ const LoginPage = () => {
|
|||
{listWalletsFetching ? (
|
||||
<Spinner className="size-6" />
|
||||
) : (
|
||||
<WalletIcon className="text-primary" onClick={async () => await listWalletsRefetch()} />
|
||||
<WalletIcon className="text-primary" onClick={() => void listWalletsRefetch()} />
|
||||
)}
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold">{/*TODO: i18n */}Welcome to Jam</CardTitle>
|
||||
|
|
@ -137,7 +137,7 @@ const LoginPage = () => {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={async () => await listWalletsRefetch()}
|
||||
onClick={() => void listWalletsRefetch()}
|
||||
disabled={listWalletsFetching}
|
||||
>
|
||||
<RefreshCwIcon className={cn({ 'motion-safe:animate-spin': listWalletsFetching })} />
|
||||
|
|
@ -172,10 +172,10 @@ const LoginPage = () => {
|
|||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button variant="link" size="sm" onClick={async () => await navigate(routes.createWallet)}>
|
||||
<Button variant="link" size="sm" onClick={() => void navigate(routes.createWallet)}>
|
||||
{t('wallets.button_new_wallet')}
|
||||
</Button>
|
||||
<Button variant="link" size="sm" onClick={async () => await navigate('/import-wallet')} disabled>
|
||||
<Button variant="link" size="sm" onClick={() => void navigate('/import-wallet')} disabled>
|
||||
{/* TODO: implement "import wallet" */}
|
||||
{t('wallets.button_import_wallet')}
|
||||
<Badge variant="destructive">Not yet implemented.</Badge>
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ export const ReceivePage = ({ walletFileName }: ReceivePageProps) => {
|
|||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => await shareAddress(getAddressQuery.data!.address)}
|
||||
onClick={() => void shareAddress(getAddressQuery.data!.address)}
|
||||
disabled={getAddressQuery.isFetching || !getAddressQuery.data?.address}
|
||||
>
|
||||
<ShareIcon />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo, useState, type ComponentProps } from 'react'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { getaddress, type ErrorMessage } from '@joinmarket-webui/joinmarket-api-ts/jm'
|
||||
import { getAddressInfo, validate as isValidBitcoinAddress } from 'bitcoin-address-validation'
|
||||
import { getAddressInfo, validate as isValidBitcoinAddress, Network } from 'bitcoin-address-validation'
|
||||
import type { AddressInfo } from 'bitcoin-address-validation'
|
||||
import { BrushCleaningIcon, MilkIcon, XIcon } from 'lucide-react'
|
||||
import { useForm, useWatch } from 'react-hook-form'
|
||||
|
|
@ -36,8 +36,8 @@ import type { SendFormValues } from './types'
|
|||
|
||||
type AddressFromJarSelectorDialog = Omit<ComponentProps<typeof JarSelectorDialog>, 'onConfirm'> & {
|
||||
walletFileName: WalletFileName
|
||||
onError: (error: ErrorMessage) => Promise<void>
|
||||
onConfirm: (jar: JarIndex, address: AddressInfo) => Promise<void>
|
||||
onError: (error: ErrorMessage) => void
|
||||
onConfirm: (jar: JarIndex, address: AddressInfo) => void
|
||||
}
|
||||
|
||||
const AddressFromJarSelectorDialog = ({
|
||||
|
|
@ -62,12 +62,12 @@ const AddressFromJarSelectorDialog = ({
|
|||
}),
|
||||
)
|
||||
if (result.error) {
|
||||
await onError(result.error)
|
||||
onError(result.error)
|
||||
} else if (result.data?.address === undefined) {
|
||||
await onError(new Error('Missing bitcoin address.'))
|
||||
onError(new Error('Missing bitcoin address.'))
|
||||
} else {
|
||||
const addressInfo = getAddressInfo(result.data.address)
|
||||
await onConfirm(selectedJarIndex, addressInfo)
|
||||
onConfirm(selectedJarIndex, addressInfo)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -255,6 +255,8 @@ export function SendForm({
|
|||
return jars.find((it) => it.jarIndex === destinationJarIndex)
|
||||
}, [jars, destinationJarIndex])
|
||||
|
||||
const doOnSubmit = handleSubmit(onSubmit)
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddressFromJarSelectorDialog
|
||||
|
|
@ -265,20 +267,20 @@ export function SendForm({
|
|||
jars={jars}
|
||||
disabledJars={sourceJar === undefined ? [] : [sourceJar]}
|
||||
walletBalanceSummary={walletBalanceSummary}
|
||||
onError={async (_ignoredOnPurpose) => {
|
||||
onError={(_ignoredOnPurpose) => {
|
||||
// TODO: i18n own key `send.error_loading_address_failed`
|
||||
toast.error(t('receive.error_loading_address_failed'))
|
||||
setValue('destination.address', undefined, { shouldValidate: true })
|
||||
setValue('destination.fromJar', undefined, { shouldValidate: true })
|
||||
}}
|
||||
onConfirm={async (jarIndex, addressInfo) => {
|
||||
onConfirm={(jarIndex, addressInfo) => {
|
||||
setValue('destination.address', addressInfo.address, { shouldValidate: true })
|
||||
setValue('destination.fromJar', jarIndex, { shouldValidate: true })
|
||||
|
||||
setShowAddressFromJarSelectorDialog(false)
|
||||
}}
|
||||
/>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={cn('flex flex-col gap-4', className)} noValidate>
|
||||
<form onSubmit={(event) => void doOnSubmit(event)} className={cn('flex flex-col gap-4', className)} noValidate>
|
||||
<div className="space-y-2">
|
||||
<Field className="space-y-4" data-invalid={errors.source !== undefined}>
|
||||
<FieldLabel>{t('send.label_source_jar')}</FieldLabel>
|
||||
|
|
@ -317,7 +319,7 @@ export function SendForm({
|
|||
<Field data-invalid={errors.destination !== undefined}>
|
||||
<FieldLabel htmlFor="send-destination">
|
||||
{t('send.label_recipient')}
|
||||
{destinationAddressInfo?.network && destinationAddressInfo.network !== 'mainnet' && (
|
||||
{destinationAddressInfo?.network && destinationAddressInfo.network !== Network.mainnet && (
|
||||
<Badge variant="outline">{destinationAddressInfo.network}</Badge>
|
||||
)}
|
||||
</FieldLabel>
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ export const SendPage = ({ walletFileName }: SendPageProps) => {
|
|||
}
|
||||
}
|
||||
|
||||
const onSubmit: SubmitHandler<SendFormValues> = async (data) => {
|
||||
const onSubmit: SubmitHandler<SendFormValues> = (data) => {
|
||||
setSendFromValuesAwaitingConfirmation(data)
|
||||
setShowPaymentConfirmDialog(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ export const FeeLimitDialog = ({ open, onOpenChange, walletFileName }: FeeLimitD
|
|||
<DevBadge />
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleSubmit} disabled={isSubmitting || isLoadingConfig}>
|
||||
<Button onClick={() => void handleSubmit()} disabled={isSubmitting || isLoadingConfig}>
|
||||
{isSubmitting ? t('settings.fees.text_button_submitting') : t('settings.fees.text_button_submit')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import languages from '@/i18n/languages'
|
|||
export const LanguageSelector = () => {
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const handleLanguageChange = (languageKey: string) => {
|
||||
i18n.changeLanguage(languageKey)
|
||||
const handleLanguageChange = async (languageKey: string) => {
|
||||
await i18n.changeLanguage(languageKey)
|
||||
}
|
||||
|
||||
const getCurrentLanguageDescription = () => {
|
||||
|
|
@ -26,7 +26,7 @@ export const LanguageSelector = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Select value={i18n.resolvedLanguage} onValueChange={handleLanguageChange}>
|
||||
<Select value={i18n.resolvedLanguage} onValueChange={(value) => void handleLanguageChange(value)}>
|
||||
<SelectTrigger className="h-7 w-38 text-xs" aria-label="Select language">
|
||||
<SelectValue placeholder={getCurrentLanguageDescription()} />
|
||||
</SelectTrigger>
|
||||
|
|
|
|||
|
|
@ -51,9 +51,10 @@ function RescanChainForm({ rescanInfo, onSubmit, disabled }: RescanChainFormProp
|
|||
resolver: yupResolver(schema),
|
||||
})
|
||||
|
||||
const doOnSubmit = handleSubmit(onSubmit)
|
||||
|
||||
return (
|
||||
/* "handleSubmit" will validate your inputs before invoking "onSubmit" */
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<form onSubmit={(event) => void doOnSubmit(event)} className="space-y-4">
|
||||
{/* include validation with required or other standard HTML validation rules */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rescanHeight" className="text-sm font-medium">
|
||||
|
|
@ -157,7 +158,7 @@ export const RescanChainPage = ({ walletFileName }: RescanChainProps) => {
|
|||
return (
|
||||
<div className="mx-auto max-w-4xl space-y-3 p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" onClick={() => navigate(routes.settings)} title={t('global.back')}>
|
||||
<Button variant="ghost" onClick={() => void navigate(routes.settings)} title={t('global.back')}>
|
||||
<ArrowLeftIcon />
|
||||
<span className="sr-only">{t('global.back')}</span>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export const SeedPhraseDialog = ({
|
|||
|
||||
useEffect(() => {
|
||||
if (open && isPasswordVerified && seedQuery.data === undefined) {
|
||||
seedQuery.refetch()
|
||||
void seedQuery.refetch()
|
||||
}
|
||||
}, [open, isPasswordVerified, seedQuery])
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ type SettingsItemProps = PropsWithChildren<{
|
|||
renderIcon?: ({ className }: { className: string }) => ReactNode
|
||||
title: string
|
||||
disabled?: boolean
|
||||
action?: () => Promise<void>
|
||||
action?: () => void | Promise<void>
|
||||
}>
|
||||
|
||||
export const SettingItem = ({
|
||||
|
|
@ -27,7 +27,7 @@ export const SettingItem = ({
|
|||
'hover:bg-muted/50 -mx-2 cursor-pointer rounded-md px-2': !disabled,
|
||||
'cursor-not-allowed opacity-60': disabled,
|
||||
})}
|
||||
onClick={!disabled ? action : undefined}
|
||||
onClick={!disabled && action ? () => void action() : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-muted/50 flex h-7 w-7 items-center justify-center rounded-lg border">
|
||||
|
|
@ -76,7 +76,7 @@ type SettingsSwitchProps = Omit<SettingsItemProps, 'children' | 'action'> & {
|
|||
}
|
||||
export const SettingSwitch = ({ checked, onCheckedChange, displayToggle = true, ...props }: SettingsSwitchProps) => {
|
||||
return (
|
||||
<SettingItem {...props} action={async () => onCheckedChange && onCheckedChange(!checked)}>
|
||||
<SettingItem {...props} action={() => onCheckedChange && onCheckedChange(!checked)}>
|
||||
{displayToggle && <Switch checked={checked} onCheckedChange={onCheckedChange} disabled={props.disabled} />}
|
||||
</SettingItem>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps)
|
|||
const [showXpubsDialog, setShowXpubsDialog] = useState(false)
|
||||
const [showFeeLimitDialog, setShowFeeLimitDialog] = useState(false)
|
||||
const hashedPassword = useStore(authStore, (state) => state.state?.hashed_password)
|
||||
const { isLogsEnabled } = useFeatures()
|
||||
const { isFeatureEnabled } = useFeatures()
|
||||
|
||||
const [isLockingWallet, setIsLockingWallet] = useState(false)
|
||||
const doOnLockWallet = async () => {
|
||||
|
|
@ -115,9 +115,7 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps)
|
|||
<SettingItem
|
||||
icon={DollarSignIcon}
|
||||
title={t('settings.show_fee_config')}
|
||||
action={async () => {
|
||||
setShowFeeLimitDialog(true)
|
||||
}}
|
||||
action={() => setShowFeeLimitDialog(true)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -131,14 +129,14 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps)
|
|||
<SettingItem
|
||||
icon={KeyRoundIcon}
|
||||
title={t('settings.show_seed')}
|
||||
action={async () => setShowSeedDialog(true)}
|
||||
action={() => setShowSeedDialog(true)}
|
||||
disabled={hashedPassword === undefined}
|
||||
/>
|
||||
<Separator className="opacity-50" />
|
||||
<SettingItem
|
||||
icon={BookKeyIcon}
|
||||
title={t('settings.show_xpubs')}
|
||||
action={async () => setShowXpubsDialog(true)}
|
||||
action={() => setShowXpubsDialog(true)}
|
||||
disabled={hashedPassword === undefined}
|
||||
/>
|
||||
<Separator className="opacity-50" />
|
||||
|
|
@ -157,7 +155,7 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps)
|
|||
icon={FileTextIcon}
|
||||
title={t('settings.show_logs')}
|
||||
to={routes.logs}
|
||||
disabled={!isLogsEnabled}
|
||||
disabled={!isFeatureEnabled('logs')}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export const TxFeeInputField = ({
|
|||
<button
|
||||
key={tab.value}
|
||||
type="button"
|
||||
onClick={() => handleUnitChange(tab.value as TxFeeUnit)}
|
||||
onClick={() => handleUnitChange(tab.value)}
|
||||
className={cx('rounded-md px-4 py-2 text-sm font-medium transition-colors', {
|
||||
'bg-primary text-primary-foreground': unit === tab.value,
|
||||
'bg-muted text-muted-foreground hover:bg-muted/80': unit !== tab.value,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
import { useMemo } from 'react'
|
||||
import { CurrencySymbol } from '@/components/ui/jam/CurrencySymbol'
|
||||
import { useJamDisplayContext } from '@/context/JamDisplayContext'
|
||||
import { cn, satsToBtc, btcToSats, isValidNumber, formatBtc, formatSats, SATS, BTC } from '@/lib/utils'
|
||||
import { cn, satsToBtc, btcToSats, isValidNumber, formatBtc, formatSats, SATS, BTC, type Unit } from '@/lib/utils'
|
||||
import type { Currency } from '@/types/global'
|
||||
|
||||
const DISPLAY_MODE_BTC = 'btc'
|
||||
const DISPLAY_MODE_SATS = 'sats'
|
||||
const DISPLAY_MODE_BTC: Currency = 'btc'
|
||||
const DISPLAY_MODE_SATS: Currency = 'sats'
|
||||
const DISPLAY_MODE_HIDDEN = 'private'
|
||||
|
||||
const getDisplayMode = (
|
||||
unit: typeof SATS | typeof BTC | undefined,
|
||||
currency: Currency,
|
||||
isPrivate: boolean,
|
||||
showBalance: boolean,
|
||||
) => {
|
||||
const getDisplayMode = (unit: Unit | undefined, currency: Currency, isPrivate: boolean, showBalance: boolean) => {
|
||||
if (!showBalance || isPrivate) return DISPLAY_MODE_HIDDEN
|
||||
|
||||
// If convertToUnit is specified, respect it, otherwise use context
|
||||
|
|
@ -129,7 +124,7 @@ const SatsBalance = ({ value, colored = true, ...props }: SatsBalanceProps) => {
|
|||
|
||||
interface BalanceProps extends Omit<BalanceComponentProps, 'symbol' | 'children'> {
|
||||
valueString: string
|
||||
convertToUnit?: typeof SATS | typeof BTC
|
||||
convertToUnit?: Unit
|
||||
showBalance?: boolean
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ function Copyable({
|
|||
type="button"
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
onClick={() => copyToClipboard(value, valueFallbackInputRef.current!).then(onSuccess, onError)}
|
||||
onClick={() => void copyToClipboard(value, valueFallbackInputRef.current!).then(onSuccess, onError)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export const LockWalletConfirmDialog = ({
|
|||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isLocking}>
|
||||
{t('global.cancel')}
|
||||
</Button>
|
||||
<Button variant="default" onClick={onConfirm} disabled={isLocking}>
|
||||
<Button variant="default" onClick={() => void onConfirm()} disabled={isLocking}>
|
||||
{isLocking ? (
|
||||
<>
|
||||
<Spinner className="motion-reduce:hidden" />
|
||||
|
|
|
|||
|
|
@ -76,8 +76,10 @@ export const PasswordVerificationForm = ({
|
|||
resolver: yupResolver(schema),
|
||||
})
|
||||
|
||||
const doOnSubmit = handleSubmit(onSubmit)
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={cn('flex flex-col gap-4', className)} noValidate>
|
||||
<form onSubmit={(event) => void doOnSubmit(event)} className={cn('flex flex-col gap-4', className)} noValidate>
|
||||
<div className="space-y-2">
|
||||
<Field data-invalid={errors.password !== undefined}>
|
||||
<FieldLabel htmlFor="password-verification-input-password">{t(/* TODO: i18n */ 'Password')}</FieldLabel>
|
||||
|
|
@ -111,7 +113,7 @@ export const PasswordVerificationForm = ({
|
|||
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
{onCancel !== undefined && (
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
<Button type="button" variant="outline" onClick={() => void onCancel()}>
|
||||
{t('global.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ const utxoTableColumns = (t: TFunction): ColumnDef<UtxoTableEntry, any>[] => {
|
|||
}),
|
||||
columnHelper.accessor('confirmations', {
|
||||
header: () => t('jar_details.utxo_list.column_title_confirmations'),
|
||||
cell: (info) => info.getValue(),
|
||||
cell: (info) => <>{info.getValue()}</>,
|
||||
meta: {
|
||||
numeric: true,
|
||||
align: 'center',
|
||||
|
|
|
|||
|
|
@ -192,7 +192,11 @@ export const UtxosContent = ({ enabled, walletFileName, addressSummary, jar }: U
|
|||
</div>
|
||||
</div>
|
||||
<div className={cn('flex items-center gap-2', {})}>
|
||||
<Button size="sm" disabled={!operationsEnabled || walletInfo.isFetching} onClick={() => walletInfo.refetch()}>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!operationsEnabled || walletInfo.isFetching}
|
||||
onClick={() => void walletInfo.refetch()}
|
||||
>
|
||||
<RefreshCwIcon className={cn({ 'motion-safe:animate-spin': walletInfo.isFetching })} />
|
||||
{t('global.refresh')}
|
||||
</Button>
|
||||
|
|
@ -201,7 +205,7 @@ export const UtxosContent = ({ enabled, walletFileName, addressSummary, jar }: U
|
|||
size="sm"
|
||||
variant={selectedUtxos.length === 0 ? 'outline' : undefined}
|
||||
disabled={!operationsEnabled || selectedUtxos.length === 0 || allSelectedUtxosFrozen}
|
||||
onClick={onFreezeClick}
|
||||
onClick={() => void onFreezeClick()}
|
||||
>
|
||||
{freezeUtxos.isPending ? <Spinner /> : <ThermometerSnowflakeIcon />}
|
||||
{t('jar_details.utxo_list.button_freeze')}
|
||||
|
|
@ -210,7 +214,7 @@ export const UtxosContent = ({ enabled, walletFileName, addressSummary, jar }: U
|
|||
size="sm"
|
||||
variant={selectedUtxos.length === 0 ? 'outline' : undefined}
|
||||
disabled={!operationsEnabled || selectedUtxos.length === 0 || allSelectedUtxosUnfrozen}
|
||||
onClick={onUnfreezeClick}
|
||||
onClick={() => void onUnfreezeClick()}
|
||||
>
|
||||
{unfreezeUtxos.isPending ? <Spinner /> : <ThermometerSunIcon />}
|
||||
{t('jar_details.utxo_list.button_unfreeze')}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { percentageToFactor, parseSemanticVersion } from '@/lib/utils'
|
|||
import type { AmountSats, Milliseconds } from '@/types/global'
|
||||
import { version as packageInfoVersion } from '../../package.json'
|
||||
import { JM_API_AUTH_TOKEN_EXPIRY, JM_DUST_THRESHOLD, JM_WALLET_FILE_EXTENSION } from './jm'
|
||||
import { parseAsIntOrDefault } from './meta-env-utils'
|
||||
|
||||
export const APP_DISPLAY_VERSION = (() => {
|
||||
return parseSemanticVersion(packageInfoVersion)
|
||||
|
|
@ -41,7 +42,7 @@ export const OFFER_MINSIZE_DEFAULT: AmountSats = 100_000
|
|||
export const JAM_JM_SESSION_REFRESH_MIN_INTERVAL: Milliseconds = 5_000
|
||||
export const JAM_JM_SESSION_REFRESH_DEFAULT_INTERVAL: Milliseconds = 30_000
|
||||
export const JAM_JM_SESSION_REFRESH_INTERVAL: Milliseconds = Math.max(
|
||||
import.meta.env.VITE_JAM_JM_SESSION_REFRESH_INTERVAL ?? JAM_JM_SESSION_REFRESH_DEFAULT_INTERVAL,
|
||||
parseAsIntOrDefault(import.meta.env.VITE_JAM_JM_SESSION_REFRESH_INTERVAL, JAM_JM_SESSION_REFRESH_DEFAULT_INTERVAL),
|
||||
JAM_JM_SESSION_REFRESH_MIN_INTERVAL,
|
||||
)
|
||||
export const JAM_API_AUTH_TOKEN_RENEW_INTERVAL: Milliseconds = Math.round(JM_API_AUTH_TOKEN_EXPIRY * 0.75)
|
||||
|
|
@ -49,29 +50,31 @@ export const JAM_API_AUTH_TOKEN_RENEW_INTERVAL: Milliseconds = Math.round(JM_API
|
|||
export const JAM_RESCAN_PROGRESS_MIN_INTERVAL: Milliseconds = 5_000
|
||||
export const JAM_RESCAN_PROGRESS_DEFAULT_INTERVAL: Milliseconds = 21_000
|
||||
export const JAM_RESCAN_PROGRESS_INTERVAL: Milliseconds = Math.max(
|
||||
import.meta.env.VITE_JAM_RESCAN_PROGRESS_INTERVAL ?? JAM_RESCAN_PROGRESS_DEFAULT_INTERVAL,
|
||||
parseAsIntOrDefault(import.meta.env.VITE_JAM_RESCAN_PROGRESS_INTERVAL, JAM_RESCAN_PROGRESS_DEFAULT_INTERVAL),
|
||||
JAM_RESCAN_PROGRESS_MIN_INTERVAL,
|
||||
)
|
||||
|
||||
const JAM_SEED_MODAL_MIN_TIMEOUT: Milliseconds = 5_000
|
||||
const JAM_SEED_MODAL_DEFAULT_TIMEOUT: Milliseconds = 30_000
|
||||
export const JAM_SEED_MODAL_TIMEOUT: Milliseconds = Math.max(
|
||||
import.meta.env.VITE_JAM_SEED_MODAL_TIMEOUT ?? JAM_SEED_MODAL_DEFAULT_TIMEOUT,
|
||||
parseAsIntOrDefault(import.meta.env.VITE_JAM_SEED_MODAL_TIMEOUT, JAM_SEED_MODAL_DEFAULT_TIMEOUT),
|
||||
JAM_SEED_MODAL_MIN_TIMEOUT,
|
||||
)
|
||||
|
||||
// minimum amount of time in milliseconds the connection must stay open to be considered "healthy"
|
||||
const JAM_JM_WEBSOCKET_CONNECTION_HEALTHY_MIN_DURATION: Milliseconds = 1_000
|
||||
export const JAM_JM_WEBSOCKET_CONNECTION_HEALTHY_DURATION: Milliseconds = Math.max(
|
||||
import.meta.env.VITE_JM_WEBSOCKET_CONNECTION_HEALTHY_DURATION ?? 0,
|
||||
parseAsIntOrDefault(import.meta.env.VITE_JM_WEBSOCKET_CONNECTION_HEALTHY_DURATION, 0),
|
||||
JAM_JM_WEBSOCKET_CONNECTION_HEALTHY_MIN_DURATION,
|
||||
)
|
||||
|
||||
const JAM_JM_WEBSOCKET_CONNECTION_AUTHENTICATED_MIN_DURATION: Milliseconds = 1_000
|
||||
const JAM_JM_WEBSOCKET_CONNECTION_AUTHENTICATED_DEFAULT_DURATION: Milliseconds = 3_000
|
||||
export const JAM_JM_WEBSOCKET_CONNECTION_AUTHENTICATED_DURATION: Milliseconds = Math.max(
|
||||
import.meta.env.VITE_JM_WEBSOCKET_CONNECTION_AUTHENTICATED_DURATION ??
|
||||
parseAsIntOrDefault(
|
||||
import.meta.env.VITE_JM_WEBSOCKET_CONNECTION_AUTHENTICATED_DURATION,
|
||||
JAM_JM_WEBSOCKET_CONNECTION_AUTHENTICATED_DEFAULT_DURATION,
|
||||
),
|
||||
JAM_JM_WEBSOCKET_CONNECTION_AUTHENTICATED_MIN_DURATION,
|
||||
)
|
||||
|
||||
|
|
@ -80,16 +83,19 @@ export const JAM_JM_WEBSOCKET_CONNECTION_AUTHENTICATED_DURATION: Milliseconds =
|
|||
const JAM_JM_WEBSOCKET_KEEPALIVE_MESSAGE_MIN_INTERVAL: Milliseconds = 5_000
|
||||
const JAM_JM_WEBSOCKET_KEEPALIVE_MESSAGE_DEFAULT_INTERVAL: Milliseconds = 30_000
|
||||
export const JAM_JM_WEBSOCKET_KEEPALIVE_MESSAGE_INTERVAL: Milliseconds = Math.max(
|
||||
import.meta.env.VITE_JM_WEBSOCKET_KEEPALIVE_MESSAGE_INTERVAL ?? JAM_JM_WEBSOCKET_KEEPALIVE_MESSAGE_DEFAULT_INTERVAL,
|
||||
parseAsIntOrDefault(
|
||||
import.meta.env.VITE_JM_WEBSOCKET_KEEPALIVE_MESSAGE_INTERVAL,
|
||||
JAM_JM_WEBSOCKET_KEEPALIVE_MESSAGE_DEFAULT_INTERVAL,
|
||||
),
|
||||
JAM_JM_WEBSOCKET_KEEPALIVE_MESSAGE_MIN_INTERVAL,
|
||||
)
|
||||
|
||||
export const JAM_JM_WEBSOCKET_RECONNECT_INTERVAL_MIN: Milliseconds = Math.max(
|
||||
import.meta.env.VITE_JM_WEBSOCKET_RECONNECT_INTERVAL_MIN ?? 0,
|
||||
parseAsIntOrDefault(import.meta.env.VITE_JM_WEBSOCKET_RECONNECT_INTERVAL_MIN, 0),
|
||||
5_000,
|
||||
)
|
||||
|
||||
export const JAM_JM_WEBSOCKET_RECONNECT_INTERVAL_MAX: Milliseconds = Math.max(
|
||||
import.meta.env.VITE_JM_WEBSOCKET_RECONNECT_INTERVAL_MAX ?? 0,
|
||||
parseAsIntOrDefault(import.meta.env.VITE_JM_WEBSOCKET_RECONNECT_INTERVAL_MAX, 0),
|
||||
60_000,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { AmountSats, Milliseconds } from '@/types/global'
|
||||
import { parseAsIntOrDefault } from './meta-env-utils'
|
||||
|
||||
export const JM_WALLET_FILE_EXTENSION = '.jmdat'
|
||||
|
||||
|
|
@ -9,7 +10,10 @@ export const JM_API_AUTH_TOKEN_EXPIRY_MAX: Milliseconds = Math.round(JM_API_AUTH
|
|||
export const JM_API_AUTH_TOKEN_EXPIRY: Milliseconds = Math.max(
|
||||
JM_API_AUTH_TOKEN_EXPIRY_MIN,
|
||||
Math.min(
|
||||
Number.parseInt(import.meta.env.VITE_JM_API_AUTH_TOKEN_EXPIRY_SECONDS, 10) * 1_000,
|
||||
parseAsIntOrDefault(
|
||||
import.meta.env.VITE_JM_API_AUTH_TOKEN_EXPIRY_SECONDS,
|
||||
JM_API_AUTH_TOKEN_EXPIRY_DEFAULT / 1_000,
|
||||
) * 1_000,
|
||||
JM_API_AUTH_TOKEN_EXPIRY_MAX,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
5
src/constants/meta-env-utils.ts
Normal file
5
src/constants/meta-env-utils.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- okay when trying to parse arbitrary value on purpose
|
||||
export const parseAsIntOrDefault = (value: any, defaultValue: number) => {
|
||||
const parsed = Number.parseInt(`${value ?? defaultValue}`, 10)
|
||||
return Number.isSafeInteger(parsed) ? parsed : defaultValue
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ import {
|
|||
type AccountMeta,
|
||||
type AccountSummary,
|
||||
type AddressMeta,
|
||||
type AddressStatus,
|
||||
type AddressSummary,
|
||||
type FidelityBondSummary,
|
||||
type Jar,
|
||||
|
|
@ -35,7 +34,7 @@ const toAccountSummary = (walletInfo: WalletInfoApiObject): AccountSummary => {
|
|||
})
|
||||
|
||||
const meta: AccountMeta = {
|
||||
jarIndex: Number.parseInt(String(__raw.account), 10) as JarIndex,
|
||||
jarIndex: Number.parseInt(String(__raw.account), 10),
|
||||
branches,
|
||||
__raw,
|
||||
}
|
||||
|
|
@ -58,7 +57,7 @@ const toAddressSummary = (accountSummary: AccountSummary): AddressSummary => {
|
|||
|
||||
const meta: AddressMeta = {
|
||||
address: __raw.address,
|
||||
status: __raw.status as AddressStatus,
|
||||
status: __raw.status,
|
||||
info: {
|
||||
bech32: info.bech32,
|
||||
network: info.network,
|
||||
|
|
@ -131,9 +130,9 @@ export const JamWalletInfoContextProvider = ({
|
|||
const walletBalanceSummary = toBalanceSummary(utxos)
|
||||
|
||||
const utxosByJarIndex = utxos.reduce((acc, utxo) => {
|
||||
const key = utxo.mixdepth as JarIndex
|
||||
const key: JarIndex = utxo.mixdepth
|
||||
acc[key] = acc[key] || []
|
||||
acc[key].push(utxo as Utxo)
|
||||
acc[key].push(utxo)
|
||||
return acc
|
||||
}, {} as UtxosByJarIndex)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,17 @@ import { isDevMode } from '@/constants/debugFeatures'
|
|||
import { fetchFeatures } from '@/lib/api/logs'
|
||||
import { authStore } from '@/store/authStore'
|
||||
|
||||
export interface Features {
|
||||
logs?: boolean
|
||||
}
|
||||
type SupportedFeature = 'logs' // add on demand
|
||||
|
||||
export interface FeatureItem {
|
||||
type FeatureItem = {
|
||||
name: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
type FeaturesApiResponse = {
|
||||
features: Record<string, boolean> | FeatureItem[]
|
||||
}
|
||||
|
||||
export const useFeatures = () => {
|
||||
const authState = useStore(authStore, (state) => state.state)
|
||||
|
||||
|
|
@ -37,41 +39,41 @@ export const useFeatures = () => {
|
|||
throw new Error(`Features request failed with status ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.features as Features
|
||||
return (await response.json()) as FeaturesApiResponse
|
||||
},
|
||||
enabled: !!authState?.auth?.token,
|
||||
retry: false,
|
||||
select: (data: FeaturesApiResponse): FeatureItem[] => {
|
||||
if (!data.features) {
|
||||
return []
|
||||
}
|
||||
// New format: { features: [{ name: 'logs', enabled: true }] }
|
||||
else if (Array.isArray(data.features)) {
|
||||
return data.features
|
||||
}
|
||||
// Old format: { features: { logs: true } }
|
||||
else if (typeof data.features === 'object') {
|
||||
return Object.entries(data.features).map(([name, enabled]) => ({ name, enabled }))
|
||||
} else {
|
||||
console.warn('Could not parse feature response. Disabling all optional features.')
|
||||
return []
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const isLogsSupported = () => {
|
||||
if (error) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (features) {
|
||||
// Old format: { features: { logs: true } }
|
||||
if (typeof features.logs === 'boolean') {
|
||||
return features.logs
|
||||
}
|
||||
|
||||
// New format: { features: [{ name: 'logs', enabled: true }] }
|
||||
if (Array.isArray(features)) {
|
||||
return features.some((feature: FeatureItem) => feature.name === 'logs' && feature.enabled === true)
|
||||
}
|
||||
}
|
||||
return false
|
||||
const isFeatureSupported = (featureName: SupportedFeature) => {
|
||||
return features?.some((feature) => feature.name === featureName && feature.enabled === true)
|
||||
}
|
||||
const isFeatureEnabled = (featureName: SupportedFeature) => {
|
||||
return isFeatureSupported(featureName) || isDevMode()
|
||||
}
|
||||
|
||||
// Show logs UI in dev mode even when not supported, but with unsupported message
|
||||
const isLogsEnabled = isLogsSupported() || (error && isDevMode())
|
||||
|
||||
return {
|
||||
features,
|
||||
error,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isLogsEnabled,
|
||||
isLogsSupported: isLogsSupported(),
|
||||
isFeatureSupported: isFeatureSupported,
|
||||
isFeatureEnabled: isFeatureEnabled,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const calcReconnectInterval = (attemptNumber: number): Milliseconds => {
|
|||
)
|
||||
}
|
||||
|
||||
const basePath: string = import.meta.env.VITE_JM_WEBSOCKET_ENDPOINT_PATH
|
||||
const basePath: string = String(import.meta.env.VITE_JM_WEBSOCKET_ENDPOINT_PATH)
|
||||
const basePathWithoutLeadingSlash = basePath.replace(/^\//, '') // remove leading slash
|
||||
|
||||
const { protocol, host } = window.location
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const resources = languages.reduce((acc, lng) => {
|
|||
}
|
||||
}, {})
|
||||
|
||||
i18n.use(LanguageDetector).use(initReactI18next).init({
|
||||
void i18n.use(LanguageDetector).use(initReactI18next).init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
void i18n.use(initReactI18next).init({
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
resources: { en: { translations: {} } },
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const buildAuthHeader = (token: string) => {
|
|||
/**
|
||||
* Validate response content type
|
||||
*/
|
||||
const withExpectedContentTypeOrThrow = async (response: Response, expectedContentType: string) => {
|
||||
const withExpectedContentTypeOrThrow = (response: Response, expectedContentType: string) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed with status ${response.status}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@ export const fetchOrderbook = async (): Promise<OrderbookResponse> => {
|
|||
throw new Error(`Failed to fetch orderbook: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const data: unknown = await response.json()
|
||||
|
||||
if (!data || !Array.isArray(data.offers)) {
|
||||
if (!data || typeof data !== 'object' || !('offers' in data) || !Array.isArray(data.offers)) {
|
||||
console.warn('Unexpected orderbook response structure:', data)
|
||||
return { offers: [], fidelitybonds: [] }
|
||||
}
|
||||
|
||||
return data
|
||||
return data as OrderbookResponse
|
||||
}
|
||||
|
||||
export const refreshOrderbook = async (): Promise<Response> => {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ describe('BalanceSummary', () => {
|
|||
frozen: true,
|
||||
} as Utxo,
|
||||
{
|
||||
value: 4,
|
||||
value: 5,
|
||||
mixdepth: 0,
|
||||
// unfrozen and expired
|
||||
frozen: false,
|
||||
|
|
@ -39,10 +39,9 @@ describe('BalanceSummary', () => {
|
|||
now,
|
||||
)
|
||||
|
||||
expect(balanceSummary).not.toBeNull()
|
||||
expect(balanceSummary!.calculatedTotalBalanceInSats).toBe(10)
|
||||
expect(balanceSummary!.calculatedAvailableBalanceInSats).toBe(5)
|
||||
expect(balanceSummary!.calculatedFrozenOrLockedBalanceInSats).toBe(5)
|
||||
expect(balanceSummary.calculatedTotalBalanceInSats).toBe(11)
|
||||
expect(balanceSummary.calculatedAvailableBalanceInSats).toBe(6)
|
||||
expect(balanceSummary.calculatedFrozenOrLockedBalanceInSats).toBe(5)
|
||||
})
|
||||
|
||||
it('should populate account balance data', () => {
|
||||
|
|
@ -71,9 +70,8 @@ describe('BalanceSummary', () => {
|
|||
now,
|
||||
)
|
||||
|
||||
expect(balanceSummary).not.toBeNull()
|
||||
expect(balanceSummary!.calculatedTotalBalanceInSats).toBe(677777777)
|
||||
expect(balanceSummary!.calculatedAvailableBalanceInSats).toBe(333333333)
|
||||
expect(balanceSummary!.calculatedFrozenOrLockedBalanceInSats).toBe(344444444)
|
||||
expect(balanceSummary.calculatedTotalBalanceInSats).toBe(677777777)
|
||||
expect(balanceSummary.calculatedAvailableBalanceInSats).toBe(333333333)
|
||||
expect(balanceSummary.calculatedFrozenOrLockedBalanceInSats).toBe(344444444)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -26,37 +26,8 @@ const calculateFrozenOrLockedBalance = (utxos: Utxo[], refTime: Milliseconds = D
|
|||
export const toBalanceSummary = (utxos: Utxo[], now?: Milliseconds): BalanceSummary => {
|
||||
const refTime = now !== undefined ? now : Date.now()
|
||||
|
||||
const utxosByAccount = (utxos ?? []).reduce(
|
||||
(acc, utxo) => {
|
||||
const key = `${utxo.mixdepth}`
|
||||
acc[key] = acc[key] || []
|
||||
acc[key].push(utxo as Utxo)
|
||||
return acc
|
||||
},
|
||||
{} as { [key: string]: Utxo[] },
|
||||
)
|
||||
|
||||
const totalCalculatedByAccount = Object.fromEntries(
|
||||
Object.entries(utxosByAccount).map(([account, utxos]) => {
|
||||
return [account, utxos.reduce((acc, utxo) => acc + utxo.value, 0)]
|
||||
}),
|
||||
)
|
||||
const frozenOrLockedCalculatedByAccount = Object.fromEntries(
|
||||
Object.entries(utxosByAccount).map(([account, utxos]) => {
|
||||
return [account, calculateFrozenOrLockedBalance(utxos, refTime)]
|
||||
}),
|
||||
)
|
||||
|
||||
const walletTotalCalculated: AmountSats = Object.values(totalCalculatedByAccount).reduce(
|
||||
(acc, totalSats) => acc + totalSats,
|
||||
0,
|
||||
)
|
||||
|
||||
const walletFrozenOrLockedCalculated: AmountSats = Object.values(frozenOrLockedCalculatedByAccount).reduce(
|
||||
(acc, frozenOrLockedSats) => acc + frozenOrLockedSats,
|
||||
0,
|
||||
)
|
||||
|
||||
const walletTotalCalculated: AmountSats = utxos.reduce((acc, utxo) => acc + utxo.value, 0)
|
||||
const walletFrozenOrLockedCalculated: AmountSats = calculateFrozenOrLockedBalance(utxos, refTime)
|
||||
const walletAvailableCalculated = walletTotalCalculated - walletFrozenOrLockedCalculated
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -10,18 +10,18 @@ const buildAuthHeader = (token: ApiToken): [string, string] => {
|
|||
return ['x-jm-authorization', `Bearer ${token}`]
|
||||
}
|
||||
|
||||
async function loggingRequestInterceptor(request: Request) {
|
||||
function loggingRequestInterceptor(request: Request) {
|
||||
console.debug('[onRequest]', request)
|
||||
return request
|
||||
}
|
||||
async function loggingResponseInterceptor(response: Response) {
|
||||
function loggingResponseInterceptor(response: Response) {
|
||||
console.debug('[onResponse]', response)
|
||||
return response
|
||||
}
|
||||
|
||||
const createJamAuthenticationMiddleware = () => {
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping -- false positive
|
||||
return async (request: Request) => {
|
||||
return (request: Request) => {
|
||||
const authState = authStore.getState().state
|
||||
if (authState?.auth?.token) {
|
||||
const authHeader = buildAuthHeader(authState.auth.token)
|
||||
|
|
@ -32,7 +32,7 @@ const createJamAuthenticationMiddleware = () => {
|
|||
}
|
||||
|
||||
export const createApiClient = (): Client => {
|
||||
const baseUrl: string = import.meta.env.VITE_JM_API_BASE_URL
|
||||
const baseUrl = String(import.meta.env.VITE_JM_API_BASE_URL)
|
||||
const clientOptions: ClientOptions = { baseUrl }
|
||||
|
||||
console.debug('Setting up JM API client…', clientOptions)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'
|
|||
import { DEFAULT_PBKDF_ITERATIONS, hashPassword } from './hash'
|
||||
|
||||
describe('hash', () => {
|
||||
it('DEFAULT_PBKDF_ITERATIONS', async () => {
|
||||
it('DEFAULT_PBKDF_ITERATIONS', () => {
|
||||
expect(DEFAULT_PBKDF_ITERATIONS).toBe(210_000)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -116,7 +116,9 @@ describe('setIntervalDebounced', () => {
|
|||
const error = new Error('Test error')
|
||||
const callback = vi.fn().mockRejectedValueOnce(error).mockResolvedValueOnce(undefined)
|
||||
const onTimerIdChanged = vi.fn()
|
||||
const onError = vi.fn((_, loop) => loop())
|
||||
const onError = vi.fn((_, loop) => {
|
||||
loop()
|
||||
})
|
||||
|
||||
setIntervalDebounced(callback, 1000, onTimerIdChanged, onError)
|
||||
|
||||
|
|
@ -406,7 +408,7 @@ describe('delayedPromise', () => {
|
|||
|
||||
// Initially the promise should not be resolved
|
||||
let resolved = false
|
||||
delayPromise.then(() => {
|
||||
void delayPromise.then(() => {
|
||||
resolved = true
|
||||
})
|
||||
|
||||
|
|
@ -417,13 +419,14 @@ describe('delayedPromise', () => {
|
|||
|
||||
// Now the promise should be resolved
|
||||
await expect(delayPromise).resolves.toBeUndefined()
|
||||
expect(resolved).toBe(true)
|
||||
})
|
||||
|
||||
it('should not resolve before 500ms', async () => {
|
||||
const delayPromise = delayedPromise(500)
|
||||
|
||||
let resolved = false
|
||||
delayPromise.then(() => {
|
||||
void delayPromise.then(() => {
|
||||
resolved = true
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -75,10 +75,11 @@ export function setIntervalDebounced(
|
|||
) {
|
||||
;(function loop() {
|
||||
onTimerIdChanged(
|
||||
setTimeout(async () => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
await callback()
|
||||
loop()
|
||||
Promise.resolve(callback())
|
||||
.then(() => loop())
|
||||
.catch((error) => onError(error, loop))
|
||||
} catch (error: unknown) {
|
||||
onError(error, loop)
|
||||
}
|
||||
|
|
@ -100,7 +101,7 @@ export const percentageToFactor = (val: number, precision = 6) => {
|
|||
return Number((val / 100).toFixed(precision))
|
||||
}
|
||||
|
||||
export const isValidNumber = (val: number | undefined | null): val is number => {
|
||||
export const isValidNumber = (val: unknown): val is number => {
|
||||
return val !== undefined && typeof val === 'number' && !Number.isNaN(val)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const {
|
|||
// https://vite.dev/config/
|
||||
export default defineConfig((): UserConfig => {
|
||||
if (!SUPPORTED_BACKENDS.includes(JAM_BACKEND)) {
|
||||
throw new Error(`Unsupported backend: Use one of ${SUPPORTED_BACKENDS}`)
|
||||
throw new Error(`Unsupported backend: Use one of [${SUPPORTED_BACKENDS.join(', ')}]`)
|
||||
}
|
||||
|
||||
if (JAM_BACKEND === BACKEND_STANDALONE && JAM_API_PORT === undefined) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue