diff --git a/src/App.tsx b/src/App.tsx index 48d96e2e..d61f1d06 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,10 +27,11 @@ import { useApiClient } from '@/hooks/useApiClient' import { token } from '@/lib/jm-api/generated/client' import { sessionOptions } from '@/lib/jm-api/generated/client/@tanstack/react-query.gen' import { queryClient } from '@/lib/queryClient' -import { setIntervalDebounced } from '@/lib/utils' +import { setIntervalDebounced, type WalletFileName } from '@/lib/utils' import { authStore } from '@/store/authStore' import { jmSessionStore } from '@/store/jmSessionStore' import { isDebugFeatureEnabled } from './constants/debugFeatures' +import { useFeeConfigValidation } from './hooks/useFeeConfigValidation' import { jamSettingsStore } from './store/jamSettingsStore' const DevSetupPage = lazy(() => import('@/components/dev/DevSetupPage')) @@ -53,6 +54,7 @@ function App() { + {walletFileName && } } +function LoadFeeConfigData({ walletFileName }: { walletFileName: WalletFileName }) { + const { refetchAll } = useFeeConfigValidation({ walletFileName }) + + useEffect(() => { + refetchAll().catch(() => { + const isDevMode = jamSettingsStore.getState().state.developerMode + if (isDevMode) { + toast.error(`[DEV] Error while loading fee config data.`) + } + }) + }, [refetchAll]) + + return <> +} + export default App diff --git a/src/constants/jm.ts b/src/constants/jm.ts index e8a3d6f8..5182fb7f 100644 --- a/src/constants/jm.ts +++ b/src/constants/jm.ts @@ -9,13 +9,18 @@ type RelOfferType = 'sw0reloffer' type AbsOfferType = 'sw0absoffer' export type OfferType = RelOfferType | AbsOfferType | string -type SectionKey = string +export type SectionKey = string -interface ConfigKey { +export interface ConfigKey { section: SectionKey field: string } +export interface ConfigValue { + key: ConfigKey + value: string | null +} + export const FEE_CONFIG_KEYS: Record = { tx_fees: { section: 'POLICY', field: 'tx_fees' }, tx_fees_factor: { section: 'POLICY', field: 'tx_fees_factor' }, diff --git a/src/hooks/useFeeConfigValidation.ts b/src/hooks/useFeeConfigValidation.ts index 94da56d2..25e0a6b6 100644 --- a/src/hooks/useFeeConfigValidation.ts +++ b/src/hooks/useFeeConfigValidation.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { useMutation } from '@tanstack/react-query' import { FEE_CONFIG_KEYS } from '@/constants/jm' import { useApiClient } from '@/hooks/useApiClient' @@ -28,28 +28,20 @@ export const useFeeConfigValidation = ({ walletFileName }: UseFeeConfigValidatio const createConfigQuery = (configKey: keyof typeof FEE_CONFIG_KEYS) => ({ ...configgetMutation({ client, - path: { walletname: walletFileName || '' }, + path: { walletname: walletFileName }, body: FEE_CONFIG_KEYS[configKey], }), }) - const maxCjFeeAbsQuery = useMutation(createConfigQuery('max_cj_fee_abs')) - const maxCjFeeRelQuery = useMutation(createConfigQuery('max_cj_fee_rel')) - const txFeesQuery = useMutation(createConfigQuery('tx_fees')) - const txFeesFactorQuery = useMutation(createConfigQuery('tx_fees_factor')) - const maxSweepFeeChangeQuery = useMutation(createConfigQuery('max_sweep_fee_change')) + const { mutateAsync: fetchMaxCjFeeAbs, ...maxCjFeeAbsQuery } = useMutation(createConfigQuery('max_cj_fee_abs')) + const { mutateAsync: fetchMaxCjFeeRel, ...maxCjFeeRelQuery } = useMutation(createConfigQuery('max_cj_fee_rel')) + const { mutateAsync: fetchTxFees, ...txFeesQuery } = useMutation(createConfigQuery('tx_fees')) + const { mutateAsync: fetchTxFeesFactor, ...txFeesFactorQuery } = useMutation(createConfigQuery('tx_fees_factor')) + const { mutateAsync: fetchMaxSweepFeeChange, ...maxSweepFeeChangeQuery } = useMutation( + createConfigQuery('max_sweep_fee_change'), + ) const feeConfigValues = useMemo(() => { - if ( - !maxCjFeeAbsQuery.data && - !maxCjFeeRelQuery.data && - !txFeesQuery.data && - !txFeesFactorQuery.data && - !maxSweepFeeChangeQuery.data - ) { - return undefined - } - return { max_cj_fee_abs: maxCjFeeAbsQuery.data?.configvalue, max_cj_fee_rel: maxCjFeeRelQuery.data?.configvalue, @@ -65,17 +57,6 @@ export const useFeeConfigValidation = ({ walletFileName }: UseFeeConfigValidatio maxSweepFeeChangeQuery.data, ]) - const maxFeesConfigMissing = useMemo(() => { - // Debug: Force the error for testing - if (forceFeeConfigMissing) { - return true - } - - return ( - feeConfigValues && (feeConfigValues.max_cj_fee_abs === undefined || feeConfigValues.max_cj_fee_rel === undefined) - ) - }, [feeConfigValues, forceFeeConfigMissing]) - const isLoading = useMemo(() => { return ( maxCjFeeAbsQuery.isPending || @@ -108,18 +89,29 @@ export const useFeeConfigValidation = ({ walletFileName }: UseFeeConfigValidatio maxSweepFeeChangeQuery.error, ]) - const refetchAll = async () => { + const refetchAll = useCallback(async () => { const args = { path: { walletname: walletFileName }, } - await Promise.all([ - maxCjFeeAbsQuery.mutateAsync(args), - maxCjFeeRelQuery.mutateAsync(args), - txFeesQuery.mutateAsync(args), - txFeesFactorQuery.mutateAsync(args), - maxSweepFeeChangeQuery.mutateAsync(args), + return await Promise.all([ + fetchMaxCjFeeAbs(args), + fetchMaxCjFeeRel(args), + fetchTxFees(args), + fetchTxFeesFactor(args), + fetchMaxSweepFeeChange(args), ]) - } + }, [walletFileName, fetchMaxCjFeeAbs, fetchMaxCjFeeRel, fetchTxFees, fetchTxFeesFactor, fetchMaxSweepFeeChange]) + + const maxFeesConfigMissing = useMemo(() => { + // Debug: Force the error for testing + if (forceFeeConfigMissing) { + return true + } + + return ( + feeConfigValues && (feeConfigValues.max_cj_fee_abs === undefined || feeConfigValues.max_cj_fee_rel === undefined) + ) + }, [feeConfigValues, forceFeeConfigMissing]) return { feeConfigValues, diff --git a/src/hooks/useJmConfig.ts b/src/hooks/useJmConfig.ts new file mode 100644 index 00000000..86427502 --- /dev/null +++ b/src/hooks/useJmConfig.ts @@ -0,0 +1,65 @@ +import { useCallback } from 'react' +import { useMutation } from '@tanstack/react-query' +import { useStore } from 'zustand' +import type { ConfigKey, ConfigValue } from '@/constants/jm' +import { useApiClient } from '@/hooks/useApiClient' +import { configgetMutation } from '@/lib/jm-api/generated/client/@tanstack/react-query.gen' +import type { WalletFileName } from '@/lib/utils' +import { jmConfigStore } from '@/store/jmConfigStore' + +interface UseJmConfigProps { + walletFileName: WalletFileName +} + +export const useJmConfig = ({ walletFileName }: UseJmConfigProps) => { + const client = useApiClient() + const jmConfigStoreState = useStore(jmConfigStore) + + const { mutateAsync: fetchConfigAsync } = useMutation({ + ...configgetMutation({ + client, + path: { walletname: walletFileName }, + }), + retry: 3, + }) + + const get = useCallback( + (key: ConfigKey): ConfigValue | null => { + return jmConfigStoreState.get(key) + }, + [jmConfigStoreState], + ) + + const refetch = useCallback( + async (key: ConfigKey): Promise => { + const { configvalue } = await fetchConfigAsync({ + path: { walletname: walletFileName }, + body: { + section: key.section, + field: key.field, + }, + }) + const result: ConfigValue = { + key, + value: configvalue ?? null, + } + jmConfigStoreState.set(result) + return result + }, + [walletFileName, jmConfigStoreState, fetchConfigAsync], + ) + + const fetchIfMissing = useCallback( + async (key: ConfigKey): Promise => { + const value = get(key) + return value !== null ? value : refetch(key) + }, + [get, refetch], + ) + + return { + get, + refetch, + fetchIfMissing, + } +} diff --git a/src/store/jmConfigStore.ts b/src/store/jmConfigStore.ts new file mode 100644 index 00000000..4d084e39 --- /dev/null +++ b/src/store/jmConfigStore.ts @@ -0,0 +1,59 @@ +import { createStore } from 'zustand' +import { persist, createJSONStorage } from 'zustand/middleware' +import type { ConfigKey, ConfigValue } from '@/constants/jm' + +export type JmConfigs = { + [key: ConfigKey['section']]: Record +} + +interface JmConfigStoreState { + state: JmConfigs + get: (val: ConfigKey) => ConfigValue | null + getAll: () => ConfigValue[] + set: (val: ConfigValue) => void + clear: () => void +} + +const initial: JmConfigs = {} + +export const jmConfigStore = createStore()( + persist( + (set, get) => ({ + state: initial, + get: (key) => { + const fields = get().state[key.section] + if (fields === undefined) return null + + const value = fields[key.field] + if (value === undefined) return null + + return { + key, + value: value || null, + } + }, + getAll: () => { + return Object.entries(get().state).flatMap(([section, entries]) => { + return Object.entries(entries).map(([field, value]) => ({ + key: { + section, + field, + }, + value, + })) + }) + }, + set: (val) => + set((state) => { + const copy = { state: { ...(state.state || {}) } } + copy.state[val.key.section][val.key.field] = val.value + return copy + }), + clear: () => set({ state: initial }), + }), + { + name: 'jm-configs', + storage: createJSONStorage(() => sessionStorage), + }, + ), +)