'use client'; import { useEffect, useMemo, useState } from 'react'; import { useAccount, useChainId, useReadContract, useReadContracts, useSwitchChain, usePublicClient, useWriteContract, useSendTransaction } from 'wagmi'; import { base, arbitrum } from 'wagmi/chains'; import { Abi, parseUnits } from 'viem'; import debtAbi from '@/ABIs/mortgagefiusdccbbtcupgraded.json'; import Link from 'next/link'; // Minimal ERC-721 ABI for balance/owner/enumeration const erc721Abi = [ { type: 'function', name: 'balanceOf', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }], outputs: [{ type: 'uint256' }] }, { type: 'function', name: 'ownerOf', stateMutability: 'view', inputs: [{ name: 'tokenId', type: 'uint256' }], outputs: [{ type: 'address' }] }, { type: 'function', name: 'supportsInterface', stateMutability: 'view', inputs: [{ name: 'interfaceId', type: 'bytes4' }], outputs: [{ type: 'bool' }] }, // ERC721Enumerable { type: 'function', name: 'tokenOfOwnerByIndex', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }, { name: 'index', type: 'uint256' }], outputs: [{ type: 'uint256' }] }, ] as const satisfies Abi; const DEFAULTS = { [base.id]: { nft: '0xcc9a350c5b1e1c9ecd23d376e6618cdfd6bbbdbe', debt: '0xe93131620945a1273b48f57f453983d270b62dc7', }, [arbitrum.id]: { nft: '0xedE6F5F8A9D6B90b1392Dcc9E7FD8A5B0192Bfe1', debt: '0x9Be2Cf73E62DD3b5dF4334D9A36888394822A33F', }, } as const; // Presets per chain (selectable pairs) const PRESETS: Record = { [base.id]: [ { key: 'cbBTC-USDC', label: 'cbBTC-USDC', nft: '0xcc9a350c5b1e1c9ecd23d376e6618cdfd6bbbdbe', debt: '0xe93131620945a1273b48f57f453983d270b62dc7' }, { key: 'WETH-USDC', label: 'WETH-USDC', nft: '0xab825f45e9e5d2459fb7a1527a8d0ca082c582f4', debt: '0x1be87d273d47c3832ab7853812e9a995a4de9eea' }, ], [arbitrum.id]: [ { key: 'USDTO-WBTC', label: 'USDTO-WBTC', nft: '0xedE6F5F8A9D6B90b1392Dcc9E7FD8A5B0192Bfe1', debt: '0x9Be2Cf73E62DD3b5dF4334D9A36888394822A33F' }, ], }; export default function DappPage() { const { address, isConnected } = useAccount(); const chainId = useChainId(); const { switchChain } = useSwitchChain(); const [selectedChainId, setSelectedChainId] = useState(base.id); const [nftAddress, setNftAddress] = useState(DEFAULTS[base.id].nft); const [debtAddress, setDebtAddress] = useState(DEFAULTS[base.id].debt); const [presetKey, setPresetKey] = useState('cbBTC-USDC'); const [manualWallet, setManualWallet] = useState(''); const [manualTokenId, setManualTokenId] = useState(''); const [detectedTokenIds, setDetectedTokenIds] = useState([]); const [scanBusy, setScanBusy] = useState(false); const [scanComplete, setScanComplete] = useState(false); const [payInputs, setPayInputs] = useState>({}); const publicClient = usePublicClient({ chainId: selectedChainId }); const { writeContractAsync, isPending: writePending } = useWriteContract(); const { sendTransactionAsync, isPending: txPending } = useSendTransaction(); // Cache helpers type WalletCache = { lastScannedIndex: number; tokenIds: string[]; balance?: string | null; updatedAt: number; complete?: boolean; // when true, stop further ownerOf scans (gap limit reached) }; // Utility: sleep for backoff const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); type ContractCache = { wallets: Record; enumerable?: boolean; }; const effectiveWallet = useMemo(() => (manualWallet?.trim() ? manualWallet.trim() : address) || '', [manualWallet, address]); const cacheKey = useMemo(() => { if (!nftAddress) return ''; return `nftScan:v1:${selectedChainId}:${nftAddress.toLowerCase()}`; }, [nftAddress, selectedChainId]); const loadCache = (): ContractCache | null => { try { if (typeof window === 'undefined' || !cacheKey) return null; const raw = localStorage.getItem(cacheKey); if (!raw) return null; return JSON.parse(raw) as ContractCache; } catch { return null; } }; const saveCache = (update: ContractCache) => { try { if (typeof window === 'undefined' || !cacheKey) return; localStorage.setItem(cacheKey, JSON.stringify(update)); } catch {} }; // Hydrate detected tokenIds from cache on changes useEffect(() => { if (!effectiveWallet || !cacheKey) return; const cache = loadCache() || { wallets: {} }; const w = cache.wallets[effectiveWallet.toLowerCase()]; if (w) { const ids = (w.tokenIds || []).map((s) => BigInt(s)); setDetectedTokenIds(Array.from(new Set(ids))); setScanComplete(Boolean(w.complete)); console.log('[NFT cache] Hydrated from cache', { cacheKey, wallet: effectiveWallet.toLowerCase(), lastScannedIndex: w.lastScannedIndex, count: w.tokenIds?.length || 0, }); } else { setDetectedTokenIds([]); setScanComplete(false); console.log('[NFT cache] No wallet entry in cache. Starting fresh', { cacheKey, wallet: effectiveWallet.toLowerCase() }); } }, [effectiveWallet, cacheKey]); // OwnerOf scan (batch of 12) const scanMore = async () => { if (!publicClient || !effectiveWallet || !nftAddress) return; // Respect completed scans const existing = loadCache(); const existingEntry = existing?.wallets?.[effectiveWallet.toLowerCase()]; if (existingEntry?.complete) { console.log('[Scan] Already marked complete via gap-limit; skipping.'); return; } setScanBusy(true); try { console.log('[Scan] Starting batch scan…', { nftAddress, wallet: effectiveWallet, chainId: selectedChainId }); const cache = loadCache() || { wallets: {} }; const key = effectiveWallet.toLowerCase(); if (!cache.wallets[key]) { cache.wallets[key] = { lastScannedIndex: -1, tokenIds: [], balance: null, updatedAt: Math.floor(Date.now() / 1000), complete: false }; } const w = cache.wallets[key]; const start = w.lastScannedIndex + 1; const end = start + 11; const indices = Array.from({ length: 12 }, (_, i) => start + i); console.log('[Scan] Indices', { start, end, indices }); const results: Array<{ i: number; owner: string | null; rateLimited?: boolean }> = []; let hadRateLimit = false; let consecutiveGaps = 0; // count of consecutive nonexistent token errors let gapLimitTriggered = false; for (const i of indices) { console.log('[Scan] ownerOf call', { tokenId: i }); let attempt = 0; let success = false; let owner: string | null = null; let wasRateLimitedForThis = false; while (attempt < 3 && !success) { try { owner = await publicClient.readContract({ abi: erc721Abi, address: nftAddress as `0x${string}`, functionName: 'ownerOf', args: [BigInt(i)], }) as string; success = true; console.log('[Scan] ownerOf result', { tokenId: i, owner }); } catch (err: any) { const msg = String(err?.message || err); const code = (err && (err.code || err.status)) as any; const is429 = msg.includes('429') || msg.toLowerCase().includes('rate') || code === 429; console.warn('[Scan] ownerOf error', { tokenId: i, attempt, is429, err }); if (is429) { hadRateLimit = true; wasRateLimitedForThis = true; const backoff = 300 * Math.pow(2, attempt); // 300ms, 600ms, 1200ms console.log('[Scan] Backing off due to 429/rate limit', { tokenId: i, backoffMs: backoff }); await sleep(backoff); } else { // Non rate-limit error, do not retry more than once break; } } attempt++; } // Track gap-limit: count only definitive non-existent (not rate limited) if (!success && !wasRateLimitedForThis) { consecutiveGaps++; console.log('[Scan] Nonexistent token encountered; consecutive gaps =', consecutiveGaps); if (consecutiveGaps >= 5) { gapLimitTriggered = true; console.warn('[Scan] Gap limit reached; stopping scan and marking as complete.'); } } else if (success) { consecutiveGaps = 0; } results.push({ i, owner, rateLimited: !success && wasRateLimitedForThis }); // Small pacing to avoid bursts await sleep(50); if (gapLimitTriggered) break; } const mine = results.filter((r) => (r.owner as string)?.toLowerCase() === effectiveWallet.toLowerCase()).map((r) => r.i.toString()); console.log('[Scan] Batch done', { foundForMe: mine, total: results.length }); // Update cache const nextIds = Array.from(new Set([...(w.tokenIds || []), ...mine])); cache.wallets[key] = { ...w, lastScannedIndex: hadRateLimit ? w.lastScannedIndex : gapLimitTriggered ? (results.at(-1)?.i ?? w.lastScannedIndex) : end, tokenIds: nextIds, updatedAt: Math.floor(Date.now() / 1000), complete: gapLimitTriggered ? true : w.complete || false, }; saveCache(cache); setDetectedTokenIds(nextIds.map((s) => BigInt(s))); setScanComplete(Boolean(cache.wallets[key].complete)); if (hadRateLimit) { console.warn('[Scan] Rate limited encountered; did not advance lastScannedIndex. Try again shortly.'); } } finally { setScanBusy(false); } }; const resetScan = () => { if (!effectiveWallet || !cacheKey) return; const cache = loadCache() || { wallets: {} }; const key = effectiveWallet.toLowerCase(); cache.wallets[key] = { lastScannedIndex: -1, tokenIds: [], balance: null, updatedAt: Math.floor(Date.now() / 1000), complete: false }; saveCache(cache); setDetectedTokenIds([]); setScanComplete(false); console.log('[Scan] Reset wallet cache', { cacheKey, wallet: effectiveWallet.toLowerCase() }); }; const deleteLocalCache = () => { if (!cacheKey) return; try { localStorage.removeItem(cacheKey); setDetectedTokenIds([]); console.log('[Cache] Deleted contract-scoped cache', { cacheKey }); } catch (e) { console.warn('[Cache] Failed to delete cache', { cacheKey, e }); } }; const [canEnumerate, setCanEnumerate] = useState(null); // Ensure selected chain for reads useEffect(() => { if (isConnected && chainId && chainId !== selectedChainId) { try { switchChain({ chainId: selectedChainId }); } catch (e) { // ignore } } }, [isConnected, chainId, selectedChainId, switchChain]); // Helper to load chain defaults into address inputs const loadChainDefaults = (id: number) => { const d = DEFAULTS[id as keyof typeof DEFAULTS]; if (d) { setNftAddress(d.nft); setDebtAddress(d.debt); // set first preset for chain const p = PRESETS[id]?.[0]; if (p) setPresetKey(p.key); console.log('[UI] Loaded defaults for chain', { selectedChainId: id, nft: d.nft, debt: d.debt }); } }; // Auto-load defaults when the selected network changes useEffect(() => { loadChainDefaults(selectedChainId); }, [selectedChainId]); // Detect ERC721Enumerable support: interfaceId 0x780e9d63 const { data: supportsEnumerable } = useReadContract({ abi: erc721Abi, address: nftAddress as `0x${string}`, functionName: 'supportsInterface', args: ['0x780e9d63'], chainId: selectedChainId, query: { enabled: !!nftAddress }, }); useEffect(() => { if (supportsEnumerable !== undefined) setCanEnumerate(Boolean(supportsEnumerable)); }, [supportsEnumerable]); // Fetch balance const { data: balance } = useReadContract({ abi: erc721Abi, functionName: 'balanceOf', args: [effectiveWallet as `0x${string}`], chainId: selectedChainId, query: { enabled: !!effectiveWallet }, }); // Enumerate first up to 10 tokenIds (adjust as needed) useEffect(() => { const fetchTokens = async () => { if (!effectiveWallet || !balance || !canEnumerate) return; const count = Number(balance); const max = Math.min(count, 10); const reads = Array.from({ length: max }, (_, i) => ({ abi: erc721Abi, address: nftAddress as `0x${string}`, functionName: 'tokenOfOwnerByIndex' as const, args: [effectiveWallet as `0x${string}`, BigInt(i)], chainId: selectedChainId, })); try { const res = await (window as any).ethereum; // just to ensure provider exists if (!res) return; } catch {} // We'll use a simple loop via public client later if needed; for now, rely on wagmi's useReadContracts not in hook }; fetchTokens(); }, [effectiveWallet, balance, canEnumerate, nftAddress, selectedChainId]); // Resolve tokenIds: either detected or manual entry const tokenIds = useMemo(() => { const manual = manualTokenId.trim() ? [BigInt(manualTokenId.trim())] : []; return [...detectedTokenIds, ...manual]; }, [detectedTokenIds, manualTokenId]); // Build reads for debt contract const debtReads = useMemo(() => { if (!tokenIds.length) return [] as any[]; return tokenIds.flatMap((id) => [ { abi: debtAbi as Abi, address: debtAddress as `0x${string}`, functionName: 'openDebt', args: [id], chainId: selectedChainId, }, { abi: debtAbi as Abi, address: debtAddress as `0x${string}`, functionName: 'feeSize', args: [id], chainId: selectedChainId, }, { abi: debtAbi as Abi, address: debtAddress as `0x${string}`, functionName: 'coinSize', args: [id], chainId: selectedChainId, }, { abi: debtAbi as Abi, address: debtAddress as `0x${string}`, functionName: 'amountPaid', args: [id], chainId: selectedChainId, }, { abi: debtAbi as Abi, address: debtAddress as `0x${string}`, functionName: 'startDate', args: [id], chainId: selectedChainId, }, { abi: debtAbi as Abi, address: debtAddress as `0x${string}`, functionName: 'expiration', args: [id], chainId: selectedChainId, }, { abi: debtAbi as Abi, address: debtAddress as `0x${string}`, functionName: 'baseSize', args: [id], chainId: selectedChainId, }, ]); }, [tokenIds, debtAddress, selectedChainId]); const { data: debtResults, isLoading: debtLoading, error: debtError, refetch } = useReadContracts({ contracts: debtReads as any, query: { enabled: debtReads.length > 0 }, }); // Global reads (chain-level): APR and token addresses const { data: apr } = useReadContract({ abi: debtAbi as Abi, address: debtAddress as `0x${string}`, functionName: 'calculateAPR', args: [], chainId: selectedChainId, query: { enabled: !!debtAddress }, }); const { data: stableAddr } = useReadContract({ abi: debtAbi as Abi, address: debtAddress as `0x${string}`, functionName: 'stablecoin', args: [], chainId: selectedChainId, query: { enabled: !!debtAddress }, }); const { data: contractCoinAddr } = useReadContract({ abi: debtAbi as Abi, address: debtAddress as `0x${string}`, functionName: 'contractCoin', args: [], chainId: selectedChainId, query: { enabled: !!debtAddress }, }); // Minimal ERC20 ABI for symbol/decimals const erc20Abi = useMemo(() => ([ { type: 'function', name: 'decimals', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint8' }] }, { type: 'function', name: 'symbol', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] }, { type: 'function', name: 'balanceOf', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }], outputs: [{ type: 'uint256' }] }, { type: 'function', name: 'allowance', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], outputs: [{ type: 'uint256' }] }, { type: 'function', name: 'approve', stateMutability: 'nonpayable', inputs: [{ name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }], outputs: [{ type: 'bool' }] }, ]) as const satisfies Abi, []); const { data: stableDecimals } = useReadContract({ abi: erc20Abi, address: (stableAddr as `0x${string}`) || undefined, functionName: 'decimals', chainId: selectedChainId, query: { enabled: !!stableAddr }, }); const { data: stableSymbol } = useReadContract({ abi: erc20Abi, address: (stableAddr as `0x${string}`) || undefined, functionName: 'symbol', chainId: selectedChainId, query: { enabled: !!stableAddr }, }); const { data: coinDecimals } = useReadContract({ abi: erc20Abi, address: (contractCoinAddr as `0x${string}`) || undefined, functionName: 'decimals', chainId: selectedChainId, query: { enabled: !!contractCoinAddr }, }); const { data: coinSymbol } = useReadContract({ abi: erc20Abi, address: (contractCoinAddr as `0x${string}`) || undefined, functionName: 'symbol', chainId: selectedChainId, query: { enabled: !!contractCoinAddr }, }); // Debt token (ERC20 implemented at debtAddress) metadata and balance const { data: debtTokenDecimals } = useReadContract({ abi: erc20Abi, address: (debtAddress as `0x${string}`) || undefined, functionName: 'decimals', chainId: selectedChainId, query: { enabled: !!debtAddress }, }); const { data: debtTokenSymbol } = useReadContract({ abi: erc20Abi, address: (debtAddress as `0x${string}`) || undefined, functionName: 'symbol', chainId: selectedChainId, query: { enabled: !!debtAddress }, }); const { data: debtTokenBalance } = useReadContract({ abi: erc20Abi, address: (debtAddress as `0x${string}`) || undefined, functionName: 'balanceOf', args: [address as `0x${string}`], chainId: selectedChainId, query: { enabled: !!debtAddress && !!address }, }); // Stablecoin balance and allowance to debt const { data: contractCoinBalance } = useReadContract({ abi: erc20Abi, address: (contractCoinAddr as `0x${string}`) || undefined, functionName: 'balanceOf', args: [address as `0x${string}`], chainId: selectedChainId, query: { enabled: !!contractCoinAddr && !!address }, }); const { data: stableBalance } = useReadContract({ abi: erc20Abi, address: (stableAddr as `0x${string}`) || undefined, functionName: 'balanceOf', args: [address as `0x${string}`], chainId: selectedChainId, query: { enabled: !!stableAddr && !!address }, }); const { data: stableAllowance } = useReadContract({ abi: erc20Abi, address: (stableAddr as `0x${string}`) || undefined, functionName: 'allowance', args: [address as `0x${string}`, debtAddress as `0x${string}`], chainId: selectedChainId, query: { enabled: !!stableAddr && !!address && !!debtAddress }, }); // Payment input setter and actions const setPaymentAmount = (tokenId: bigint, value: string) => { setPayInputs((prev) => ({ ...prev, [tokenId.toString()]: value })); }; const handleApprove = async (tokenId: bigint) => { try { if (!stableAddr || !debtAddress) return; const dec = Number(stableDecimals ?? 6); const raw = payInputs[tokenId.toString()] || '0'; const amount = parseUnits(raw || '0', dec); await writeContractAsync({ abi: erc20Abi, address: stableAddr as `0x${string}`, functionName: 'approve', args: [debtAddress as `0x${string}`, amount], chainId: selectedChainId, }); // No direct refetch for allowance; rely on revalidation } catch (e) { console.warn('[Approve] Failed', e); } }; const handlePay = async (tokenId: bigint) => { try { if (!debtAddress) return; const dec = Number(stableDecimals ?? 6); const raw = payInputs[tokenId.toString()] || '0'; let amount = parseUnits(raw || '0', dec); const max = payMaxByTokenId[tokenId.toString()]; if (typeof max === 'bigint' && amount > max) amount = max; // cap to reset-timer amount if (amount === BigInt(0)) return; await writeContractAsync({ abi: debtAbi as Abi, address: debtAddress as `0x${string}`, functionName: 'payDownContract', args: [tokenId, amount], chainId: selectedChainId, }); // Refresh debt data after payment try { await refetch(); } catch {} } catch (e) { console.warn('[Pay] Failed', e); } }; const parsed = useMemo(() => { if (!debtResults || !tokenIds.length) return [] as Array<{ tokenId: bigint; currentPaymentPending?: bigint; debtAtThisSize?: bigint; secondsTillLiq?: bigint; feeSize?: bigint; coinSize?: bigint; amountPaid?: bigint; startDate?: bigint; expiration?: bigint; baseSize?: bigint; }>; const out: Array = []; for (let i = 0; i < tokenIds.length; i++) { const baseIdx = i * 7; // openDebt, feeSize, coinSize, amountPaid, startDate, expiration, baseSize const openDebtRes = debtResults[baseIdx + 0]?.result as any; const feeRes = debtResults[baseIdx + 1]?.result as any; const coinSizeRes = debtResults[baseIdx + 2]?.result as any; const amountPaidRes = debtResults[baseIdx + 3]?.result as any; const startDateRes = debtResults[baseIdx + 4]?.result as any; const expirationRes = debtResults[baseIdx + 5]?.result as any; const baseSizeRes = debtResults[baseIdx + 6]?.result as any; out.push({ tokenId: tokenIds[i], currentPaymentPending: openDebtRes?.[0] as bigint | undefined, debtAtThisSize: openDebtRes?.[1] as bigint | undefined, secondsTillLiq: openDebtRes?.[2] as bigint | undefined, feeSize: feeRes as bigint | undefined, coinSize: coinSizeRes as bigint | undefined, amountPaid: amountPaidRes as bigint | undefined, startDate: startDateRes as bigint | undefined, expiration: expirationRes as bigint | undefined, baseSize: baseSizeRes as bigint | undefined, }); } return out; }, [debtResults, tokenIds]); // Map tokenId -> max payable (Amount to reset timer) const payMaxByTokenId = useMemo(() => { const m: Record = {}; for (const row of parsed) { if (row.currentPaymentPending !== undefined) m[row.tokenId.toString()] = row.currentPaymentPending as bigint; } return m; }, [parsed]); const fmt = (v?: bigint, decimals = 0, precision = 6) => { if (v === undefined) return '-'; const factor = BigInt(10) ** BigInt(decimals); const int = v / factor; const frac = v % factor; const fracStr = (factor + frac).toString().slice(1).padStart(Number(decimals), '0').slice(0, precision); return `${int.toString()}${precision > 0 ? '.' + fracStr : ''}`; }; // Initialize default pay amounts to the "Amount to reset timer" when parsed data loads useEffect(() => { if (!parsed.length) return; const dec = Number(stableDecimals ?? 6); setPayInputs((prev) => { const next = { ...prev }; for (const row of parsed) { const key = row.tokenId.toString(); if (next[key] === undefined && row.currentPaymentPending !== undefined) { next[key] = fmt(row.currentPaymentPending, dec, dec); } } return next; }); }, [parsed, stableDecimals]); const aprDisplay = useMemo(() => { if (apr === undefined) return '-'; // Try to infer scale: common is APR in basis points (1e2) or 1e18. const n = BigInt(apr as any); // Heuristic: if n > 1e9 assume 1e18 scale, else basis points (1e2) if (n > BigInt(1000000000)) { // percent with 2 decimals: (n / 1e16) const percentScaled = n / BigInt(10000000000000000); // 1e16 return `${fmt(percentScaled, 2, 2)} %`; } // basis points -> percent with 2 decimals return `${fmt(n, 2, 2)} %`; }, [apr]); const fmtDuration = (seconds?: bigint) => { if (seconds === undefined) return '-'; let s = Number(seconds); if (!Number.isFinite(s) || s < 0) return '-'; const d = Math.floor(s / 86400); s %= 86400; const h = Math.floor(s / 3600); s %= 3600; const m = Math.floor(s / 60); return `${d}d ${h}h ${m}m`; }; // Compound action: raw tx to debt token contract with provided data const handleCompound = async () => { try { if (!debtAddress) return; await sendTransactionAsync({ to: debtAddress as `0x${string}`, data: '0x4e71d92d', chainId: selectedChainId, }); } catch (e) { console.warn('[Compound] Failed', e); } }; return (

MortgageFi DApp

Connect wallet, detect your NFTs, and fetch debt details.

{!isConnected && (
Please connect your wallet using the Connect Wallet button in the navbar.
)}
Inputs
{/* Preset selector for pair addresses */}
{scanComplete && (
Owner scan completed (gap limit reached). Reset or delete cache to rescan.
)}
Cached IDs: {detectedTokenIds.length ? detectedTokenIds.map((id) => id.toString()).join(', ') : '—'}
About

Enter your NFT contract and token ID(s), then fetch debt details from the mortgage contract.

Base network is required. The app will attempt to detect your token IDs via on-chain queries.

Chain: {selectedChainId === base.id ? 'Base' : selectedChainId === arbitrum.id ? 'Arbitrum' : 'Other'} {String(coinSymbol ?? '')} Balance: {fmt(contractCoinBalance as bigint | undefined, Number(coinDecimals ?? 8), 8)} {String(coinSymbol ?? '')} Stable Balance: {fmt(stableBalance as bigint | undefined, Number(stableDecimals ?? 6), 6)} {String(stableSymbol ?? '')} Allowance → Debt: {fmt(stableAllowance as bigint | undefined, Number(stableDecimals ?? 6), 6)} {String(stableSymbol ?? '')} Debt Token Balance: {fmt(debtTokenBalance as bigint | undefined, Number(debtTokenDecimals ?? 18), 6)} {String(debtTokenSymbol ?? '')} {(() => { const dec = Number(debtTokenDecimals ?? 18); const bal = (debtTokenBalance as bigint | undefined) ?? undefined; const oneToken = BigInt(10) ** BigInt(dec); const hasOnePlus = bal !== undefined && bal > oneToken; const manualSet = Boolean(manualWallet?.trim()); const canCompound = !!address && !manualSet && !!debtAddress && hasOnePlus && !txPending; return ( ); })()}
{debtError &&
{String(debtError?.message || debtError)}
}
{debtLoading &&
Loading...
} {!debtLoading && parsed.map((row) => { const stableDec = Number(stableDecimals ?? 6); const coinDec = Number(coinDecimals ?? 8); // Interpret expiration: if small (<= 200), treat as years; otherwise treat as unix seconds let loanTermDisplay = '-'; if (row.expiration !== undefined) { const exp = Number(row.expiration); if (!Number.isNaN(exp)) { if (exp <= 200) { loanTermDisplay = `${exp} Years`; } else { loanTermDisplay = new Date(exp * 1000).toLocaleDateString(); } } } // Precompute values const costToClose2pct: bigint | undefined = row.baseSize !== undefined ? (row.baseSize as bigint) * BigInt(2) / BigInt(100) : undefined; const totalCostToClose102pct: bigint | undefined = row.baseSize !== undefined ? (row.baseSize as bigint) * BigInt(102) / BigInt(100) : undefined; const monthlyPaymentFixed: bigint | undefined = row.baseSize !== undefined && row.feeSize !== undefined ? ((row.baseSize as bigint) + (row.feeSize as bigint)) / BigInt(360) : undefined; const isClosed = row.coinSize !== undefined && row.coinSize === BigInt(0); return (
Loan Details for {String(stableSymbol ?? 'USDC')}-{String(coinSymbol ?? 'cbBTC')} - ID: {row.tokenId.toString()}
{isClosed && ( Closed/Defaulted )}
Loan Collateral
{fmt(row.coinSize, coinDec, 8)} {String(coinSymbol ?? '')}
Interest Rate
{aprDisplay}
Loan Dollar Value
{fmt(row.baseSize, stableDec, 6)} {String(stableSymbol ?? '')}
Cost to close Loan
{fmt(costToClose2pct, stableDec, 6)} {String(stableSymbol ?? '')}
Total Cost to Close
{fmt(totalCostToClose102pct, stableDec, 6)} {String(stableSymbol ?? '')}
Total Interest to pay over Length of Loan
{fmt(row.feeSize, stableDec, 6)} {String(stableSymbol ?? '')}
Total Repaid
{fmt(row.amountPaid, stableDec, 6)} {String(stableSymbol ?? '')}
Debt Remaining
{fmt(row.debtAtThisSize, stableDec, 6)} {String(stableSymbol ?? '')}
Amount to reset timer
{fmt(row.currentPaymentPending, stableDec, 6)} {String(stableSymbol ?? '')}
Monthly Payment Size
{fmt(monthlyPaymentFixed, stableDec, 6)} {String(stableSymbol ?? '')}
Loan Term
{loanTermDisplay}
Start Date
{row.startDate ? new Date(Number(row.startDate) * 1000).toLocaleDateString() : '-'}
Seconds Till Liquidation
{fmtDuration(row.secondsTillLiq)}
{/* Pay controls (hidden for Closed/Defaulted loans) */} {!isClosed && (
Make a Payment
{(() => { const key = row.tokenId.toString(); const inputVal = payInputs[key] ?? ''; const dec = Number(stableDecimals ?? 6); let amount: bigint | null = null; try { amount = inputVal ? parseUnits(inputVal, dec) : null; } catch { amount = null; } const hasAllowance = stableAllowance !== undefined && amount !== null && (stableAllowance as bigint) >= amount; const hasBalance = stableBalance !== undefined && amount !== null && (stableBalance as bigint) >= amount; const disableAll = !stableAddr || !debtAddress; return ( <> setPaymentAmount(row.tokenId, e.target.value)} /> {!hasAllowance ? ( ) : null} {!hasBalance && amount !== null && ( Insufficient balance )} ); })()}
)}
); })}
If there's a specific "start date" function in the ABI, let me know its exact name so I can add it to the reads.
); }