// chain/app-chain.jsx — React glue over chain/core.js: wallet session, address book, read/write contracts, tx lifecycle. // Exports to window: ChainProvider, useChain, useLoad, useEpoch, usePrices, useTokenBalance, useAllowance, ensureAllowance const ChainContext = React.createContext(null); function ChainProvider({ children }) { const { useState, useEffect, useMemo, useCallback, useRef } = React; const { ethers, CHAINS, DEFAULT_CHAIN, loadBook, rpcUrl, bindBook } = window.Chain; const [wallet, setWallet] = useState(null); // { info, provider } from EIP-6963 const [account, setAccount] = useState(null); const [walletChainId, setWalletChainId] = useState(null); const [book, setBook] = useState(undefined); // undefined = loading, null = no deployment on this chain const [tick, setTick] = useState(0); const [blockNumber, setBlockNumber] = useState(0); const chainId = walletChainId ?? DEFAULT_CHAIN; const chain = CHAINS[chainId] || { name: `Chain ${chainId}`, short: '?', native: 'ETH', explorer: null }; const readProvider = useMemo(() => { const url = rpcUrl(chainId); return url ? new ethers.JsonRpcProvider(url, chainId, { staticNetwork: true, polling: true, pollingInterval: 4000 }) : null; }, [chainId]); const browserProvider = useMemo(() => (wallet ? new ethers.BrowserProvider(wallet.provider, 'any') : null), [wallet]); useEffect(() => { let alive = true; setBook(undefined); loadBook(chainId).then((b) => { if (alive) setBook(b); }); return () => { alive = false; }; }, [chainId]); useEffect(() => { if (!readProvider) return; const onBlock = (n) => setBlockNumber(n); readProvider.on('block', onBlock); return () => { readProvider.off('block', onBlock); }; }, [readProvider]); useEffect(() => { if (!wallet) return; const p = wallet.provider; const onAccounts = (accs) => setAccount(accs[0] ? ethers.getAddress(accs[0]) : null); const onChain = (hex) => setWalletChainId(Number(hex)); const onDisconnect = () => { setAccount(null); setWallet(null); setWalletChainId(null); }; p.on?.('accountsChanged', onAccounts); p.on?.('chainChanged', onChain); p.on?.('disconnect', onDisconnect); return () => { p.removeListener?.('accountsChanged', onAccounts); p.removeListener?.('chainChanged', onChain); p.removeListener?.('disconnect', onDisconnect); }; }, [wallet]); const connect = useCallback(async (w) => { const accs = await w.provider.request({ method: 'eth_requestAccounts' }); const hex = await w.provider.request({ method: 'eth_chainId' }); setWallet(w); setAccount(ethers.getAddress(accs[0])); setWalletChainId(Number(hex)); localStorage.setItem('anarchy.wallet', w.info.rdns); return accs[0]; }, []); const disconnect = useCallback(() => { setWallet(null); setAccount(null); setWalletChainId(null); localStorage.removeItem('anarchy.wallet'); }, []); useEffect(() => { const rdns = localStorage.getItem('anarchy.wallet'); if (!rdns) return; const t = setTimeout(async () => { const w = window.Chain.providers().find((x) => x.info.rdns === rdns); if (!w) return; try { const accs = await w.provider.request({ method: 'eth_accounts' }); if (accs.length) await connect(w); } catch {} }, 300); return () => clearTimeout(t); }, [connect]); const switchChain = useCallback(async (target) => { if (!wallet) return; const c = CHAINS[target]; try { await wallet.provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: c.hex }] }); } catch (e) { if (e?.code === 4902 || /unrecognized|not added/i.test(e?.message || '')) { await wallet.provider.request({ method: 'wallet_addEthereumChain', params: [{ chainId: c.hex, chainName: c.name, rpcUrls: [rpcUrl(target)], nativeCurrency: { name: c.native, symbol: c.native, decimals: 18 }, blockExplorerUrls: c.explorer ? [c.explorer] : [] }], }); } else throw e; } }, [wallet]); const read = useMemo(() => (book && readProvider ? bindBook(book, readProvider) : null), [book, readProvider]); const getSigner = useCallback(async () => { if (!browserProvider) throw new Error('Connect a wallet first'); return browserProvider.getSigner(); }, [browserProvider]); const write = useCallback(async () => { if (!book) throw new Error('No deployment on this chain'); return bindBook(book, await getSigner()); }, [book, getSigner]); const refresh = useCallback(() => setTick((t) => t + 1), []); const tokens = useMemo(() => (book?.tokens ?? []).map((t) => ({ ...t, address: ethers.getAddress(t.address) })), [book]); const tokenBySym = useMemo(() => Object.fromEntries(tokens.map((t) => [t.symbol, t])), [tokens]); const tokenByAddr = useMemo(() => Object.fromEntries(tokens.map((t) => [t.address.toLowerCase(), t])), [tokens]); const value = { ethers, wallet, account, connected: !!account, chainId, chain, book, tokens, tokenBySym, tokenByAddr, readProvider, browserProvider, read, write, getSigner, connect, disconnect, switchChain, tick, refresh, blockNumber, explorerTx: (h) => (chain.explorer ? `${chain.explorer}/tx/${h}` : null), explorerAddr: (a) => (chain.explorer ? `${chain.explorer}/address/${a}` : null), wrongChain: walletChainId !== null && book === null, }; return {children}; } function useChain() { return React.useContext(ChainContext); } // Runs an async read whenever deps, the account, or the refresh tick change. Keeps the previous value while reloading. function useLoad(fn, deps = [], { every = 0 } = {}) { const { useState, useEffect, useRef } = React; const chain = useChain(); const [state, setState] = useState({ data: undefined, loading: true, error: null }); const seq = useRef(0); useEffect(() => { if (!chain.read) { setState({ data: undefined, loading: chain.book === undefined, error: chain.book === null ? 'no deployment' : null }); return; } let alive = true; const run = async () => { const my = ++seq.current; setState((s) => ({ ...s, loading: true })); try { const data = await fn(chain.read, chain); if (alive && my === seq.current) setState({ data, loading: false, error: null }); } catch (e) { console.warn('read failed', e); if (alive && my === seq.current) setState((s) => ({ data: s.data, loading: false, error: window.Chain.decodeError(e).message })); } }; run(); const id = every ? setInterval(run, every) : null; return () => { alive = false; if (id) clearInterval(id); }; }, [chain.read, chain.account, chain.tick, ...deps]); return state; } function useEpoch() { const { data } = useLoad(async (c) => { const [active, week] = await Promise.all([c.minter.active_period(), c.minter.WEEK()]); const blk = await c.minter.runner.provider.getBlock('latest'); return { activePeriod: Number(active), week: Number(week), chainNow: blk.timestamp, wallClock: Math.floor(Date.now() / 1000) }; }, [], { every: 30000 }); if (!data) return { number: null, endsAt: null, activePeriod: null, week: null, secondsLeft: null }; const drift = data.chainNow - data.wallClock; const endsAt = (data.activePeriod + data.week - drift) * 1000; return { number: Math.floor(data.activePeriod / data.week), endsAt, activePeriod: data.activePeriod, week: data.week, chainDrift: drift, secondsLeft: Math.max(0, data.activePeriod + data.week - data.chainNow), }; } // All pairs from PairAPI plus USD prices, the ARCH price and per-pool TVL. One call, cached by useLoad. function usePairs() { return useLoad(async (c, chain) => { const total = Number(await c.pairFactory.allPairsLength()); const rows = total ? await c.pairApi.getAllPair(chain.account ?? window.Chain.ethers.ZeroAddress, total, 0) : []; const pairs = rows.filter((p) => p.pair_address !== window.Chain.ethers.ZeroAddress).map(plainPair); const { price, dec } = window.Chain.priceTokens(pairs, chain.tokens); const archAddr = chain.book.contracts.arch.toLowerCase(); for (const p of pairs) { const p0 = price[p.token0.toLowerCase()] ?? 0, p1 = price[p.token1.toLowerCase()] ?? 0; p.tvl = window.Chain.fmt.toNum(p.reserve0, p.token0_decimals) * p0 + window.Chain.fmt.toNum(p.reserve1, p.token1_decimals) * p1; p.lpPrice = p.total_supply > 0n ? p.tvl / window.Chain.fmt.toNum(p.total_supply, 18) : 0; p.isArchPair = p.token0.toLowerCase() === archAddr || p.token1.toLowerCase() === archAddr; } return { pairs, price, dec, archPrice: price[archAddr] ?? 0 }; }, []); } function plainPair(p) { return { pair_address: p.pair_address, symbol: p.symbol, name: p.name, stable: p.stable, total_supply: p.total_supply, token0: p.token0, token0_symbol: p.token0_symbol, token0_decimals: Number(p.token0_decimals), reserve0: p.reserve0, claimable0: p.claimable0, token1: p.token1, token1_symbol: p.token1_symbol, token1_decimals: Number(p.token1_decimals), reserve1: p.reserve1, claimable1: p.claimable1, gauge: p.gauge, gauge_total_supply: p.gauge_total_supply, fee: p.fee, bribe: p.bribe, emissions: p.emissions, account_lp_balance: p.account_lp_balance, account_token0_balance: p.account_token0_balance, account_token1_balance: p.account_token1_balance, account_gauge_balance: p.account_gauge_balance, account_gauge_earned: p.account_gauge_earned, }; } function useTokenBalance(token) { const chain = useChain(); return useLoad(async (c) => { if (!chain.account || !token) return null; if (token.native) return c.router.runner.provider.getBalance(chain.account); return c.at('ERC20', token.address).balanceOf(chain.account); }, [token?.address, chain.blockNumber]); } // Approves `spender` for `amount` of `token` if the allowance is short. Returns the approval receipt or null. async function ensureAllowance(w, owner, tokenAddress, spender, amount, onStage) { const erc = w.at('ERC20', tokenAddress); const have = await erc.allowance(owner, spender); if (have >= amount) return null; onStage?.('approve'); const tx = await erc.approve(spender, window.Chain.ethers.MaxUint256); return tx.wait(); } Object.assign(window, { ChainProvider, useChain, useLoad, useEpoch, usePairs, useTokenBalance, ensureAllowance });