From 35211977703c11e7b35efc33bbf447160a3f5ec3 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 27 Dec 2025 11:36:40 +0800 Subject: [PATCH 1/8] misc: migrate storages --- .gitignore | 2 + app/layout.tsx | 2 + app/settings/page.tsx | 25 +- src/components/StorageMigrator.tsx | 321 ++++++++++++++++++ src/contexts/MarketsContext.tsx | 13 +- .../modals/deposit-to-vault-modal.tsx | 4 +- .../history/components/history-table.tsx | 8 +- .../components/market-settings-modal.tsx | 21 +- .../components/markets-table-same-loan.tsx | 62 ++-- src/features/markets/markets-view.tsx | 77 ++--- .../components/onboarding/setup-positions.tsx | 5 +- .../components/rebalance/rebalance-modal.tsx | 4 +- .../supplied-morpho-blue-grouped-table.tsx | 9 +- src/hooks/useBorrowTransaction.ts | 3 +- src/hooks/useRebalance.ts | 4 +- src/hooks/useRepayTransaction.ts | 3 +- src/hooks/useSupplyMarket.ts | 4 +- src/hooks/useVaultV2Deposit.ts | 4 +- .../components/add-collateral-and-borrow.tsx | 4 +- src/modals/settings/trusted-vaults-modal.tsx | 22 +- src/modals/supply/supply-modal-content.tsx | 4 +- src/stores/useAppSettings.ts | 69 ++++ src/stores/useHistoryPreferences.ts | 51 +++ src/stores/useMarketPreferences.ts | 121 +++++++ src/stores/useModalStore.ts | 6 +- src/stores/usePositionsPreferences.ts | 44 +++ src/stores/useTrustedVaults.ts | 88 +++++ src/utils/storage-migration.ts | 265 +++++++++++++++ src/utils/storageKeys.ts | 40 ++- 29 files changed, 1109 insertions(+), 176 deletions(-) create mode 100644 src/components/StorageMigrator.tsx create mode 100644 src/stores/useAppSettings.ts create mode 100644 src/stores/useHistoryPreferences.ts create mode 100644 src/stores/useMarketPreferences.ts create mode 100644 src/stores/usePositionsPreferences.ts create mode 100644 src/stores/useTrustedVaults.ts create mode 100644 src/utils/storage-migration.ts diff --git a/.gitignore b/.gitignore index 8cd357d0..1558d4b3 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,5 @@ CLAUDE.md FULLAUTO_CONTEXT.md .claude/settings.local.json + +docs/ZUSTAND_MIGRATION_COMPLETE.md \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 52ef6b7b..f60005f6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,6 +7,7 @@ import RiskNotificationModal from '@/modals/risk-notification-modal'; import { VaultRegistryProvider } from '@/contexts/VaultRegistryContext'; import OnchainProviders from '@/OnchainProviders'; import { ModalRenderer } from '@/components/modals/ModalRenderer'; +import { StorageMigrator } from '@/components/StorageMigrator'; import { initAnalytics } from '@/utils/analytics'; import { ThemeProviders } from '../src/components/providers/ThemeProvider'; @@ -37,6 +38,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) + {children} diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 49a5ecb4..67c1226c 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -7,17 +7,21 @@ import { IconSwitch } from '@/components/ui/icon-switch'; import Header from '@/components/layout/header/Header'; import { AdvancedRpcSettings } from '@/modals/settings/custom-rpc-settings'; import { VaultIdentity } from '@/features/autovault/components/vault-identity'; -import { defaultTrustedVaults, type TrustedVault } from '@/constants/vaults/known_vaults'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { defaultTrustedVaults } from '@/constants/vaults/known_vaults'; import { useMarkets } from '@/hooks/useMarkets'; import { useModal } from '@/hooks/useModal'; -import { storageKeys } from '@/utils/storageKeys'; +import { useTrustedVaults } from '@/stores/useTrustedVaults'; +import { useAppSettings } from '@/stores/useAppSettings'; +import { useMarketPreferences } from '@/stores/useMarketPreferences'; export default function SettingsPage() { - const [usePermit2, setUsePermit2] = useLocalStorage('usePermit2', true); - const [includeUnknownTokens, setIncludeUnknownTokens] = useLocalStorage('includeUnknownTokens', false); - const [showUnknownOracle, setShowUnknownOracle] = useLocalStorage('showUnknownOracle', false); - const [userTrustedVaults, setUserTrustedVaults] = useLocalStorage(storageKeys.UserTrustedVaultsKey, defaultTrustedVaults); + // App settings from Zustand store + const { usePermit2, setUsePermit2 } = useAppSettings(); + + // Market preferences from Zustand store + const { includeUnknownTokens, setIncludeUnknownTokens, showUnknownOracle, setShowUnknownOracle } = useMarketPreferences(); + + const { vaults: userTrustedVaults } = useTrustedVaults(); const { showUnwhitelistedMarkets, setShowUnwhitelistedMarkets, isAprDisplay, setIsAprDisplay } = useMarkets(); const { open: openModal } = useModal(); @@ -184,12 +188,7 @@ export default function SettingsPage() { diff --git a/src/components/StorageMigrator.tsx b/src/components/StorageMigrator.tsx new file mode 100644 index 00000000..74e77319 --- /dev/null +++ b/src/components/StorageMigrator.tsx @@ -0,0 +1,321 @@ +'use client'; + +import { useEffect } from 'react'; +import storage from 'local-storage-fallback'; +import type { TrustedVault } from '@/constants/vaults/known_vaults'; +import { useTrustedVaults } from '@/stores/useTrustedVaults'; +import { useMarketPreferences } from '@/stores/useMarketPreferences'; +import { useAppSettings } from '@/stores/useAppSettings'; +import { useHistoryPreferences } from '@/stores/useHistoryPreferences'; +import { usePositionsPreferences } from '@/stores/usePositionsPreferences'; +import { SortColumn } from '@/features/markets/components/constants'; +import { DEFAULT_MIN_SUPPLY_USD } from '@/constants/markets'; +import { DEFAULT_COLUMN_VISIBILITY } from '@/features/markets/components/column-visibility'; +import { + type MigrationDefinition, + isMigrationComplete, + isMigrationExpired, + markMigrationComplete, + executeAllMigrations, +} from '@/utils/storage-migration'; + +/** + * One-Time Storage Migrator Component + * + * **Purpose:** Migrate user data from old useLocalStorage format to new Zustand stores + * + * **Timeline:** + * - Created: January 2025 + * - Expires: February 1, 2025 + * - DELETE THIS FILE AFTER: February 2025 + * + * **How it works:** + * 1. Runs once on app load + * 2. Checks if migrations already completed (via localStorage flag) + * 3. Executes all defined migrations + * 4. Logs progress to console + * 5. Marks as complete + * 6. Auto-disables after expiry date + * + * **To delete (after Feb 2025):** + * 1. Remove `` from `app/layout.tsx` + * 2. Delete this file: `src/components/StorageMigrator.tsx` + * 3. Delete `src/utils/storage-migration.ts` + * 4. Remove any stores that were only created for migration + * + * @returns null - This component renders nothing + */ + +/** + * Helper: Safely parse JSON from localStorage + */ +function _safeParseJSON(value: string | null, defaultValue: T): T { + if (!value) return defaultValue; + try { + return JSON.parse(value) as T; + } catch { + return defaultValue; + } +} + +/** + * Helper: Get localStorage value or default + */ +function getStorageValue(key: string, defaultValue: T): T { + const value = storage.getItem(key); + if (value === null) return defaultValue; + + // If default is a boolean/number, parse accordingly + if (typeof defaultValue === 'boolean') { + return (value === 'true') as T; + } + if (typeof defaultValue === 'number') { + const parsed = Number(value); + return (Number.isNaN(parsed) ? defaultValue : parsed) as T; + } + + // Try JSON parse + try { + return JSON.parse(value) as T; + } catch { + return value as T; + } +} + +export function StorageMigrator() { + useEffect(() => { + // Skip if already completed + if (isMigrationComplete()) { + console.log('โœ… Storage migrations already completed - skipping'); + return; + } + + // Skip if expired (with warning) + if (isMigrationExpired()) { + return; + } + + // Define all migrations + const migrations: MigrationDefinition[] = [ + // ======================================== + // Migration 1: Trusted Vaults + // ======================================== + { + oldKey: 'userTrustedVaults', + newKey: 'monarch_store_trustedVaults', + storeName: 'useTrustedVaults', + description: 'Migrated Trusted Vaults to Zustand store', + validate: (data): data is TrustedVault[] => { + if (!Array.isArray(data)) return false; + return data.every( + (item) => + item && + typeof item === 'object' && + 'address' in item && + 'chainId' in item && + typeof item.address === 'string' && + typeof item.chainId === 'number', + ); + }, + migrate: (oldData: TrustedVault[]) => { + try { + const store = useTrustedVaults.getState(); + store.setVaults(oldData); + return true; + } catch (error) { + console.error('Error migrating trusted vaults:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 2: Market Preferences (MULTI-KEY) + // ======================================== + { + oldKey: 'monarch_marketsSortColumn', // Primary key to check + newKey: 'monarch_store_marketPreferences', + storeName: 'useMarketPreferences', + description: 'Migrated Market Preferences to Zustand store (14 keys)', + // No validate function - we handle all keys ourselves + migrate: () => { + try { + // Read all old market preference keys + const sortColumn = getStorageValue('monarch_marketsSortColumn', SortColumn.Supply); + const sortDirection = getStorageValue('monarch_marketsSortDirection', -1); + const entriesPerPage = getStorageValue('monarch_marketsEntriesPerPage', 8); + const includeUnknownTokens = getStorageValue('includeUnknownTokens', false); + const showUnknownOracle = getStorageValue('showUnknownOracle', false); + const trustedVaultsOnly = getStorageValue('monarch_marketsTrustedVaultsOnly', false); + const columnVisibility = getStorageValue('monarch_marketsColumnVisibility', DEFAULT_COLUMN_VISIBILITY); + const tableViewMode = getStorageValue('monarch_marketsTableViewMode', 'compact'); + const usdMinSupply = getStorageValue('monarch_marketsUsdMinSupply_2', DEFAULT_MIN_SUPPLY_USD.toString()); + const usdMinBorrow = getStorageValue('monarch_marketsUsdMinBorrow', ''); + const usdMinLiquidity = getStorageValue('monarch_marketsUsdMinLiquidity', ''); + const minSupplyEnabled = getStorageValue('monarch_minSupplyEnabled', false); + const minBorrowEnabled = getStorageValue('monarch_minBorrowEnabled', false); + const minLiquidityEnabled = getStorageValue('monarch_minLiquidityEnabled', false); + + // Write to store + const store = useMarketPreferences.getState(); + store.setAll({ + sortColumn, + sortDirection, + entriesPerPage, + includeUnknownTokens, + showUnknownOracle, + trustedVaultsOnly, + columnVisibility, + tableViewMode, + usdMinSupply, + usdMinBorrow, + usdMinLiquidity, + minSupplyEnabled, + minBorrowEnabled, + minLiquidityEnabled, + }); + + return true; + } catch (error) { + console.error('Error migrating market preferences:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 3: App Settings (MULTI-KEY) + // ======================================== + { + oldKey: 'usePermit2', // Primary key to check + newKey: 'monarch_store_appSettings', + storeName: 'useAppSettings', + description: 'Migrated App Settings to Zustand store (5 keys)', + // No validate function - we handle all keys ourselves + migrate: () => { + try { + // Read all old app setting keys + const usePermit2 = getStorageValue('usePermit2', true); + const useEth = getStorageValue('useEth', false); + const showUnwhitelistedMarkets = getStorageValue('showUnwhitelistedMarkets', false); + const showFullRewardAPY = getStorageValue('showFullRewardAPY', false); + const isAprDisplay = getStorageValue('settings-apr-display', false); + + // Write to store + const store = useAppSettings.getState(); + store.setAll({ + usePermit2, + useEth, + showUnwhitelistedMarkets, + showFullRewardAPY, + isAprDisplay, + }); + + return true; + } catch (error) { + console.error('Error migrating app settings:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 4: History Preferences (MULTI-KEY) + // ======================================== + { + oldKey: 'monarch_historyEntriesPerPage', // Primary key to check + newKey: 'monarch_store_historyPreferences', + storeName: 'useHistoryPreferences', + description: 'Migrated History Preferences to Zustand store (2 keys)', + // No validate function - we handle all keys ourselves + migrate: () => { + try { + // Read all old history preference keys + const entriesPerPage = getStorageValue('monarch_historyEntriesPerPage', 10); + const isGroupedView = getStorageValue('monarch_historyGroupedView', true); + + // Write to store + const store = useHistoryPreferences.getState(); + store.setAll({ + entriesPerPage, + isGroupedView, + }); + + return true; + } catch (error) { + console.error('Error migrating history preferences:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 5: Positions Preferences + // ======================================== + { + oldKey: 'positions:show-collateral-exposure', + newKey: 'monarch_store_positionsPreferences', + storeName: 'usePositionsPreferences', + description: 'Migrated Positions Preferences to Zustand store', + // No validate function - we handle it ourselves + migrate: () => { + try { + const showCollateralExposure = getStorageValue('positions:show-collateral-exposure', true); + + const store = usePositionsPreferences.getState(); + store.setShowCollateralExposure(showCollateralExposure); + + return true; + } catch (error) { + console.error('Error migrating positions preferences:', error); + return false; + } + }, + }, + ]; + + // Execute all migrations + const results = executeAllMigrations(migrations); + + // Clean up ALL old localStorage keys after migrations + // (executeMigration only deletes the primary oldKey, not related keys) + const anySuccess = results.some((r) => r.status === 'success'); + if (anySuccess || results.every((r) => r.status === 'skipped')) { + console.log('๐Ÿงน Cleaning up old localStorage keys...'); + + // Market Preferences keys + const marketKeys = [ + 'monarch_marketsSortDirection', + 'monarch_marketsEntriesPerPage', + 'includeUnknownTokens', + 'showUnknownOracle', + 'monarch_marketsTrustedVaultsOnly', + 'monarch_marketsColumnVisibility', + 'monarch_marketsTableViewMode', + 'monarch_marketsUsdMinSupply_2', + 'monarch_marketsUsdMinBorrow', + 'monarch_marketsUsdMinLiquidity', + 'monarch_minSupplyEnabled', + 'monarch_minBorrowEnabled', + 'monarch_minLiquidityEnabled', + ]; + + // App Settings keys + const appKeys = ['useEth', 'showUnwhitelistedMarkets', 'showFullRewardAPY', 'settings-apr-display']; + + // History Preferences keys + const historyKeys = ['monarch_historyGroupedView']; + + // Delete all secondary keys (primary keys already deleted by executeMigration) + [...marketKeys, ...appKeys, ...historyKeys].forEach((key) => { + storage.removeItem(key); + }); + + console.log('โœ… Cleanup complete'); + markMigrationComplete(); + } + }, []); // Run once on mount + + // Render nothing + return null; +} diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx index 4048ac88..97e63ea2 100644 --- a/src/contexts/MarketsContext.tsx +++ b/src/contexts/MarketsContext.tsx @@ -6,7 +6,7 @@ import { useOracleDataContext } from '@/contexts/OracleDataContext'; import { fetchMorphoMarkets } from '@/data-sources/morpho-api/market'; import { fetchSubgraphMarkets } from '@/data-sources/subgraph/market'; import { useBlacklistedMarkets } from '@/hooks/useBlacklistedMarkets'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useAppSettings } from '@/stores/useAppSettings'; import { ALL_SUPPORTED_NETWORKS, isSupportedChain } from '@/utils/networks'; import type { Market } from '@/utils/types'; @@ -48,14 +48,9 @@ export function MarketsProvider({ children }: MarketsProviderProps) { // Store raw unfiltered markets to avoid refetching when blacklist changes const [rawMarkets, setRawMarkets] = useState([]); - // Global setting for showing unwhitelisted markets - const [showUnwhitelistedMarkets, setShowUnwhitelistedMarkets] = useLocalStorage('showUnwhitelistedMarkets', false); - - // Global setting for showing full reward APY (base + external rewards) - const [showFullRewardAPY, setShowFullRewardAPY] = useLocalStorage('showFullRewardAPY', false); - - // Global setting for showing APR instead of APY - const [isAprDisplay, setIsAprDisplay] = useLocalStorage('settings-apr-display', false); + // Global settings from Zustand store + const { showUnwhitelistedMarkets, setShowUnwhitelistedMarkets, showFullRewardAPY, setShowFullRewardAPY, isAprDisplay, setIsAprDisplay } = + useAppSettings(); // Blacklisted markets management const { allBlacklistedMarketKeys, addBlacklistedMarket, removeBlacklistedMarket, isBlacklisted, isDefaultBlacklisted } = diff --git a/src/features/autovault/components/vault-detail/modals/deposit-to-vault-modal.tsx b/src/features/autovault/components/vault-detail/modals/deposit-to-vault-modal.tsx index 88bda90d..cccff212 100644 --- a/src/features/autovault/components/vault-detail/modals/deposit-to-vault-modal.tsx +++ b/src/features/autovault/components/vault-detail/modals/deposit-to-vault-modal.tsx @@ -6,7 +6,7 @@ import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import Input from '@/components/Input/Input'; import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; import { TokenIcon } from '@/components/shared/token-icon'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useVaultV2Deposit } from '@/hooks/useVaultV2Deposit'; import { formatBalance } from '@/utils/balance'; import { VaultDepositProcessModal } from './vault-deposit-process-modal'; @@ -32,7 +32,7 @@ export function DepositToVaultModal({ onClose, onSuccess, }: DepositToVaultModalProps): JSX.Element { - const [usePermit2Setting] = useLocalStorage('usePermit2', true); + const { usePermit2: usePermit2Setting } = useAppSettings(); const { depositAmount, diff --git a/src/features/history/components/history-table.tsx b/src/features/history/components/history-table.tsx index 461617da..e36375a2 100644 --- a/src/features/history/components/history-table.tsx +++ b/src/features/history/components/history-table.tsx @@ -27,12 +27,11 @@ import { RebalanceDetail } from './rebalance-detail'; import { useMarkets } from '@/contexts/MarketsContext'; import useUserTransactions from '@/hooks/useUserTransactions'; import { useDisclosure } from '@/hooks/useDisclosure'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useHistoryPreferences } from '@/stores/useHistoryPreferences'; import { useStyledToast } from '@/hooks/useStyledToast'; import { formatReadable } from '@/utils/balance'; import { getNetworkImg, getNetworkName } from '@/utils/networks'; import { groupTransactionsByHash, getWithdrawals, getSupplies, type GroupedTransaction } from '@/utils/transactionGrouping'; -import { storageKeys } from '@/utils/storageKeys'; import { UserTxTypes, type Market, type MarketPosition, type UserTransaction } from '@/utils/types'; type HistoryTableProps = { @@ -87,9 +86,8 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His const [isInitialized, setIsInitialized] = useState(false); const [totalPages, setTotalPages] = useState(0); - // Settings state - const [pageSize, setPageSize] = useLocalStorage(storageKeys.HistoryEntriesPerPageKey, 10); - const [isGroupedView, setIsGroupedView] = useLocalStorage(storageKeys.HistoryGroupedViewKey, true); + // Settings state from Zustand store + const { entriesPerPage: pageSize, setEntriesPerPage: setPageSize, isGroupedView, setIsGroupedView } = useHistoryPreferences(); const [expandedRows, setExpandedRows] = useState>(new Set()); const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onOpenChange: onSettingsOpenChange } = useDisclosure(); diff --git a/src/features/markets/components/market-settings-modal.tsx b/src/features/markets/components/market-settings-modal.tsx index 0b5b26e6..09ddb9ed 100644 --- a/src/features/markets/components/market-settings-modal.tsx +++ b/src/features/markets/components/market-settings-modal.tsx @@ -6,11 +6,9 @@ import { Button } from '@/components/ui/button'; import { IconSwitch } from '@/components/ui/icon-switch'; import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { TrustedByCell } from '@/features/autovault/components/trusted-vault-badges'; -import { defaultTrustedVaults, type TrustedVault } from '@/constants/vaults/known_vaults'; import { useMarkets } from '@/hooks/useMarkets'; import { useModal } from '@/hooks/useModal'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { storageKeys } from '@/utils/storageKeys'; +import { useTrustedVaults } from '@/stores/useTrustedVaults'; import { type ColumnVisibility, COLUMN_LABELS, COLUMN_DESCRIPTIONS, DEFAULT_COLUMN_VISIBILITY } from './column-visibility'; type MarketSettingsModalProps = { @@ -25,7 +23,7 @@ type MarketSettingsModalProps = { entriesPerPage: number; onEntriesPerPageChange: (value: number) => void; columnVisibility: ColumnVisibility; - setColumnVisibility: (visibility: ColumnVisibility) => void; + setColumnVisibility: (visibilityOrUpdater: ColumnVisibility | ((prev: ColumnVisibility) => ColumnVisibility)) => void; }; function SettingItem({ title, description, children }: { title: string; description: string; children: React.ReactNode }) { @@ -51,7 +49,7 @@ export default function MarketSettingsModal({ setColumnVisibility, }: MarketSettingsModalProps) { const [customEntries, setCustomEntries] = React.useState(entriesPerPage.toString()); - const [userTrustedVaults, setUserTrustedVaults] = useLocalStorage(storageKeys.UserTrustedVaultsKey, defaultTrustedVaults); + const { vaults: userTrustedVaults } = useTrustedVaults(); const totalVaults = userTrustedVaults.length; const { showFullRewardAPY, setShowFullRewardAPY } = useMarkets(); const { open: openModal } = useModal(); @@ -177,10 +175,10 @@ export default function MarketSettingsModal({ id={`col-${key}`} selected={isVisible} onChange={(value) => - setColumnVisibility({ - ...columnVisibility, + setColumnVisibility((prev) => ({ + ...prev, [key]: value, - }) + })) } size="xs" color="primary" @@ -216,12 +214,7 @@ export default function MarketSettingsModal({ - + {/* Hide expand/compact toggle on mobile */} @@ -149,17 +143,15 @@ function MarketsTable({ /> } > - - - + )} @@ -171,17 +163,15 @@ function MarketsTable({ /> } > - - - + ); @@ -359,8 +349,6 @@ function MarketsTable({ expandedRowId={expandedRowId} setExpandedRowId={setExpandedRowId} trustedVaultMap={trustedVaultMap} - addBlacklistedMarket={addBlacklistedMarket} - isBlacklisted={isBlacklisted} /> )} diff --git a/src/features/markets/markets-view.tsx b/src/features/markets/markets-view.tsx index eda847e6..7b193bd1 100644 --- a/src/features/markets/markets-view.tsx +++ b/src/features/markets/markets-view.tsx @@ -12,7 +12,6 @@ import { usePagination } from '@/hooks/usePagination'; import { useStyledToast } from '@/hooks/useStyledToast'; import { useTrustedVaults } from '@/stores/useTrustedVaults'; import { useMarketPreferences } from '@/stores/useMarketPreferences'; -import { useBlacklistedMarkets } from '@/stores/useBlacklistedMarkets'; import { filterMarkets, sortMarkets, createPropertySort, createStarredSort } from '@/utils/marketFilters'; import type { SupportedNetworks } from '@/utils/networks'; import type { PriceFeedVendors } from '@/utils/oracle'; @@ -39,20 +38,6 @@ export default function Markets({ initialNetwork, initialCollaterals, initialLoa const { loading, markets: rawMarkets, refetch, isRefetching } = useMarkets(); - const { isBlacklisted, addBlacklistedMarket: addBlacklistedMarketStore } = useBlacklistedMarkets(); - - // Wrap addBlacklistedMarket with toast notification - const addBlacklistedMarket = useCallback( - (uniqueKey: string, chainId: number, reason?: string) => { - const success = addBlacklistedMarketStore(uniqueKey, chainId, reason); - if (success) { - toast.success('Market blacklisted', 'Market added to blacklist'); - } - return success; - }, - [addBlacklistedMarketStore, toast], - ); - // Initialize state with server-parsed values const [selectedCollaterals, setSelectedCollaterals] = useState(initialCollaterals); const [selectedLoanAssets, setSelectedLoanAssets] = useState(initialLoanAssets); @@ -439,8 +424,6 @@ export default function Markets({ initialNetwork, initialCollaterals, initialLoa trustedVaults={userTrustedVaults} className={effectiveTableViewMode === 'compact' ? 'w-full' : undefined} tableClassName={effectiveTableViewMode === 'compact' ? 'w-full min-w-full' : undefined} - addBlacklistedMarket={addBlacklistedMarket} - isBlacklisted={isBlacklisted} onOpenSettings={() => openModal('marketSettings', {})} onRefresh={handleRefresh} isRefetching={isRefetching} diff --git a/src/features/positions-report/components/report-table.tsx b/src/features/positions-report/components/report-table.tsx index 69cb2d5c..b86c3c2b 100644 --- a/src/features/positions-report/components/report-table.tsx +++ b/src/features/positions-report/components/report-table.tsx @@ -12,7 +12,6 @@ import { Badge } from '@/components/ui/badge'; import { NetworkIcon } from '@/components/shared/network-icon'; import OracleVendorBadge from '@/features/markets/components/oracle-vendor-badge'; import { TokenIcon } from '@/components/shared/token-icon'; -import { useMarkets } from '@/hooks/useMarkets'; import { useAppSettings } from '@/stores/useAppSettings'; import type { ReportSummary } from '@/hooks/usePositionReport'; import { useRateLabel } from '@/hooks/useRateLabel'; diff --git a/src/features/positions/components/from-markets-table.tsx b/src/features/positions/components/from-markets-table.tsx index bd00a53c..8bcb8994 100644 --- a/src/features/positions/components/from-markets-table.tsx +++ b/src/features/positions/components/from-markets-table.tsx @@ -3,7 +3,6 @@ import { Button } from '@/components/ui/button'; import { TablePagination } from '@/components/shared/table-pagination'; import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'; import { MarketIdentity, MarketIdentityMode, MarketIdentityFocus } from '@/features/markets/components/market-identity'; -import { useMarkets } from '@/hooks/useMarkets'; import { useAppSettings } from '@/stores/useAppSettings'; import { useRateLabel } from '@/hooks/useRateLabel'; import { formatReadable } from '@/utils/balance'; diff --git a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx index 3662fd61..48af610f 100644 --- a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx +++ b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx @@ -19,7 +19,6 @@ import { TableContainerWithHeader } from '@/components/common/table-container-wi import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { useDisclosure } from '@/hooks/useDisclosure'; import { usePositionsPreferences } from '@/stores/usePositionsPreferences'; -import { useMarkets } from '@/hooks/useMarkets'; import { useAppSettings } from '@/stores/useAppSettings'; import { computeMarketWarnings } from '@/hooks/useMarketWarnings'; import { useRateLabel } from '@/hooks/useRateLabel'; diff --git a/src/features/positions/components/user-vaults-table.tsx b/src/features/positions/components/user-vaults-table.tsx index 9c02059b..18eaf0f6 100644 --- a/src/features/positions/components/user-vaults-table.tsx +++ b/src/features/positions/components/user-vaults-table.tsx @@ -11,7 +11,6 @@ import { TooltipContent } from '@/components/shared/tooltip-content'; import { TableContainerWithHeader } from '@/components/common/table-container-with-header'; import type { UserVaultV2 } from '@/data-sources/subgraph/v2-vaults'; import { useTokens } from '@/components/providers/TokenProvider'; -import { useMarkets } from '@/hooks/useMarkets'; import { useAppSettings } from '@/stores/useAppSettings'; import { useRateLabel } from '@/hooks/useRateLabel'; import { formatReadable } from '@/utils/balance'; diff --git a/src/stores/useAppSettings.ts b/src/stores/useAppSettings.ts index 5f3661cc..1ef81a95 100644 --- a/src/stores/useAppSettings.ts +++ b/src/stores/useAppSettings.ts @@ -24,7 +24,6 @@ type AppSettingsActions = { }; type AppSettingsStore = AppSettingsState & AppSettingsActions; - /** * Zustand store for global app settings (transaction preferences, display options). * Automatically persisted to localStorage. diff --git a/src/utils/storageKeys.ts b/src/utils/storageKeys.ts index 4c50613e..5eef1374 100644 --- a/src/utils/storageKeys.ts +++ b/src/utils/storageKeys.ts @@ -3,4 +3,3 @@ export const storageKeys = { ThemeKey: 'theme', CacheMarketPositionKeys: 'monarch_cache_market_unique_keys', } as const; - From e7043879e53370b288c5dd5f17a38c2b8c829178 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 27 Dec 2025 14:37:11 +0800 Subject: [PATCH 6/8] chore: fix --- src/contexts/MarketsContext.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx index 2dca8f00..b9f62497 100644 --- a/src/contexts/MarketsContext.tsx +++ b/src/contexts/MarketsContext.tsx @@ -44,8 +44,8 @@ export function MarketsProvider({ children }: MarketsProviderProps) { // Blacklisted markets management from Zustand store (internal use only for filtering) const { getAllBlacklistedKeys } = useBlacklistedMarkets(); - // Get all blacklisted keys for filtering - const allBlacklistedMarketKeys = getAllBlacklistedKeys(); + // Get all blacklisted keys for filtering - memoize to prevent infinite loops + const allBlacklistedMarketKeys = useMemo(() => getAllBlacklistedKeys(), [getAllBlacklistedKeys]); // Oracle data context for enriching markets const { getOracleData } = useOracleDataContext(); From dcc6cf50f2712b47e4bd3f9436c56f635de75c36 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 27 Dec 2025 15:03:12 +0800 Subject: [PATCH 7/8] refactor: remaining migration --- src/components/StorageMigrator.tsx | 88 ++++++++++++ .../providers/CustomRpcProvider.tsx | 2 +- src/features/market-detail/market-view.tsx | 2 +- src/hooks/useBorrowTransaction.ts | 2 +- src/hooks/useCustomRpc.ts | 45 ------- src/hooks/useMultiMarketSupply.ts | 2 +- src/hooks/usePositionReport.ts | 2 +- src/hooks/useRebalance.ts | 2 +- src/hooks/useSupplyMarket.ts | 2 +- src/hooks/useTransactionFilters.ts | 51 ------- src/hooks/useUserMarketsCache.ts | 94 ------------- src/hooks/useUserPositions.ts | 4 +- src/store/createWagmiConfig.ts | 2 +- src/stores/useCustomRpc.ts | 67 ++++++++++ src/stores/useTransactionFilters.ts | 96 ++++++++++++++ src/stores/useUserMarketsCache.ts | 125 ++++++++++++++++++ src/utils/storage-migration.ts | 2 +- src/utils/storageKeys.ts | 5 - 18 files changed, 387 insertions(+), 206 deletions(-) delete mode 100644 src/hooks/useCustomRpc.ts delete mode 100644 src/hooks/useTransactionFilters.ts delete mode 100644 src/hooks/useUserMarketsCache.ts create mode 100644 src/stores/useCustomRpc.ts create mode 100644 src/stores/useTransactionFilters.ts create mode 100644 src/stores/useUserMarketsCache.ts delete mode 100644 src/utils/storageKeys.ts diff --git a/src/components/StorageMigrator.tsx b/src/components/StorageMigrator.tsx index f79ff0b0..13d07e9f 100644 --- a/src/components/StorageMigrator.tsx +++ b/src/components/StorageMigrator.tsx @@ -9,6 +9,9 @@ import { useAppSettings } from '@/stores/useAppSettings'; import { useHistoryPreferences } from '@/stores/useHistoryPreferences'; import { usePositionsPreferences } from '@/stores/usePositionsPreferences'; import { useBlacklistedMarkets, type BlacklistedMarket } from '@/stores/useBlacklistedMarkets'; +import { useCustomRpc, type CustomRpcUrls } from '@/stores/useCustomRpc'; +import { useTransactionFiltersStore } from '@/stores/useTransactionFilters'; +import { useUserMarketsCacheStore } from '@/stores/useUserMarketsCache'; import { SortColumn } from '@/features/markets/components/constants'; import { DEFAULT_MIN_SUPPLY_USD } from '@/constants/markets'; import { DEFAULT_COLUMN_VISIBILITY } from '@/features/markets/components/column-visibility'; @@ -38,6 +41,22 @@ import { * @returns null - This component renders nothing */ +// Type definitions for migrations +type SymbolFilters = Record< + string, + { + minSupplyAmount: string; + minBorrowAmount: string; + } +>; + +type MarketIdentifier = { + marketUniqueKey: string; + chainId: number; +}; + +type UserMarketsCache = Record; + /** * Helper: Safely parse JSON from localStorage */ @@ -300,6 +319,75 @@ export function StorageMigrator() { } }, }, + + // ======================================== + // Migration 7: Custom RPC URLs + // ======================================== + { + oldKey: 'customRpcUrls', + newKey: 'monarch_store_customRpc', + storeName: 'useCustomRpc', + description: 'Migrated Custom RPC URLs to Zustand store', + validate: (data): data is CustomRpcUrls => { + return typeof data === 'object' && data !== null; + }, + migrate: (oldData: CustomRpcUrls) => { + try { + const store = useCustomRpc.getState(); + store.setAll({ customRpcUrls: oldData }); + return true; + } catch (error) { + console.error('Error migrating custom RPC URLs:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 8: Transaction Filters + // ======================================== + { + oldKey: 'monarch_transaction_filters_v2', + newKey: 'monarch_store_transactionFilters', + storeName: 'useTransactionFilters', + description: 'Migrated Transaction Filters to Zustand store', + validate: (data): data is SymbolFilters => { + return typeof data === 'object' && data !== null; + }, + migrate: (oldData: SymbolFilters) => { + try { + const store = useTransactionFiltersStore.getState(); + store.setAll({ filters: oldData }); + return true; + } catch (error) { + console.error('Error migrating transaction filters:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 9: User Markets Cache + // ======================================== + { + oldKey: 'monarch_cache_market_unique_keys', + newKey: 'monarch_store_userMarketsCache', + storeName: 'useUserMarketsCache', + description: 'Migrated User Markets Cache to Zustand store', + validate: (data): data is UserMarketsCache => { + return typeof data === 'object' && data !== null; + }, + migrate: (oldData: UserMarketsCache) => { + try { + const store = useUserMarketsCacheStore.getState(); + store.setAll({ cache: oldData }); + return true; + } catch (error) { + console.error('Error migrating user markets cache:', error); + return false; + } + }, + }, ]; // Execute all migrations diff --git a/src/components/providers/CustomRpcProvider.tsx b/src/components/providers/CustomRpcProvider.tsx index 23abd81c..3f35570d 100644 --- a/src/components/providers/CustomRpcProvider.tsx +++ b/src/components/providers/CustomRpcProvider.tsx @@ -1,7 +1,7 @@ 'use client'; import { createContext, useContext, useEffect, useState, type ReactNode, useMemo } from 'react'; -import { useCustomRpc, type CustomRpcUrls } from '@/hooks/useCustomRpc'; +import { useCustomRpc, type CustomRpcUrls } from '@/stores/useCustomRpc'; import type { SupportedNetworks } from '@/utils/networks'; type CustomRpcContextType = { diff --git a/src/features/market-detail/market-view.tsx b/src/features/market-detail/market-view.tsx index 13ff3191..88cb1af3 100644 --- a/src/features/market-detail/market-view.tsx +++ b/src/features/market-detail/market-view.tsx @@ -21,7 +21,7 @@ import { useModal } from '@/hooks/useModal'; import { useMarketData } from '@/hooks/useMarketData'; import { useMarketHistoricalData } from '@/hooks/useMarketHistoricalData'; import { useOraclePrice } from '@/hooks/useOraclePrice'; -import { useTransactionFilters } from '@/hooks/useTransactionFilters'; +import { useTransactionFilters } from '@/stores/useTransactionFilters'; import useUserPositions from '@/hooks/useUserPosition'; import MORPHO_LOGO from '@/imgs/tokens/morpho.svg'; import { getExplorerURL, getMarketURL } from '@/utils/external'; diff --git a/src/hooks/useBorrowTransaction.ts b/src/hooks/useBorrowTransaction.ts index cecfec30..751b76a3 100644 --- a/src/hooks/useBorrowTransaction.ts +++ b/src/hooks/useBorrowTransaction.ts @@ -11,7 +11,7 @@ import { usePermit2 } from './usePermit2'; import { useAppSettings } from '@/stores/useAppSettings'; import { useStyledToast } from './useStyledToast'; import { useTransactionWithToast } from './useTransactionWithToast'; -import { useUserMarketsCache } from './useUserMarketsCache'; +import { useUserMarketsCache } from '@/stores/useUserMarketsCache'; type UseBorrowTransactionProps = { market: Market; diff --git a/src/hooks/useCustomRpc.ts b/src/hooks/useCustomRpc.ts deleted file mode 100644 index 6a184b1a..00000000 --- a/src/hooks/useCustomRpc.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { type SupportedNetworks, getDefaultRPC } from '@/utils/networks'; -import { useLocalStorage } from './useLocalStorage'; - -export type CustomRpcUrls = Partial>; - -export function useCustomRpc() { - const [customRpcUrls, setCustomRpcUrls] = useLocalStorage('customRpcUrls', {}); - - const setRpcUrl = (chainId: SupportedNetworks, url: string | undefined) => { - setCustomRpcUrls((prev) => { - const newUrls = { ...prev }; - if (url === undefined || url === '' || url === getDefaultRPC(chainId)) { - delete newUrls[chainId]; - } else { - newUrls[chainId] = url; - } - return newUrls; - }); - }; - - const resetRpcUrl = (chainId: SupportedNetworks) => { - setRpcUrl(chainId, undefined); - }; - - const resetAllRpcUrls = () => { - setCustomRpcUrls({}); - }; - - const isUsingCustomRpc = (chainId: SupportedNetworks): boolean => { - return Boolean(customRpcUrls[chainId]); - }; - - const hasAnyCustomRpcs = (): boolean => { - return Object.keys(customRpcUrls).length > 0; - }; - - return { - customRpcUrls, - setRpcUrl, - resetRpcUrl, - resetAllRpcUrls, - isUsingCustomRpc, - hasAnyCustomRpcs, - }; -} diff --git a/src/hooks/useMultiMarketSupply.ts b/src/hooks/useMultiMarketSupply.ts index 95b3863e..814ca313 100644 --- a/src/hooks/useMultiMarketSupply.ts +++ b/src/hooks/useMultiMarketSupply.ts @@ -12,7 +12,7 @@ import type { Market } from '@/utils/types'; import { GAS_COSTS, GAS_MULTIPLIER } from '@/features/markets/components/constants'; import { useERC20Approval } from './useERC20Approval'; import { useStyledToast } from './useStyledToast'; -import { useUserMarketsCache } from './useUserMarketsCache'; +import { useUserMarketsCache } from '@/stores/useUserMarketsCache'; export type MarketSupply = { market: Market; amount: bigint; diff --git a/src/hooks/usePositionReport.ts b/src/hooks/usePositionReport.ts index 038d2d2a..e6b81e05 100644 --- a/src/hooks/usePositionReport.ts +++ b/src/hooks/usePositionReport.ts @@ -4,7 +4,7 @@ import type { SupportedNetworks } from '@/utils/networks'; import { fetchPositionsSnapshots } from '@/utils/positions'; import { estimatedBlockNumber, getClient } from '@/utils/rpc'; import type { Market, MarketPosition, UserTransaction } from '@/utils/types'; -import { useCustomRpc } from './useCustomRpc'; +import { useCustomRpc } from '@/stores/useCustomRpc'; import useUserTransactions from './useUserTransactions'; export type PositionReport = { diff --git a/src/hooks/useRebalance.ts b/src/hooks/useRebalance.ts index 4f8f8aa3..08fc72fc 100644 --- a/src/hooks/useRebalance.ts +++ b/src/hooks/useRebalance.ts @@ -11,7 +11,7 @@ import { useMorphoAuthorization } from './useMorphoAuthorization'; import { usePermit2 } from './usePermit2'; import { useAppSettings } from '@/stores/useAppSettings'; import { useStyledToast } from './useStyledToast'; -import { useUserMarketsCache } from './useUserMarketsCache'; +import { useUserMarketsCache } from '@/stores/useUserMarketsCache'; // Define more specific step types export type RebalanceStepType = diff --git a/src/hooks/useSupplyMarket.ts b/src/hooks/useSupplyMarket.ts index c5edb037..23277d7d 100644 --- a/src/hooks/useSupplyMarket.ts +++ b/src/hooks/useSupplyMarket.ts @@ -7,7 +7,7 @@ import { usePermit2 } from '@/hooks/usePermit2'; import { useAppSettings } from '@/stores/useAppSettings'; import { useStyledToast } from '@/hooks/useStyledToast'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; -import { useUserMarketsCache } from '@/hooks/useUserMarketsCache'; +import { useUserMarketsCache } from '@/stores/useUserMarketsCache'; import { formatBalance } from '@/utils/balance'; import { getBundlerV2, MONARCH_TX_IDENTIFIER } from '@/utils/morpho'; import type { Market } from '@/utils/types'; diff --git a/src/hooks/useTransactionFilters.ts b/src/hooks/useTransactionFilters.ts deleted file mode 100644 index 3b9275c2..00000000 --- a/src/hooks/useTransactionFilters.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useLocalStorage } from './useLocalStorage'; - -type SymbolFilters = Record< - string, - { - minSupplyAmount: string; - minBorrowAmount: string; - } ->; - -/** - * Hook to manage transaction filter settings with localStorage persistence - * Filters are cached per token symbol (e.g., all USDC markets share the same filter) - */ -export function useTransactionFilters(loanAssetSymbol: string) { - const [allFilters, setAllFilters] = useLocalStorage('monarch_transaction_filters_v2', {}); - - const currentFilters = allFilters[loanAssetSymbol] ?? { - minSupplyAmount: '0', - minBorrowAmount: '0', - }; - - const setMinSupplyAmount = (value: string) => { - setAllFilters((prev) => ({ - ...prev, - [loanAssetSymbol]: { - ...prev[loanAssetSymbol], - minSupplyAmount: value, - minBorrowAmount: prev[loanAssetSymbol]?.minBorrowAmount ?? '0', - }, - })); - }; - - const setMinBorrowAmount = (value: string) => { - setAllFilters((prev) => ({ - ...prev, - [loanAssetSymbol]: { - ...prev[loanAssetSymbol], - minSupplyAmount: prev[loanAssetSymbol]?.minSupplyAmount ?? '0', - minBorrowAmount: value, - }, - })); - }; - - return { - minSupplyAmount: currentFilters.minSupplyAmount, - minBorrowAmount: currentFilters.minBorrowAmount, - setMinSupplyAmount, - setMinBorrowAmount, - }; -} diff --git a/src/hooks/useUserMarketsCache.ts b/src/hooks/useUserMarketsCache.ts deleted file mode 100644 index 63af9a1a..00000000 --- a/src/hooks/useUserMarketsCache.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { useCallback } from 'react'; -import storage from 'local-storage-fallback'; -import { storageKeys } from '../utils/storageKeys'; - -type MarketIdentifier = { - marketUniqueKey: string; - chainId: number; -}; - -type UserMarketsCache = Record; - -export function useUserMarketsCache(address: string | undefined) { - const userAddress = address?.toLowerCase() ?? ''; - - // Load cache from localStorage - const loadCache = useCallback((): UserMarketsCache => { - try { - const cached = storage.getItem(storageKeys.CacheMarketPositionKeys); - if (cached) { - return JSON.parse(cached) as UserMarketsCache; - } - } catch (error) { - console.error('Failed to load markets cache:', error); - } - return {}; - }, []); - - // Save cache to localStorage - const saveCache = useCallback((cache: UserMarketsCache) => { - try { - storage.setItem(storageKeys.CacheMarketPositionKeys, JSON.stringify(cache)); - } catch (error) { - console.error('Failed to save markets cache:', error); - } - }, []); - - // Add markets to the user's known list - const addUserMarkets = useCallback( - (markets: MarketIdentifier[]) => { - if (!userAddress) return; - - const cache = loadCache(); - const userMarkets = cache[userAddress] ?? []; - - const updatedMarkets = [...userMarkets]; - let hasChanges = false; - - markets.forEach((market) => { - // Check if market already exists - const exists = updatedMarkets.some((m) => m.marketUniqueKey === market.marketUniqueKey && m.chainId === market.chainId); - - if (!exists) { - updatedMarkets.push(market); - hasChanges = true; - } - }); - - if (hasChanges) { - cache[userAddress] = updatedMarkets; - saveCache(cache); - } - }, - [userAddress, loadCache, saveCache], - ); - - // Get markets for the current user - const getUserMarkets = useCallback((): MarketIdentifier[] => { - if (!userAddress) return []; - - const cache = loadCache(); - return cache[userAddress] ?? []; - }, [userAddress, loadCache]); - - // Update cache with markets from API response - const batchAddUserMarkets = useCallback( - (apiMarkets: { marketUniqueKey: string; chainId: number }[]) => { - if (!userAddress || !apiMarkets.length) return; - - addUserMarkets( - apiMarkets.map((market) => ({ - marketUniqueKey: market.marketUniqueKey, - chainId: market.chainId, - })), - ); - }, - [userAddress, addUserMarkets], - ); - - return { - addUserMarkets, - getUserMarkets, - batchAddUserMarkets, - }; -} diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index f8796c31..eb13ca3b 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -8,8 +8,8 @@ import { SupportedNetworks } from '@/utils/networks'; import { fetchPositionsSnapshots, type PositionSnapshot } from '@/utils/positions'; import { getClient } from '@/utils/rpc'; import type { Market } from '@/utils/types'; -import { useUserMarketsCache } from '../hooks/useUserMarketsCache'; -import { useCustomRpc } from './useCustomRpc'; +import { useUserMarketsCache } from '@/stores/useUserMarketsCache'; +import { useCustomRpc } from '@/stores/useCustomRpc'; import { useMarkets } from './useMarkets'; // Type for market key and chain identifier diff --git a/src/store/createWagmiConfig.ts b/src/store/createWagmiConfig.ts index 7a88249f..e80ab44c 100644 --- a/src/store/createWagmiConfig.ts +++ b/src/store/createWagmiConfig.ts @@ -1,6 +1,6 @@ import { createConfig, http } from 'wagmi'; import { arbitrum, base, mainnet, monad, polygon, unichain } from 'wagmi/chains'; -import type { CustomRpcUrls } from '@/hooks/useCustomRpc'; +import type { CustomRpcUrls } from '@/stores/useCustomRpc'; import { SupportedNetworks, getDefaultRPC, hyperEvm } from '@/utils/networks'; /** diff --git a/src/stores/useCustomRpc.ts b/src/stores/useCustomRpc.ts new file mode 100644 index 00000000..44353364 --- /dev/null +++ b/src/stores/useCustomRpc.ts @@ -0,0 +1,67 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import { type SupportedNetworks, getDefaultRPC } from '@/utils/networks'; + +export type CustomRpcUrls = Partial>; + +type CustomRpcState = { + customRpcUrls: CustomRpcUrls; +}; + +type CustomRpcActions = { + setRpcUrl: (chainId: SupportedNetworks, url: string | undefined) => void; + resetRpcUrl: (chainId: SupportedNetworks) => void; + resetAllRpcUrls: () => void; + isUsingCustomRpc: (chainId: SupportedNetworks) => boolean; + hasAnyCustomRpcs: () => boolean; + + // Bulk update for migration + setAll: (state: Partial) => void; +}; + +type CustomRpcStore = CustomRpcState & CustomRpcActions; + +/** + * Zustand store for custom RPC URLs. + * + * @example + * ```tsx + * const { customRpcUrls, setRpcUrl, isUsingCustomRpc } = useCustomRpc(); + * ``` + */ +export const useCustomRpc = create()( + persist( + (set, get) => ({ + // Default state + customRpcUrls: {}, + + // Actions + setRpcUrl: (chainId, url) => { + set((state) => { + const newUrls = { ...state.customRpcUrls }; + if (url === undefined || url === '' || url === getDefaultRPC(chainId)) { + delete newUrls[chainId]; + } else { + newUrls[chainId] = url; + } + return { customRpcUrls: newUrls }; + }); + }, + + resetRpcUrl: (chainId) => { + get().setRpcUrl(chainId, undefined); + }, + + resetAllRpcUrls: () => set({ customRpcUrls: {} }), + + isUsingCustomRpc: (chainId) => Boolean(get().customRpcUrls[chainId]), + + hasAnyCustomRpcs: () => Object.keys(get().customRpcUrls).length > 0, + + setAll: (state) => set(state), + }), + { + name: 'monarch_store_customRpc', + }, + ), +); diff --git a/src/stores/useTransactionFilters.ts b/src/stores/useTransactionFilters.ts new file mode 100644 index 00000000..cf388539 --- /dev/null +++ b/src/stores/useTransactionFilters.ts @@ -0,0 +1,96 @@ +import { useMemo } from 'react'; +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +type SymbolFilters = Record< + string, + { + minSupplyAmount: string; + minBorrowAmount: string; + } +>; + +type TransactionFiltersState = { + filters: SymbolFilters; +}; + +type TransactionFiltersActions = { + setMinSupplyAmount: (symbol: string, value: string) => void; + setMinBorrowAmount: (symbol: string, value: string) => void; + + // Bulk update for migration + setAll: (state: Partial) => void; +}; + +type TransactionFiltersStore = TransactionFiltersState & TransactionFiltersActions; + +/** + * Zustand store for transaction filter settings with localStorage persistence. + * Filters are cached per token symbol (e.g., all USDC markets share the same filter). + */ +export const useTransactionFiltersStore = create()( + persist( + (set) => ({ + // Default state + filters: {}, + + // Actions + setMinSupplyAmount: (symbol, value) => { + set((state) => ({ + filters: { + ...state.filters, + [symbol]: { + ...state.filters[symbol], + minSupplyAmount: value, + minBorrowAmount: state.filters[symbol]?.minBorrowAmount ?? '0', + }, + }, + })); + }, + + setMinBorrowAmount: (symbol, value) => { + set((state) => ({ + filters: { + ...state.filters, + [symbol]: { + ...state.filters[symbol], + minSupplyAmount: state.filters[symbol]?.minSupplyAmount ?? '0', + minBorrowAmount: value, + }, + }, + })); + }, + + setAll: (state) => set(state), + }), + { + name: 'monarch_store_transactionFilters', + }, + ), +); + +/** + * Convenience hook with scoped API for a specific token symbol. + * Maintains backward-compatible interface with the old useLocalStorage-based hook. + * + * @example + * ```tsx + * const { minSupplyAmount, setMinSupplyAmount } = useTransactionFilters('USDC'); + * ``` + */ +export function useTransactionFilters(loanAssetSymbol: string) { + const currentFilters = useTransactionFiltersStore((s) => s.filters[loanAssetSymbol] ?? { minSupplyAmount: '0', minBorrowAmount: '0' }); + + const setMinSupply = useTransactionFiltersStore((s) => s.setMinSupplyAmount); + const setMinBorrow = useTransactionFiltersStore((s) => s.setMinBorrowAmount); + + return useMemo( + () => ({ + minSupplyAmount: currentFilters.minSupplyAmount, + minBorrowAmount: currentFilters.minBorrowAmount, + setMinSupplyAmount: (value: string) => setMinSupply(loanAssetSymbol, value), + setMinBorrowAmount: (value: string) => setMinBorrow(loanAssetSymbol, value), + }), + [currentFilters, setMinSupply, setMinBorrow, loanAssetSymbol], + ); +} diff --git a/src/stores/useUserMarketsCache.ts b/src/stores/useUserMarketsCache.ts new file mode 100644 index 00000000..cf7d1ef6 --- /dev/null +++ b/src/stores/useUserMarketsCache.ts @@ -0,0 +1,125 @@ +import { useCallback } from 'react'; +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +type MarketIdentifier = { + marketUniqueKey: string; + chainId: number; +}; + +type UserMarketsCache = Record; + +type UserMarketsCacheState = { + cache: UserMarketsCache; +}; + +type UserMarketsCacheActions = { + addUserMarkets: (address: string, markets: MarketIdentifier[]) => void; + getUserMarkets: (address: string) => MarketIdentifier[]; + batchAddUserMarkets: (address: string, markets: MarketIdentifier[]) => void; + + // Bulk update for migration + setAll: (state: Partial) => void; +}; + +type UserMarketsCacheStore = UserMarketsCacheState & UserMarketsCacheActions; + +/** + * Zustand store for caching user's market positions. + * Stores a mapping of user addresses to their market identifiers. + */ +export const useUserMarketsCacheStore = create()( + persist( + (set, get) => ({ + // Default state + cache: {}, + + // Actions + addUserMarkets: (address, markets) => { + const userAddress = address.toLowerCase(); + + set((state) => { + const userMarkets = state.cache[userAddress] ?? []; + const updatedMarkets = [...userMarkets]; + let hasChanges = false; + + markets.forEach((market) => { + // Check if market already exists + const exists = updatedMarkets.some((m) => m.marketUniqueKey === market.marketUniqueKey && m.chainId === market.chainId); + + if (!exists) { + updatedMarkets.push(market); + hasChanges = true; + } + }); + + if (hasChanges) { + return { + cache: { + ...state.cache, + [userAddress]: updatedMarkets, + }, + }; + } + + return state; + }); + }, + + getUserMarkets: (address) => { + if (!address) return []; + const userAddress = address.toLowerCase(); + return get().cache[userAddress] ?? []; + }, + + batchAddUserMarkets: (address, markets) => { + get().addUserMarkets(address, markets); + }, + + setAll: (state) => set(state), + }), + { + name: 'monarch_store_userMarketsCache', + }, + ), +); + +/** + * Convenience hook with scoped API for a specific user address. + * Maintains backward-compatible interface with the old localStorage-based hook. + * + * @example + * ```tsx + * const { addUserMarkets, getUserMarkets } = useUserMarketsCache(userAddress); + * ``` + */ +export function useUserMarketsCache(address: string | undefined) { + const userAddress = address?.toLowerCase() ?? ''; + + const addMarkets = useUserMarketsCacheStore((s) => s.addUserMarkets); + const getMarkets = useUserMarketsCacheStore((s) => s.getUserMarkets); + const batchAdd = useUserMarketsCacheStore((s) => s.batchAddUserMarkets); + + return { + addUserMarkets: useCallback( + (markets: MarketIdentifier[]) => { + if (!userAddress) return; + addMarkets(userAddress, markets); + }, + [addMarkets, userAddress], + ), + + getUserMarkets: useCallback((): MarketIdentifier[] => { + if (!userAddress) return []; + return getMarkets(userAddress); + }, [getMarkets, userAddress]), + + batchAddUserMarkets: useCallback( + (apiMarkets: { marketUniqueKey: string; chainId: number }[]) => { + if (!userAddress || !apiMarkets.length) return; + batchAdd(userAddress, apiMarkets); + }, + [batchAdd, userAddress], + ), + }; +} diff --git a/src/utils/storage-migration.ts b/src/utils/storage-migration.ts index 1c9c10d4..caadda55 100644 --- a/src/utils/storage-migration.ts +++ b/src/utils/storage-migration.ts @@ -18,7 +18,7 @@ import storage from 'local-storage-fallback'; */ // Migration configuration -export const MIGRATION_STATUS_KEY = 'monarch_migration_v1_complete-0.2'; +export const MIGRATION_STATUS_KEY = 'monarch_migration_v1_complete'; export const MIGRATION_EXPIRY_DATE = '2026-02-01'; export const MIGRATION_VERSION = 'v1'; diff --git a/src/utils/storageKeys.ts b/src/utils/storageKeys.ts deleted file mode 100644 index 5eef1374..00000000 --- a/src/utils/storageKeys.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const storageKeys = { - // === STILL IN USE (not migrated) === - ThemeKey: 'theme', - CacheMarketPositionKeys: 'monarch_cache_market_unique_keys', -} as const; From 97b816ece9c7e907cbb494bc85c84a380062304d Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 27 Dec 2025 15:46:40 +0800 Subject: [PATCH 8/8] feat: finish migration --- src/contexts/MarketsContext.tsx | 4 ++-- src/features/markets/components/table/markets-table.tsx | 7 +++++-- src/modals/settings/blacklisted-markets-modal.tsx | 5 +++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx index b9f62497..83d8a2e0 100644 --- a/src/contexts/MarketsContext.tsx +++ b/src/contexts/MarketsContext.tsx @@ -42,10 +42,10 @@ export function MarketsProvider({ children }: MarketsProviderProps) { const { showUnwhitelistedMarkets } = useAppSettings(); // Blacklisted markets management from Zustand store (internal use only for filtering) - const { getAllBlacklistedKeys } = useBlacklistedMarkets(); + const { getAllBlacklistedKeys, customBlacklistedMarkets } = useBlacklistedMarkets(); // Get all blacklisted keys for filtering - memoize to prevent infinite loops - const allBlacklistedMarketKeys = useMemo(() => getAllBlacklistedKeys(), [getAllBlacklistedKeys]); + const allBlacklistedMarketKeys = useMemo(() => getAllBlacklistedKeys(), [customBlacklistedMarkets, getAllBlacklistedKeys]); // Oracle data context for enriching markets const { getOracleData } = useOracleDataContext(); diff --git a/src/features/markets/components/table/markets-table.tsx b/src/features/markets/components/table/markets-table.tsx index 06892b2c..17f632c9 100644 --- a/src/features/markets/components/table/markets-table.tsx +++ b/src/features/markets/components/table/markets-table.tsx @@ -101,7 +101,9 @@ function MarketsTable({ const effectiveTableViewMode = isMobile ? 'compact' : tableViewMode; - const containerClassName = ['flex flex-col gap-2 pb-4', className].filter((value): value is string => Boolean(value)).join(' '); + const containerClassName = ['flex flex-col gap-2 pb-4', loading || isEmpty || markets.length === 0 ? 'container items-center' : className] + .filter((value): value is string => Boolean(value)) + .join(' '); const tableClassNames = ['responsive', tableClassName].filter((value): value is string => Boolean(value)).join(' '); // Header actions (filter, refresh, expand/compact, settings) @@ -199,11 +201,12 @@ function MarketsTable({ title="" actions={headerActions} noPadding={loading || isEmpty || markets.length === 0} + className="w-full" > {loading ? ( ) : isEmpty ? (
diff --git a/src/modals/settings/blacklisted-markets-modal.tsx b/src/modals/settings/blacklisted-markets-modal.tsx index 051d3830..0950f116 100644 --- a/src/modals/settings/blacklisted-markets-modal.tsx +++ b/src/modals/settings/blacklisted-markets-modal.tsx @@ -23,7 +23,8 @@ export function BlacklistedMarketsModal({ isOpen, onOpenChange }: BlacklistedMar const [searchQuery, setSearchQuery] = React.useState(''); const [currentPage, setCurrentPage] = React.useState(1); const { rawMarketsUnfiltered } = useMarkets(); - const { isBlacklisted, addBlacklistedMarket, removeBlacklistedMarket, isDefaultBlacklisted } = useBlacklistedMarkets(); + const { customBlacklistedMarkets, isBlacklisted, addBlacklistedMarket, removeBlacklistedMarket, isDefaultBlacklisted } = + useBlacklistedMarkets(); const { success: toastSuccess } = useStyledToast(); // Reset to page 1 when search query changes @@ -48,7 +49,7 @@ export function BlacklistedMarketsModal({ isOpen, onOpenChange }: BlacklistedMar blacklistedMarkets: blacklisted.sort((a, b) => (a.loanAsset?.symbol ?? '').localeCompare(b.loanAsset?.symbol ?? '')), availableMarkets: available.sort((a, b) => (a.loanAsset?.symbol ?? '').localeCompare(b.loanAsset?.symbol ?? '')), }; - }, [rawMarketsUnfiltered, isBlacklisted]); + }, [rawMarketsUnfiltered, customBlacklistedMarkets, isBlacklisted]); // Filter available markets based on search query // Only show results if user has typed at least 2 characters