Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions auto-qa/tests/twap-chain-dispatch.test.mjs
Original file line number Diff line number Diff line change
@@ -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\]\)/);
});
34 changes: 28 additions & 6 deletions src/components/futarchyFi/marketPage/MarketPageShowcase.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -500,6 +507,7 @@ const Spinner = () => (

// TWAP Countdown Component
const TwapCountdown = ({
chainId = 100,
twapStartTimestamp,
twapDurationHours = 24,
twapDescription = DEFAULT_TWAP_DESCRIPTION,
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -602,7 +623,7 @@ const TwapCountdown = ({

const normalizedPrice = useInversion ? 1 / rawPrice : rawPrice;
return normalizedPrice;
}, [ensureProvider]);
}, [chainId, ensureProvider]);

useEffect(() => {
const calculateTimeRemaining = () => {
Expand Down Expand Up @@ -5105,6 +5126,7 @@ const MarketPageShowcase = ({ hidden = false, debugMode = false, proposal = null
{/* TWAP Countdown Widget - always visible */}
{!marketData.isLoading && config?.marketInfo?.twapStartTimestamp && (
<TwapCountdown
chainId={config.chainId || 100}
twapStartTimestamp={config.marketInfo.twapStartTimestamp}
twapDurationHours={config.marketInfo.twapDurationHours || 24}
twapDescription={config.marketInfo.twapDescription || DEFAULT_TWAP_DESCRIPTION}
Expand Down
Loading