// app-screens.jsx — AnarchyFi reskinned UI screens // Exports to window: TokenIcon, TokenPair, Nav, SwapScreen, LiquidityScreen, ManageScreen, POOLS const { useState, useRef, useEffect } = React; function useMobile() { const [m, setM] = useState(typeof window !== 'undefined' && window.innerWidth < 768); useEffect(() => { const fn = () => setM(window.innerWidth < 768); window.addEventListener('resize', fn); return () => window.removeEventListener('resize', fn); }, []); return m; } /* Epoch clock — 7d epochs aligned to MinterUpgradeable.active_period() */ function useCountdown(target) { const [now, setNow] = useState(Date.now()); useEffect(() => { const iv = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(iv); }, []); const s = target == null ? 0 : Math.max(0, Math.floor((target - now) / 1000)); return { d: Math.floor(s / 86400), h: Math.floor((s % 86400) / 3600), m: Math.floor((s % 3600) / 60), s: s % 60, total: s, ready: target != null }; } const { fmt, ethers: E } = window.Chain; const DEADLINE = () => BigInt(Math.floor(Date.now() / 1000) + 20 * 60); const SLIPPAGE_BPS = 50n; const withSlip = (v) => (v * (10000n - SLIPPAGE_BPS)) / 10000n; const SECONDS_PER_YEAR = 365 * 86400; /* PairAPI row → the shape the tables render. APR = weekly gauge emissions in USD annualised over gauge TVL. */ function toPool(p, prices, archPrice) { const staked = p.gauge !== E.ZeroAddress && p.total_supply > 0n ? fmt.toNum(p.gauge_total_supply, 18) * p.lpPrice : 0; const weeklyUsd = fmt.toNum(p.emissions, 18) * archPrice; const apr = staked > 0 ? (weeklyUsd * 52 / staked) * 100 : 0; const myPool = fmt.toNum(p.account_lp_balance, 18) * p.lpPrice; const myStake = fmt.toNum(p.account_gauge_balance, 18) * p.lpPrice; return { id: p.pair_address, address: p.pair_address, pair: [p.token0_symbol, p.token1_symbol], type: p.stable ? 'Stable' : 'Volatile', stable: p.stable, tvl: fmt.usd(p.tvl), tvlNum: p.tvl, apr: p.gauge === E.ZeroAddress ? '—' : fmt.pct(apr), aprNum: apr, boosted: false, votes: p.gauge !== E.ZeroAddress, gauge: p.gauge, bribe: p.bribe, myPool: myPool > 0 ? fmt.usd(myPool) : '—', myStake: myStake > 0 ? fmt.usd(myStake) : '—', raw: p, }; } const TOKEN_CFG = { ARCH: { bg: '#d62534', label: 'A' }, ETH: { bg: '#627EEA', label: 'Ξ' }, BTC: { bg: '#F7931A', label: '₿' }, USDC: { bg: '#2775CA', label: '$' }, USDT: { bg: '#26A17B', label: '₮' }, ARB: { bg: '#28A0F0', label: 'A' }, BNB: { bg: '#F0B90B', label: 'B' }, WBNB: { bg: '#F0B90B', label: 'B' }, WETH: { bg: '#627EEA', label: 'Ξ' }, FTM: { bg: '#1969FF', label: 'F' }, }; function TokenIcon({ symbol, size = 24, style }) { const c = TOKEN_CFG[symbol] || { bg: '#4a4640', label: (symbol || '?').slice(0, 1) }; return (
{c.label}
); } function TokenPair({ tokens, size = 24 }) { return (
); } /* ── NAV ─────────────────────────────────────────────────────────────── */ const NAV_TABS = [['swap','SWAP'],['liquidity','LIQUIDITY'],['vote','VOTE'],['delegate','DELEGATE'],['pro','PRO'],['match','MATCH'],['bribe','BRIBE'],['il','IL']]; const NAV_TABS_SHORT = { liquidity:'LIQ', delegate:'DELG' }; function Nav({ screen, setScreen, accent, connected, onWalletClick }) { const isMobile = useMobile(); const chain = useChain(); const epoch = useEpoch(); const ep = useCountdown(epoch.endsAt); const short = chain.account ? `${chain.account.slice(0, 6)}...${chain.account.slice(-4)}` : ''; return ( ); } /* ── CARD WRAPPER ────────────────────────────────────────────────────── */ function Card({ children, style, accent, width }) { return (
{/* corner brackets */} {[['top','left'],['top','right'],['bottom','left'],['bottom','right']].map(([v,h]) => (
))} {children}
); } /* ── SWAP SCREEN ─────────────────────────────────────────────────────── */ function SwapScreen({ accent, onTx }) { const isMobile = useMobile(); const chain = useChain(); const TOKENS_LIST = chain.tokens.map(t => t.symbol); const [fromTok, setFromTok] = useState('USDC'); const [toTok, setToTok] = useState(null); const [fromAmt, setFromAmt] = useState(''); const [picking, setPicking] = useState(null); const [quote, setQuote] = useState(null); // { out, outNoFee, stable, error } useEffect(() => { if (TOKENS_LIST.length && !TOKENS_LIST.includes(fromTok)) setFromTok(TOKENS_LIST[0]); }, [TOKENS_LIST.join()]); const from = chain.tokenBySym[fromTok]; const to = toTok ? chain.tokenBySym[toTok] : null; const { data: fromBal } = useTokenBalance(from); const { data: toBal } = useTokenBalance(to); const BALANCE = fromBal == null || !from ? null : fmt.toNum(fromBal, from.decimals); const amountIn = (() => { try { return from && fromAmt ? fmt.parse(fromAmt, from.decimals) : 0n; } catch { return 0n; } })(); useEffect(() => { if (!chain.read || !from || !to || amountIn <= 0n) { setQuote(null); return; } let alive = true; const t = setTimeout(async () => { try { const c = chain.read; const [out, stable] = await c.router.getAmountOut(amountIn, from.address, to.address); if (out === 0n) throw new Error('No pool for this pair'); let outForUser = out; try { const pair = await c.router.pairFor(from.address, to.address, stable); outForUser = await c.at('Pair', pair).getAmountOutFor.staticCall(amountIn, from.address, chain.account ?? E.ZeroAddress, { from: chain.account ?? E.ZeroAddress }); } catch {} if (alive) setQuote({ out: outForUser, outNoFee: out, stable, error: null }); } catch (e) { if (alive) setQuote({ out: 0n, error: window.Chain.decodeError(e).message }); } }, 250); return () => { alive = false; clearTimeout(t); }; }, [chain.read, from?.address, to?.address, amountIn.toString(), chain.account, chain.blockNumber]); const toAmt = quote?.out ? fmt.units(quote.out, to.decimals, 6) : ''; function setPct(p) { if (BALANCE == null) return; let a = BALANCE * p / 100; if (from.native && p === 100) a = Math.max(0, a - 0.01); setFromAmt(a.toFixed(Math.min(6, from.decimals))); } function flip() { if (!toTok) return; setFromTok(toTok); setToTok(fromTok); setFromAmt(toAmt); } function onFromChange(v) { setFromAmt(v.replace(/[^0-9.]/g, '')); } const insufficient = BALANCE != null && parseFloat(fromAmt || '0') > BALANCE; const canSwap = amountIn > 0n && to && quote?.out > 0n && !insufficient && chain.connected; function doSwap() { const minOut = withSlip(quote.out); const routes = [{ from: from.native ? chain.book.contracts.weth : from.address, to: to.native ? chain.book.contracts.weth : to.address, stable: quote.stable }]; onTx('swap', async (w, stage) => { const me = chain.account; if (from.native) return w.router.swapExactETHForTokens(minOut, routes, me, DEADLINE(), { value: amountIn }); await ensureAllowance(w, me, from.address, chain.book.contracts.router, amountIn, stage); stage('sign'); if (to.native) return w.router.swapExactTokensForETH(amountIn, minOut, routes, me, DEADLINE()); return w.router.swapExactTokensForTokens(amountIn, minOut, routes, me, DEADLINE()); }); } return (
{/* Token picker overlay */} {picking && (
setPicking(null)} style={{ position:'fixed', inset:0, background:'rgba(5,5,5,0.82)', zIndex:300, display:'flex', alignItems:'center', justifyContent:'center', }}> e.stopPropagation()}>
SELECT TOKEN
{TOKENS_LIST.filter(t => t !== (picking==='from'?toTok:fromTok)).map(tok => ( ))}
)} {/* header */}
SWAP
{/* FROM */}
FROM BAL: {BALANCE == null ? '—' : fmt.num(BALANCE, 4)} {fromTok}
onFromChange(e.target.value)} placeholder="0.00" style={{ background:'none', border:'none', outline:'none', fontFamily:'var(--font-m)', fontSize:26, fontWeight:700, color:'var(--ink)', width:160, minWidth:0 }} />
{/* % row */}
{[10,25,50,75,100].map(p => ( ))}
{/* flip */}
{/* TO */}
TO BAL: {toBal != null && to ? {fmt.num(fmt.toNum(toBal, to.decimals), 4)} {toTok} : '—'}
{toAmt || '0.00'}
{quote?.out > 0n && (
RATE1 {fromTok} = {fmt.num(fmt.toNum(quote.out, to.decimals) / parseFloat(fromAmt), 6)} {toTok}
MIN RECEIVED ({Number(SLIPPAGE_BPS)/100}% SLIPPAGE){fmt.units(withSlip(quote.out), to.decimals, 6)} {toTok}
ROUTE{quote.stable ? 'STABLE' : 'VOLATILE'} POOL
)} {quote?.error && (
{quote.error}
)}
); } /* ── LIQUIDITY SCREEN ────────────────────────────────────────────────── */ function LiquidityScreen({ accent, onSelect, onTx }) { const isMobile = useMobile(); const chain = useChain(); const [infoPool, setInfoPool] = useState(null); const [search, setSearch] = useState(''); const [filter, setFilter] = useState('all'); const { data: pairData, loading, error } = usePairs(); const POOLS = (pairData?.pairs ?? []).map(p => toPool(p, pairData.price, pairData.archPrice)).sort((a, b) => b.tvlNum - a.tvlNum); const claimable = POOLS.filter(p => p.raw.account_gauge_earned > 0n); function claimAll() { onTx('claim-emissions', async (w) => { const gauges = claimable.map(p => p.gauge); const ids = []; for (const p of claimable) { const g = w.at('VeGauge', p.gauge); const n = Number(await g.balanceOf(chain.account)); for (let i = 0; i < n; i++) ids.push({ gauge: p.gauge, id: await g.tokenOfOwnerByIndex(chain.account, i) }); } const gs = ids.map(x => x.gauge), tids = ids.map(x => x.id); return w.voter.claimRewards(gs, tids); }); } const filtered = POOLS.filter(p => { const q = search.toLowerCase(); const matchQ = p.pair.some(t => t.toLowerCase().includes(q)); const matchF = filter === 'all' || (filter === 'stable' && p.type === 'Stable') || (filter === 'volatile' && p.type === 'Volatile') || (filter === 'mine' && p.myPool !== '—'); return matchQ && matchF; }); return (
{/* Page header */}
LIQUIDITY POOLS
POOLS
{/* Toolbar */}
setSearch(e.target.value)} placeholder="Search..." style={{ background:'none', border:'none', outline:'none', fontFamily:'var(--font-m)', fontSize:12, color:'var(--ink)', width:'100%', letterSpacing:'0.06em' }} />
{(isMobile ? [['all','ALL'],['stable','STB'],['volatile','VOL'],['mine','MINE']] : [['all','ALL'],['stable','STABLE'],['volatile','VOLATILE'],['mine','MY POOLS']] ).map(([k,l]) => ( ))}
{!isMobile && <> } {isMobile && }
{/* Table header */}
{(isMobile?['PAIR','','APR','']:['PAIR','TVL','APR','MY POOL','MY STAKE','']).map((h,i) => (
1 : i>1) ? 'right' : 'left' }}>{h}
))}
{/* Rows */} {filtered.map((pool, i) => ( onSelect(pool)} onInfo={() => setInfoPool(pool)} /> ))} {filtered.length === 0 && (
{loading && !pairData ? 'LOADING POOLS...' : error ? `COULD NOT LOAD POOLS — ${error.toUpperCase()}` : chain.book === null ? 'NOT DEPLOYED ON THIS NETWORK' : 'NO POOLS FOUND'}
)}
{/* Mobile info sheet */} {infoPool && ( setInfoPool(null)} stats={[ { label:'TYPE', value: infoPool.type.toUpperCase(), mono:true }, { label:'APR', value: infoPool.apr, hi:true }, { label:'TVL', value: infoPool.tvl }, { label:'GAUGE', value: infoPool.votes ? 'YES' : 'NO', color: infoPool.votes ? '#4ade80' : 'var(--ink-mute)' }, { label:'MY POOL', value: infoPool.myPool, color: infoPool.myPool==='—'?'var(--ink-mute)':undefined }, { label:'MY STAKE', value: infoPool.myStake, color: infoPool.myStake==='—'?'var(--ink-mute)':undefined }, ]} /> )}
); } function PoolRow({ pool, accent, odd, onClick, onInfo }) { const [hov, setHov] = useState(false); const isMobile = useMobile(); return (
setHov(true)} onMouseLeave={() => setHov(false)} style={{ display:'grid', gridTemplateColumns: isMobile ? '1fr 26px 58px 24px' : '200px 1fr 100px 110px 110px 40px', padding: isMobile ? '12px 14px' : '14px 20px', cursor:'pointer', transition:'background 0.15s', background: hov ? 'rgba(232,230,225,0.04)' : odd ? 'rgba(232,230,225,0.015)' : 'transparent', borderBottom:'1px solid rgba(232,230,225,0.04)', alignItems:'center', }}>
{pool.pair.join('/')} {pool.boosted && !isMobile && ⚡BOOST}
{pool.type.toUpperCase()}{pool.boosted && isMobile ? ' ⚡' : ''} {!pool.votes && (
NO VOTING POWER
)} {pool.votes && !isMobile && (
VOTING ELIGIBLE
)}
{!isMobile &&
{pool.tvl}
} {/* ℹ — mobile only, own column */} {isMobile && (
)} {/* APR */}
{pool.apr}
{!isMobile &&
{pool.myPool}
} {!isMobile &&
{pool.myStake}
}
); } /* ── MANAGE SCREEN ───────────────────────────────────────────────────── */ function ManageScreen({ pool: initial, accent, onBack, onTx, onBoosted }) { const [tab, setTab] = useState('staking'); const isMobile = useMobile(); const chain = useChain(); const { data: pairData } = usePairs(); const live = pairData?.pairs.find(p => p.pair_address === initial.address); const pool = live ? toPool(live, pairData.price, pairData.archPrice) : initial; const { data: positions, loading: posLoading } = useLoad(async (c) => { if (!chain.account || pool.gauge === E.ZeroAddress) return []; const rows = await c.positionLens.positionsOf(pool.gauge, chain.account); const g = c.at('VeGauge', pool.gauge); const [extBribe, intBribe] = await Promise.all([g.external_bribe(), g.internal_bribe()]); return rows.map(r => ({ tokenId: r.tokenId, amount: r.amount, latchedInitialFee: Number(r.latchedInitialFee), entry: Number(r.entry), end: Number(r.end), voted: r.voted, earned: r.earned, exitFeeRate: Number(r.exitFeeRate), feeFree: r.feeFree, netIfWithdrawnNow: r.netIfWithdrawnNow, withdrawBlocker: r.withdrawBlocker, extBribe, intBribe, })); }, [pool.gauge]); return (
{/* Left panel */}
{/* Pool header */}
{pool.pair.join('/')}
{pool.type.toUpperCase()} {!pool.votes && NO GAUGE}
{chain.explorerAddr(pool.address) ? ( {fmt.short(pool.address)} ) : ( {fmt.short(pool.address)} )}
{/* Tab bar */}
{[['deposit','DEPOSIT'],['staking','STAKING'],['positions',`POSITIONS${positions?.length ? ` (${positions.length})` : ''}`]].map(([k,l]) => ( ))}
{tab === 'deposit' && } {tab === 'staking' && } {tab === 'positions' && }
{/* Right stats */}
{[ { label:'TVL', value:pool.tvl }, { label:'APR', value:pool.apr, highlight:true }, { label:'MY POOL', value:pool.myPool }, { label:'MY STAKE', value:pool.myStake }, ].map(s => (
{s.label}
{s.value}
))}
RESERVES
{pool.raw ? <>{fmt.units(pool.raw.reserve0, pool.raw.token0_decimals, 2)} {pool.pair[0]}
{fmt.units(pool.raw.reserve1, pool.raw.token1_decimals, 2)} {pool.pair[1]} : '—'}
); } function TokenAmountBox({ label, symbol, value, onChange, balance, decimals, onMax, tokens }) { return (
{label}
onChange(e.target.value.replace(/[^0-9.]/g, ''))} placeholder="0.00" style={{ background:'none', border:'none', outline:'none', fontFamily:'var(--font-m)', fontSize:22, fontWeight:700, color:'var(--ink)', width:140 }} />
{tokens ? : } {symbol}
); } const safeParse = (v, d) => { try { return v ? fmt.parse(v, d) : 0n; } catch { return 0n; } }; function DepositTab({ pool, accent, onTx }) { const chain = useChain(); const [a0, setA0] = useState(''); const [a1, setA1] = useState(''); const [mode, setMode] = useState('add'); const [lpAmt, setLpAmt] = useState(''); const raw = pool.raw; const t0 = chain.tokenByAddr[raw?.token0.toLowerCase()] || { address: raw?.token0, symbol: pool.pair[0], decimals: raw?.token0_decimals }; const t1 = chain.tokenByAddr[raw?.token1.toLowerCase()] || { address: raw?.token1, symbol: pool.pair[1], decimals: raw?.token1_decimals }; const weth = chain.book?.contracts.weth.toLowerCase(); const isEth = (t) => t.address?.toLowerCase() === weth; const { data: b0 } = useTokenBalance(isEth(t0) ? { ...t0, native: true } : t0); const { data: b1 } = useTokenBalance(isEth(t1) ? { ...t1, native: true } : t1); const lpBal = raw?.account_lp_balance ?? null; // Keep the two sides at the pool ratio: editing one side fills the other. function edit0(v) { setA0(v); if (raw && raw.reserve0 > 0n) { const x = safeParse(v, t0.decimals); setA1(x ? fmt.units(x * raw.reserve1 / raw.reserve0, t1.decimals, 6) : ''); } } function edit1(v) { setA1(v); if (raw && raw.reserve1 > 0n) { const x = safeParse(v, t1.decimals); setA0(x ? fmt.units(x * raw.reserve0 / raw.reserve1, t0.decimals, 6) : ''); } } const x0 = safeParse(a0, t0.decimals), x1 = safeParse(a1, t1.decimals), lp = safeParse(lpAmt, 18); const canAdd = x0 > 0n && x1 > 0n && chain.connected && (b0 == null || x0 <= b0) && (b1 == null || x1 <= b1); const canRemove = lp > 0n && chain.connected && (lpBal == null || lp <= lpBal); function add() { onTx('deposit', async (w, stage) => { const me = chain.account, router = chain.book.contracts.router; if (isEth(t0) || isEth(t1)) { const [tok, ta, eth] = isEth(t0) ? [t1, x1, x0] : [t0, x0, x1]; await ensureAllowance(w, me, tok.address, router, ta, stage); stage('sign'); return w.router.addLiquidityETH(tok.address, pool.stable, ta, withSlip(ta), withSlip(eth), me, DEADLINE(), { value: eth }); } await ensureAllowance(w, me, t0.address, router, x0, stage); await ensureAllowance(w, me, t1.address, router, x1, stage); stage('sign'); return w.router.addLiquidity(t0.address, t1.address, pool.stable, x0, x1, withSlip(x0), withSlip(x1), me, DEADLINE()); }); } function remove() { onTx('withdraw', async (w, stage) => { const me = chain.account, router = chain.book.contracts.router; const [q0, q1] = await w.router.quoteRemoveLiquidity(t0.address, t1.address, pool.stable, lp); await ensureAllowance(w, me, pool.address, router, lp, stage); stage('sign'); if (isEth(t0) || isEth(t1)) { const [tok, qt, qe] = isEth(t0) ? [t1, q1, q0] : [t0, q0, q1]; return w.router.removeLiquidityETH(tok.address, pool.stable, lp, withSlip(qt), withSlip(qe), me, DEADLINE()); } return w.router.removeLiquidity(t0.address, t1.address, pool.stable, lp, withSlip(q0), withSlip(q1), me, DEADLINE()); }); } return (
{[['add','ADD LIQUIDITY'],['remove','REMOVE']].map(([k,l]) => ( ))}
{mode === 'add' ? ( <> b0 != null && edit0(fmt.units(isEth(t0) && b0 > 10n**16n ? b0 - 10n**16n : b0, t0.decimals, 6))} /> b1 != null && edit1(fmt.units(isEth(t1) && b1 > 10n**16n ? b1 - 10n**16n : b1, t1.decimals, 6))} />
Amounts follow the pool ratio. You receive {pool.pair.join('/')} LP tokens; stake them under STAKING to earn emissions.
) : ( <> lpBal != null && setLpAmt(fmt.units(lpBal, 18, 18))} />
Only unstaked LP can be removed. Staked LP is withdrawn from the position first.
)}
); } function StakingTab({ pool, accent, onTx, onBoosted }) { const chain = useChain(); const [amt, setAmt] = useState(''); const lpBal = pool.raw?.account_lp_balance ?? null; const amount = safeParse(amt, 18); const hasGauge = pool.gauge && pool.gauge !== E.ZeroAddress; const canStake = hasGauge && amount > 0n && chain.connected && (lpBal == null || amount <= lpBal); const { data: exitFee } = useLoad(async (c) => hasGauge ? Number(await c.globals.getGaugeInitialExitFee(pool.gauge, false)) : null, [pool.gauge]); function stake() { onTx('stake', async (w, stage) => { await ensureAllowance(w, chain.account, pool.address, pool.gauge, amount, stage); stage('sign'); return w.at('VeGauge', pool.gauge).deposit(amount); }); } return (
lpBal != null && setAmt(fmt.units(lpBal, 18, 18))} /> {!hasGauge && (
This pool has no gauge yet, so LP cannot be staked here.
)} {hasGauge && exitFee != null && (
Staking mints a position NFT. Its exit fee starts at {(exitFee/100).toFixed(2)}% and decays over 52 weeks.
)}
); } function PositionsTab({ positions, loading, pool, accent, onTx }) { const chain = useChain(); const [withdrawAmt, setWithdrawAmt] = useState({}); if (!chain.connected) { return
CONNECT A WALLET TO SEE POSITIONS
; } if (positions.length === 0) { return (
{loading ? 'LOADING...' : <>NO ACTIVE POSITIONS
stake to earn rewards}
); } const lpPrice = pool.raw?.lpPrice ?? 0; const gauge = pool.gauge; const bribeTokens = chain.tokens.filter(t => !t.native).map(t => t.address); return (
{positions.map(pos => { const W = 52; const weeks = Math.min(W, Math.max(0, (Math.floor(Date.now()/1000) - pos.entry) / (7*86400))); const feePct = pos.feeFree ? 0 : pos.exitFeeRate / 100; const lockedPct = pos.latchedInitialFee / 100; const usd = fmt.toNum(pos.amount, 18) * lpPrice; const feeNow = usd * feePct / 100; const feeColor = feePct > 15 ? '#ef4444' : feePct > 8 ? '#ffd47c' : '#4ade80'; const votingLocked = pos.voted; const id = pos.tokenId.toString(); const wAmt = withdrawAmt[id] ?? ''; const wParsed = safeParse(wAmt, 18); const withdrawAll = !wAmt || wParsed >= pos.amount; const amountToWithdraw = withdrawAll ? pos.amount : wParsed; const blocked = pos.withdrawBlocker !== '0x00000000'; return (
NFT #{id} — {fmt.units(pos.amount, 18, 4)} LP
VALUE: {fmt.usd(usd)} · STAKED {new Date(pos.entry*1000).toISOString().slice(0,10)}
{votingLocked &&
🔒 VOTING — LOCKED
}
{/* exit fee decay */}
EXIT FEE · WEEK {weeks.toFixed(1)} / 52 {pos.feeFree ? 'fee-free gauge' : `rate locked at deposit: ${lockedPct.toFixed(2)}%`}
{[ { l:'EXIT TODAY', v:`${feePct.toFixed(2)}%`, s:`−${fmt.usd(feeNow)}`, c:feeColor }, { l:'NET IF WITHDRAWN NOW', v:`${fmt.units(pos.netIfWithdrawnNow, 18, 4)} LP`, s:fmt.usd(fmt.toNum(pos.netIfWithdrawnNow, 18) * lpPrice), c:'var(--ink)' }, ].map(m => (
{m.l}
{m.v}
{m.s}
))}
{/* claims */}
CLAIMS — ONE TX EACH
{[ { l:'LP EMISSIONS', v:`${fmt.units(pos.earned, 18, 4)} ARCH`, tx:'VeGauge.getReward(tokenId)', k:'claim-emissions', enabled: pos.earned > 0n, run: (w) => w.at('VeGauge', gauge).getReward(pos.tokenId) }, { l:'BRIBES + TRADING FEES', v:'for voted pools', tx:'Voter.claimFees(bribes, tokens)', k:'claim-bribes', enabled: true, run: async (w) => { const n = Number(await w.voter.poolVoteLength(pos.tokenId, gauge)); const pools = await Promise.all(Array.from({ length: n }, (_, i) => w.voter.poolVote(pos.tokenId, gauge, i))); if (!pools.length) throw new Error('This NFT has not voted, so it has no bribes to claim.'); const gauges = await Promise.all(pools.map(p => w.voter.gauges(p))); const bribes = []; for (const g of gauges) { bribes.push(await w.voter.external_bribes(g)); bribes.push(await w.voter.internal_bribes(g)); } return w.voter.claimFees(bribes, bribes.map(() => bribeTokens)); } }, ].map(c => (
{c.l}
{c.tx}
{c.v}
))}
{/* exit sequence */}
setWithdrawAmt(s => ({ ...s, [id]: e.target.value.replace(/[^0-9.]/g,'') }))} placeholder={`all (${fmt.units(pos.amount, 18, 4)})`} style={{ width:120, background:'rgba(232,230,225,0.05)', border:'1px solid rgba(232,230,225,0.1)', padding:'6px 8px', outline:'none', fontFamily:'var(--font-m)', fontSize:10, color:'var(--ink)' }} />
{votingLocked && (
Reset your votes before withdrawing. Full withdrawal burns the position NFT.
)} {!votingLocked && blocked && (
Withdrawal is currently blocked by the gauge (code {pos.withdrawBlocker}).
)}
); })}
); } /* ── VOTE SCREEN ─────────────────────────────────────────────────────── */ function useVoteData() { const chain = useChain(); const { data: pairData } = usePairs(); const gaugePools = (pairData?.pairs ?? []).filter(p => p.gauge !== E.ZeroAddress); const key = gaugePools.map(p => p.pair_address).join(); return useLoad(async (c) => { if (!pairData) return null; const { price, archPrice } = pairData; const bribeTokenAddrs = chain.tokens.filter(t => !t.native).map(t => t.address); const [totalWeight, voteDelay, epochStart] = await Promise.all([c.voter.totalWeight(), c.voter.VOTE_DELAY(), gaugePools.length ? c.at('Bribe', gaugePools[0].bribe).getNextEpochStart() : 0n]); const usdOf = (token, amt) => fmt.toNum(amt, chain.tokenByAddr[token.toLowerCase()]?.decimals ?? 18) * (price[token.toLowerCase()] ?? 0); const pools = await Promise.all(gaugePools.map(async (p) => { const [weight, ext, intb] = await Promise.all([c.voter.weights(p.pair_address), c.voter.external_bribes(p.gauge), c.voter.internal_bribes(p.gauge)]); let bribesUsd = 0, feesUsd = 0; for (const [addr, into] of [[ext, 'b'], [intb, 'f']]) { if (!addr || addr === E.ZeroAddress) continue; const b = c.at('Bribe', addr); const rows = await Promise.all(bribeTokenAddrs.map(t => b.rewardData(t, epochStart).catch(() => null))); rows.forEach((r, i) => { if (r) { const u = usdOf(bribeTokenAddrs[i], r.rewardsPerEpoch); if (into === 'b') bribesUsd += u; else feesUsd += u; } }); } return { ...toPool(p, price, archPrice), weight, ext, intb, bribesUsd, feesUsd }; })); const nfts = []; if (chain.account) { for (const p of gaugePools) { const g = c.at('VeGauge', p.gauge); const n = Number(await g.balanceOf(chain.account)); for (let i = 0; i < n; i++) { const id = await g.tokenOfOwnerByIndex(chain.account, i); const [vp, voted, last, amount] = await Promise.all([g.getVotingPowerView(id), g.voted(id), c.voter.lastVoted(id, p.gauge), g.getAmount(id)]); const n2 = Number(await c.voter.poolVoteLength(id, p.gauge)); const votedPools = await Promise.all(Array.from({ length: n2 }, (_, k) => c.voter.poolVote(id, p.gauge, k))); const votedWeights = await Promise.all(votedPools.map(vp2 => c.voter.votes(id, p.gauge, vp2))); nfts.push({ key: `${p.gauge}:${id}`, gauge: p.gauge, gaugeLabel: `${p.token0_symbol}/${p.token1_symbol}`, tokenId: id, vp, voted, lastVoted: Number(last), amount, current: Object.fromEntries(votedPools.map((vp2, k) => [vp2.toLowerCase(), votedWeights[k]])) }); } } } if (chain.account) { const [keys, deps] = await c.custody.getUserDeposits(chain.account); for (let i = 0; i < keys.length; i++) { const d = deps[i]; if (await c.custody.nftDelegated(keys[i])) continue; const p = gaugePools.find(gp => gp.gauge.toLowerCase() === d.underlyingVotingSource.toLowerCase()); if (!p) continue; const g = c.at('VeGauge', p.gauge); const id = d.tokenId; const [vp, voted, last] = await Promise.all([g.getVotingPowerView(id), g.voted(id), c.voter.lastVoted(id, p.gauge)]); const n2 = Number(await c.voter.poolVoteLength(id, p.gauge)); const votedPools = await Promise.all(Array.from({ length: n2 }, (_, k) => c.voter.poolVote(id, p.gauge, k))); const votedWeights = await Promise.all(votedPools.map(vp2 => c.voter.votes(id, p.gauge, vp2))); nfts.push({ key: `custody:${keys[i]}`, nftKey: keys[i], gauge: p.gauge, gaugeLabel: `${p.token0_symbol}/${p.token1_symbol} · IN CUSTODY`, tokenId: id, vp, voted, lastVoted: Number(last), amount: d.underlyingAmount, current: Object.fromEntries(votedPools.map((vp2, k) => [vp2.toLowerCase(), votedWeights[k]])) }); } } const totalVp = nfts.reduce((a, n) => a + n.vp, 0n); return { pools, nfts, totalWeight, voteDelay: Number(voteDelay), totalVp, totalBribes: pools.reduce((a, p) => a + p.bribesUsd, 0), totalFees: pools.reduce((a, p) => a + p.feesUsd, 0) }; }, [key, pairData?.archPrice]); } function VoteScreen({ accent, onTx }) { const isMobile = useMobile(); const chain = useChain(); const [infoVote, setInfoVote] = useState(null); const epoch = useEpoch(); const ep = useCountdown(epoch.endsAt); const { data, loading, error } = useVoteData(); const [votes, setVotes] = useState({}); const [search, setSearch] = useState(''); const [nftOpen, setNftOpen] = useState(false); const [selKey, setSelKey] = useState(null); const nfts = data?.nfts ?? []; const sel = nfts.find(n => n.key === selKey) ?? nfts[0] ?? null; const selVP = sel ? fmt.toNum(sel.vp, 18) : 0; const TOTAL_VP = selVP; useEffect(() => { if (!sel) return; const cur = {}; for (const [pool, w] of Object.entries(sel.current)) cur[pool] = fmt.toNum(w, 18).toFixed(0); setVotes(cur); }, [sel?.key]); const totalAlloc = Object.values(votes).reduce((a,v) => a+(parseFloat(v)||0), 0); const vpPct = TOTAL_VP > 0 ? Math.min((totalAlloc/TOTAL_VP)*100, 100) : 0; const anyVoted = totalAlloc > 0; const nowSec = Math.floor(Date.now()/1000); const cooldownLeft = sel ? Math.max(0, sel.lastVoted + (data?.voteDelay ?? 0) - nowSec) : 0; const canCast = anyVoted && sel && chain.connected && cooldownLeft === 0; function setVote(id, val) { setVotes(p => ({...p, [id]: val.replace(/[^0-9.]/g,'')})); } function setMax(id) { const used = Object.entries(votes).reduce((a,[k,v]) => k===id ? a : a+(parseFloat(v)||0), 0); setVote(id, Math.max(0, TOTAL_VP - used).toFixed(0)); } function cast() { const entries = Object.entries(votes).filter(([, v]) => parseFloat(v) > 0); const pools = entries.map(([p]) => p), weights = entries.map(([, v]) => fmt.parse(v, 18)); onTx('vote', (w) => sel.nftKey ? w.custody.selfVote(sel.nftKey, pools, weights) : w.voter.vote(sel.tokenId, sel.gauge, pools, weights)); } function resetVotes() { onTx('reset', (w) => sel.nftKey ? w.custody.selfResetVotes(sel.nftKey) : w.voter.reset(sel.tokenId, sel.gauge)); } const totalWeightNum = data ? fmt.toNum(data.totalWeight, 18) : 0; const weeklyEmissionUsd = data ? data.pools.reduce((a, p) => a + fmt.toNum(p.raw.emissions, 18), 0) : 0; const VOTE_POOLS = (data?.pools ?? []).map(p => { const w = fmt.toNum(p.weight, 18); const rewardsUsd = p.bribesUsd + p.feesUsd; const per1k = w > 0 ? rewardsUsd / w * 1000 : rewardsUsd > 0 ? Infinity : 0; const voteApr = w > 0 && data ? (rewardsUsd * 52) / (w * (data.pools[0]?.raw.lpPrice ?? 1)) * 100 : 0; return { id: p.address.toLowerCase(), address: p.address, pair: p.pair, name: p.pair.join('/'), sub: p.type, totalVotes: w, pct: totalWeightNum > 0 ? (w / totalWeightNum * 100) : 0, apr: voteApr, rewards: rewardsUsd, estimPer1k: per1k, bribesUsd: p.bribesUsd, feesUsd: p.feesUsd, gauge: p.gauge }; }).sort((a, b) => b.totalVotes - a.totalVotes); const filtered = VOTE_POOLS.filter(p => { const q = search.toLowerCase(); return p.name.toLowerCase().includes(q) || p.sub.toLowerCase().includes(q) || p.pair.some(t=>t.toLowerCase().includes(q)); }); const avgApr = VOTE_POOLS.length ? VOTE_POOLS.reduce((a, p) => a + (isFinite(p.apr) ? p.apr : 0), 0) / VOTE_POOLS.length : 0; return (
{/* Page header */}
GOVERNANCE
VOTE
{!isMobile && sel && sel.voted && }
{/* Stats bar */}
{[ { label:'YOUR VOTING POWER', value: data ? fmt.num(fmt.toNum(data.totalVp, 18), 2) : '—', hi:false, mono:false }, { label:'AVG VOTE APR', value: data ? fmt.pct(avgApr) : '—', hi:true, mono:false }, { label:`EPOCH ${epoch.number ?? '—'} ENDS`, value: ep.ready ? `${ep.d}d ${String(ep.h).padStart(2,'0')}h ${String(ep.m).padStart(2,'0')}m ${String(ep.s).padStart(2,'0')}s` : '—', hi:false, mono:true }, { label:'BRIBES NEXT EPOCH', value: data ? fmt.usd(data.totalBribes) : '—', hi:false, mono:false }, { label:'FEES NEXT EPOCH', value: data ? fmt.usd(data.totalFees) : '—', hi:false, mono:false }, ].map((st,i) => (
0) ? '1px solid rgba(232,230,225,0.07)' : 'none', borderTop: (isMobile && i>=2) ? '1px solid rgba(232,230,225,0.07)' : 'none', }}>
{st.label}
{st.value}
))}
{/* Main vote card */} {/* NFT Dropdown */} {nftOpen && (
setNftOpen(false)} style={{ position:'fixed', inset:0, zIndex:300 }}>
e.stopPropagation()} style={{ position:'absolute', top:58, right:20, background:'#0a0807', border:`1px solid ${accent}55`, padding:'8px 0', minWidth:220, zIndex:301, }}> {nfts.map(n => ( ))} {nfts.length === 0 &&
No staked positions in your wallet
}
)} {/* Toolbar */}
setSearch(e.target.value)} placeholder="Search..." style={{ background:'none', border:'none', outline:'none', fontFamily:'var(--font-m)', fontSize:12, color:'var(--ink)', width:'100%', letterSpacing:'0.06em' }} />
{/* Table header */}
{(isMobile ? ['POOL','APR','YOUR VOTE'] : ['POOL','↓ TOTAL VOTES','APR','REWARDS','ESTIMATION','YOUR VOTES','']).map((h,i) => (
0 ? 'center' : 'left', }}>{h}
))}
{/* Vote rows */} {filtered.map((pool,i) => ( setVote(pool.id, v)} onMax={() => setMax(pool.id)} totalVP={TOTAL_VP} onInfo={() => setInfoVote(pool)} /> ))} {filtered.length === 0 && (
{loading && !data ? 'LOADING GAUGES...' : error ? `COULD NOT LOAD — ${error.toUpperCase()}` : 'NO GAUGES FOUND'}
)} {/* Bottom: VP bar + cast */}
{/* VP bar */}
VotingPower used: {vpPct.toFixed(2)}% Voted this epoch: {sel?.voted ? 'Yes' : 'No'}
{fmt.num(totalAlloc, 0)} / {fmt.num(TOTAL_VP, 0)} VP
Weights are relative: the numbers you enter are split proportionally across your NFT's full voting power. Casting again replaces the previous vote.
{/* Mobile info sheet */} {infoVote && ( setInfoVote(null)} stats={[ { label:'TOTAL VOTES', value: fmt.num(infoVote.totalVotes, 0), sub: infoVote.pct.toFixed(1)+'%' }, { label:'APR', value: fmt.pct(infoVote.apr), hi:true, color:'#4ade80' }, { label:'REWARDS', value: fmt.usd(infoVote.rewards) }, { label:'EST / 1K', value: isFinite(infoVote.estimPer1k) ? fmt.usd(infoVote.estimPer1k) : '∞', sub:'per 1K votes' }, { label:'BRIBES', value: fmt.usd(infoVote.bribesUsd), mono:true }, { label:'FEES', value: fmt.usd(infoVote.feesUsd), mono:true }, ]} /> )}
); } function VoteRow({ pool, accent, odd, voteVal, onVoteChange, onMax, totalVP, onInfo }) { const [hov, setHov] = useState(false); const isMobile = useMobile(); const myVote = parseFloat(voteVal) || 0; const myPct = myVote && totalVP ? (myVote/totalVP*100).toFixed(1)+'%' : '—'; const voted = myVote > 0; return (
setHov(true)} onMouseLeave={()=>setHov(false)} style={{ display:'grid', gridTemplateColumns: isMobile ? '1fr 60px 110px' : '1fr 110px 70px 80px 120px 100px 130px', padding: isMobile ? '11px 14px' : '13px 18px', alignItems:'center', transition:'background 0.14s', background: hov ? 'rgba(232,230,225,0.04)' : odd ? 'rgba(232,230,225,0.016)' : 'transparent', borderBottom:'1px solid rgba(232,230,225,0.04)', }}> {/* Pool */}
{pool.name}
{pool.sub}
{isMobile && ( )}
{!isMobile &&
{fmt.num(pool.totalVotes, 0)}
{pool.pct.toFixed(1)}%
}
{fmt.pct(pool.apr)}
{!isMobile &&
{fmt.usd(pool.rewards)}
} {!isMobile &&
{isFinite(pool.estimPer1k) ? fmt.usd(pool.estimPer1k) : '∞'}
PER 1K VOTES
} {!isMobile &&
{voted ? fmt.num(myVote, 0) : '—'}
{myPct}
}
onVoteChange(e.target.value)} placeholder="0" inputMode="decimal" style={{ flex:1, background:'rgba(232,230,225,0.05)', border:`1px solid ${voted ? accent+'88' : 'rgba(232,230,225,0.1)'}`, padding:'6px 8px', outline:'none', fontFamily:'var(--font-m)', fontSize:12, color:'var(--ink)', width:0, minWidth:0, transition:'border-color 0.15s', }} />
); } Object.assign(window, { TokenIcon, TokenPair, Nav, SwapScreen, LiquidityScreen, ManageScreen, VoteScreen, Card, useMobile, useCountdown, toPool, TokenAmountBox, safeParse, DEADLINE, withSlip }); /* ── MOBILE INFO SHEET ───────────────────────────────────────────────── */ function MobileInfoSheet({ title, tokens, stats, onClose, accent }) { return (
e.stopPropagation()} style={{ position:'relative', width:'100%', background:'rgba(8,6,5,0.99)', borderTop:`2px solid ${accent}`, padding:'20px 18px 36px', animation:'fadeUp 0.2s forwards', }}> {/* accent line */}
{/* Header */}
{title}
{/* Stats grid */}
{stats.map((s, i) => (
{s.label}
{s.value}
{s.sub &&
{s.sub}
}
))}
); } Object.assign(window, { MobileInfoSheet });