From 4c67bf3a1522adb04b6059d7b4b9b2d51659fff0 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Fri, 29 Aug 2025 09:05:58 +0400 Subject: [PATCH] added nftcache functionality and cleaned up the settings page --- app/dapp/page.tsx | 85 ++++++- components/SettingsModal.tsx | 462 ++++++++++++++++++++--------------- 2 files changed, 350 insertions(+), 197 deletions(-) diff --git a/app/dapp/page.tsx b/app/dapp/page.tsx index e9cebb8..2ba0978 100644 --- a/app/dapp/page.tsx +++ b/app/dapp/page.tsx @@ -78,6 +78,23 @@ export default function DappPage() { const { writeContractAsync, isPending: writePending } = useWriteContract(); const { sendTransactionAsync, isPending: txPending } = useSendTransaction(); + // NFTCache settings (from localStorage; defaults to env or '/nftcache') + const [nftcacheEnabled, setNftcacheEnabled] = useState(false); + const [nftcacheBaseUrl, setNftcacheBaseUrl] = useState((process.env.NEXT_PUBLIC_NFTCACHE_URL || process.env.NFTCACHE_URL || '/nftcache').replace(/\/$/, '')); + const [nftcacheApiKey, setNftcacheApiKey] = useState((process.env.NEXT_PUBLIC_NFTCACHE_API_KEY || process.env.NFTCACHE_API_KEY || '').trim()); + useEffect(() => { + try { + if (typeof window !== 'undefined' && window.localStorage) { + const ls = window.localStorage; + setNftcacheEnabled(ls.getItem('nftcache:enabled') === '1'); + const nurl = (ls.getItem('nftcache:baseUrl') || '').trim(); + if (nurl) setNftcacheBaseUrl(nurl.replace(/\/$/, '')); + const nkey = (ls.getItem('nftcache:apiKey') || '').trim(); + if (nkey) setNftcacheApiKey(nkey); + } + } catch {} + }, []); + // Notification settings (global) and per-position preferences const [settingsOpen, setSettingsOpen] = useState(false); @@ -203,6 +220,26 @@ export default function DappPage() { return `nftScan:v1:${selectedChainId}:${nftAddress.toLowerCase()}`; }, [nftAddress, selectedChainId]); + // Resolve network key used by NFTCache API + const nftcacheNetworkKey = useMemo(() => { + if (selectedChainId === base.id) return 'base'; + if (selectedChainId === arbitrum.id) return 'arb'; + if (selectedChainId === mainnet.id) return 'eth'; + return String(selectedChainId); + }, [selectedChainId]); + + // Map known contracts to nftcache slug keys + const nftcacheContractSlug = useMemo(() => { + const addr = (nftAddress || '').toLowerCase(); + // base + if (addr === '0xcc9a350c5b1e1c9ecd23d376e6618cdfd6bbbdbe') return 'cbbtc'; + if (addr === '0xab825f45e9e5d2459fb7a1527a8d0ca082c582f4') return 'weth'; + // arbitrum + if (addr === '0xede6f5f8a9d6b90b1392dcc9e7fd8a5b0192bfe1') return 'wbbtc'; + // fallback: return empty to use raw address + return ''; + }, [nftAddress]); + const loadCache = (): ContractCache | null => { try { if (typeof window === 'undefined' || !cacheKey) return null; @@ -242,6 +279,41 @@ export default function DappPage() { } }, [effectiveWallet, cacheKey]); + // Try NFTCache fetch first when enabled; fallback to local cache/scan + useEffect(() => { + const run = async () => { + if (!nftcacheEnabled) return; // disabled => keep current behavior + if (!effectiveWallet || !nftAddress) return; + try { + setScanBusy(true); + const contractParam = nftcacheContractSlug || nftAddress; + const url = `${nftcacheBaseUrl}/nfts?network=${encodeURIComponent(nftcacheNetworkKey)}&nft_contract=${encodeURIComponent(contractParam)}&user_wallet=${encodeURIComponent(effectiveWallet)}`; + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), 12000); + const headers: Record = { 'Accept': 'application/json' }; + if (nftcacheApiKey) headers['X-API-Key'] = nftcacheApiKey; + const res = await fetch(url, { signal: ctrl.signal, headers }); + clearTimeout(t); + if (!res.ok) throw new Error(`nftcache status ${res.status}`); + const data = await res.json(); + let ids: string[] = []; + if (Array.isArray(data)) ids = data.map(String); + else if (data && Array.isArray((data as any).tokenIds)) ids = (data as any).tokenIds.map(String); + else if (data && Array.isArray((data as any).token_ids)) ids = (data as any).token_ids.map(String); + const bigs = ids.map((s) => BigInt(s)).filter((x, i, arr) => (arr.indexOf(x) === i)); + setDetectedTokenIds(bigs); + setScanComplete(true); + console.log('[NFTCache] Loaded owner tokens', { count: bigs.length }); + } catch (e) { + console.warn('[NFTCache] Fetch failed; falling back to local scan', e); + } finally { + setScanBusy(false); + } + }; + run(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nftcacheEnabled, nftcacheBaseUrl, nftcacheApiKey, nftcacheNetworkKey, effectiveWallet, nftAddress, nftcacheContractSlug]); + // OwnerOf scan (batch of 12) const scanMore = async () => { if (!publicClient || !effectiveWallet || !nftAddress) return; @@ -442,11 +514,16 @@ export default function DappPage() { // Resolve tokenIds: either detected or manual entry const tokenIds = useMemo(() => { - if (deeplinkTokenId.trim()) { - try { return [BigInt(deeplinkTokenId.trim())]; } catch { return []; } + // Show all sources together: detected, manual, and deep-linked (deduped) + const set = new Set(); + for (const id of detectedTokenIds) set.add(id.toString()); + if (manualTokenId.trim()) { + try { set.add(BigInt(manualTokenId.trim()).toString()); } catch {} } - const manual = manualTokenId.trim() ? [BigInt(manualTokenId.trim())] : []; - return [...detectedTokenIds, ...manual]; + if (deeplinkTokenId.trim()) { + try { set.add(BigInt(deeplinkTokenId.trim()).toString()); } catch {} + } + return Array.from(set).map((s) => BigInt(s)); }, [detectedTokenIds, manualTokenId, deeplinkTokenId]); // Build reads for debt contract diff --git a/components/SettingsModal.tsx b/components/SettingsModal.tsx index cdf788f..9f81d67 100644 --- a/components/SettingsModal.tsx +++ b/components/SettingsModal.tsx @@ -40,6 +40,11 @@ export default function SettingsModal({ open, initial, onClose, onSave }: Settin const [rpcBase, setRpcBase] = useState(""); const [rpcArbitrum, setRpcArbitrum] = useState(""); const [rpcMainnet, setRpcMainnet] = useState(""); + // NFTCache settings (stored in localStorage) + const ENV_NFTCACHE = (process.env.NEXT_PUBLIC_NFTCACHE_URL || process.env.NFTCACHE_URL || '/nftcache').replace(/\/$/, ''); + const [nftcacheEnabled, setNftcacheEnabled] = useState(false); + const [nftcacheBaseUrl, setNftcacheBaseUrl] = useState(ENV_NFTCACHE); + const [nftcacheApiKey, setNftcacheApiKey] = useState(''); useEffect(() => { if (open) { @@ -58,6 +63,13 @@ export default function SettingsModal({ open, initial, onClose, onSave }: Settin setRpcBase(b || 'https://base.llamarpc.com'); setRpcArbitrum(a || ''); setRpcMainnet(m || ''); + // Load NFTCache settings + const nEn = ls?.getItem('nftcache:enabled'); + const nUrl = ls?.getItem('nftcache:baseUrl'); + const nKey = ls?.getItem('nftcache:apiKey'); + setNftcacheEnabled(nEn === '1'); + setNftcacheBaseUrl((nUrl && nUrl.trim()) || ENV_NFTCACHE); + setNftcacheApiKey(nKey || (process.env.NEXT_PUBLIC_NFTCACHE_API_KEY || process.env.NFTCACHE_API_KEY || '')); } catch {} } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -118,35 +130,91 @@ export default function SettingsModal({ open, initial, onClose, onSave }: Settin
Notification Settings
-

Configure your notification provider and scheduler credentials.

-
- +

Configure your notification provider, scheduler, RPC and NFTCache.

- + {/* Section: Basics */} +
+ Basics +
+ + +
+
+ + {/* Section: Email & Timing */} +
+ Email & Timing +
+ + + + + + +
+
+ + {/* Section: Scheduler (Schedy) */} +
+ Scheduler {scheduler === 'schedy' && ( - <> +
- - )} - - - - - - - - {provider === 'ntfy' && ( - <> - - - - )} - - {provider === 'gotify' && ( - <> - - - - )} - - {provider === 'sns' && ( - <> - - - - -
- Storing AWS credentials in the browser is insecure. Prefer a server-side relay. -
- - )} - - {/* RPC settings */} -
-
RPC endpoints (per network)
-
- - - -
Changes apply immediately after Save without rebuild.
+ )} +
+ + {/* Section: Provider-specific */} +
+ Provider Settings +
+ {provider === 'ntfy' && ( + <> + + + + )} + + {provider === 'gotify' && ( + <> + + + + )} + + {provider === 'sns' && ( + <> + + + + +
+ Storing AWS credentials in the browser is insecure. Prefer a server-side relay. +
+ + )}
-
+ + + {/* Section: RPC endpoints */} +
+ RPC endpoints +
+ + + +
Changes apply immediately after Save without rebuild.
+
+
+ + {/* Section: NFTCache */} +
+ NFTCache +
+ + + +
+
@@ -367,6 +437,12 @@ export default function SettingsModal({ open, initial, onClose, onSave }: Settin if (b) ls.setItem('rpc:base', b); else ls.removeItem('rpc:base'); if (a) ls.setItem('rpc:arbitrum', a); else ls.removeItem('rpc:arbitrum'); if (m) ls.setItem('rpc:mainnet', m); else ls.removeItem('rpc:mainnet'); + // Persist NFTCache settings + ls.setItem('nftcache:enabled', nftcacheEnabled ? '1' : '0'); + const nurl = (nftcacheBaseUrl || '').trim(); + if (nurl) ls.setItem('nftcache:baseUrl', nurl); else ls.removeItem('nftcache:baseUrl'); + const nkey = (nftcacheApiKey || '').trim(); + if (nkey) ls.setItem('nftcache:apiKey', nkey); else ls.removeItem('nftcache:apiKey'); } } catch {} onSave(form);