diff --git a/auto-qa/tests/twap-chain-dispatch.test.mjs b/auto-qa/tests/twap-chain-dispatch.test.mjs new file mode 100644 index 00000000..9496f498 --- /dev/null +++ b/auto-qa/tests/twap-chain-dispatch.test.mjs @@ -0,0 +1,25 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; + +const SOURCE = readFileSync( + new URL('../../src/components/futarchyFi/marketPage/MarketPageShowcase.jsx', import.meta.url), + 'utf8', +); + +test('TWAP dispatch uses the market chain for its RPC and pool ABI', () => { + assert.match(SOURCE, /1:\s*process\.env\.NEXT_PUBLIC_ETHEREUM_RPC\s*\|\|\s*'https:\/\/ethereum-rpc\.publicnode\.com'/); + assert.match(SOURCE, /100:\s*process\.env\.NEXT_PUBLIC_GNOSIS_RPC\s*\|\|\s*'https:\/\/rpc\.gnosischain\.com'/); + assert.match(SOURCE, /chainId=\{config\.chainId\s*\|\|\s*100\}/); + assert.match(SOURCE, /numericChainId === 1\s*\?\s*UNISWAP_V3_TWAP_ABI\s*:\s*ALGEBRA_TWAP_ABI/); + assert.match(SOURCE, /numericChainId === 1\s*\?\s*await poolContract\.observe\(\[aStart, aEnd\]\)\s*:\s*await poolContract\.getTimepoints\(\[aStart, aEnd\]\)/); +}); + +test('unsupported chains fail closed instead of reading Gnosis', () => { + assert.match(SOURCE, /const rpcUrl = TWAP_RPC_BY_CHAIN\[Number\(chainId\)\]/); + assert.match(SOURCE, /if \(!rpcUrl\) \{\s*throw new Error\(`TWAP is not configured for chain \$\{chainId\}`\)/); +}); + +test('changing chains clears both provider and token-order caches', () => { + assert.match(SOURCE, /providerRef\.current = null;\s*poolToken0CacheRef\.current = \{\};\s*\}, \[chainId\]\)/); +}); diff --git a/src/components/futarchyFi/marketPage/MarketPageShowcase.jsx b/src/components/futarchyFi/marketPage/MarketPageShowcase.jsx index 34b31540..e343db8f 100644 --- a/src/components/futarchyFi/marketPage/MarketPageShowcase.jsx +++ b/src/components/futarchyFi/marketPage/MarketPageShowcase.jsx @@ -65,11 +65,18 @@ import { UNISWAP_V3_POOL_ABI } from "./constants/contracts"; // Subgraph pool fetcher instance for latest prices const subgraphPoolFetcher = createSubgraphPoolFetcher(); -const GNOSIS_DEFAULT_RPC = process.env.NEXT_PUBLIC_GNOSIS_RPC || 'https://rpc.gnosischain.com'; +const TWAP_RPC_BY_CHAIN = { + 1: process.env.NEXT_PUBLIC_ETHEREUM_RPC || 'https://ethereum-rpc.publicnode.com', + 100: process.env.NEXT_PUBLIC_GNOSIS_RPC || 'https://rpc.gnosischain.com' +}; const ALGEBRA_TWAP_ABI = [ "function getTimepoints(uint32[] secondsAgos) external view returns (int56[] tickCumulatives, uint160[] secondsPerLiquidityCumulatives, uint112[] volatilityCumulatives, uint256[] volumePerAvgLiquiditys)", "function token0() external view returns (address)" ]; +const UNISWAP_V3_TWAP_ABI = [ + "function observe(uint32[] secondsAgos) external view returns (int56[] tickCumulatives, uint160[] secondsPerLiquidityCumulativeX128s)", + "function token0() external view returns (address)" +]; const DEFAULT_TWAP_DESCRIPTION = "The Futarchy Test is considered passed if the time-weighted average price (TWAP) of the \u201cpass\u201d (yes) outcome over the final 24 hours of the Issuance KIP\u2019s voting period is greater than or equal to that of the \u201cfail\u201d (no) outcome. If not, the proposal fails the futarchy test, regardless of the Kleros DAO vote result."; const TWAP_REFRESH_INTERVAL_MS = 30000; // refresh every 30 seconds while active @@ -500,6 +507,7 @@ const Spinner = () => ( // TWAP Countdown Component const TwapCountdown = ({ + chainId = 100, twapStartTimestamp, twapDurationHours = 24, twapDescription = DEFAULT_TWAP_DESCRIPTION, @@ -529,10 +537,19 @@ const TwapCountdown = ({ const ensureProvider = useCallback(() => { if (!providerRef.current) { - providerRef.current = new ethers.providers.JsonRpcProvider(GNOSIS_DEFAULT_RPC); + const rpcUrl = TWAP_RPC_BY_CHAIN[Number(chainId)]; + if (!rpcUrl) { + throw new Error(`TWAP is not configured for chain ${chainId}`); + } + providerRef.current = new ethers.providers.JsonRpcProvider(rpcUrl); } return providerRef.current; - }, []); + }, [chainId]); + + useEffect(() => { + providerRef.current = null; + poolToken0CacheRef.current = {}; + }, [chainId]); const fetchPoolTwap = useCallback(async (poolConfig, secondsAgoStart, shouldInvert = null, secondsAgoEnd = 0, companyTokenAddress = null) => { if (!poolConfig?.address) { @@ -542,8 +559,12 @@ const TwapCountdown = ({ const aEnd = Math.max(0, Math.floor(secondsAgoEnd)); const secondsWindow = Math.max(1, aStart - aEnd); const provider = ensureProvider(); - const poolContract = new ethers.Contract(poolConfig.address, ALGEBRA_TWAP_ABI, provider); - const { tickCumulatives } = await poolContract.getTimepoints([aStart, aEnd]); + const numericChainId = Number(chainId); + const poolAbi = numericChainId === 1 ? UNISWAP_V3_TWAP_ABI : ALGEBRA_TWAP_ABI; + const poolContract = new ethers.Contract(poolConfig.address, poolAbi, provider); + const { tickCumulatives } = numericChainId === 1 + ? await poolContract.observe([aStart, aEnd]) + : await poolContract.getTimepoints([aStart, aEnd]); const oldest = BigInt(tickCumulatives[0].toString()); const latest = BigInt(tickCumulatives[1].toString()); const tickDelta = latest - oldest; @@ -602,7 +623,7 @@ const TwapCountdown = ({ const normalizedPrice = useInversion ? 1 / rawPrice : rawPrice; return normalizedPrice; - }, [ensureProvider]); + }, [chainId, ensureProvider]); useEffect(() => { const calculateTimeRemaining = () => { @@ -5105,6 +5126,7 @@ const MarketPageShowcase = ({ hidden = false, debugMode = false, proposal = null {/* TWAP Countdown Widget - always visible */} {!marketData.isLoading && config?.marketInfo?.twapStartTimestamp && (