From 4467d41edfd063b4341514674d37a160bdaeb845 Mon Sep 17 00:00:00 2001 From: johba Date: Wed, 24 Sep 2025 14:02:28 +0200 Subject: [PATCH] fix web-app --- scripts/local_env.sh | 12 +- services/ponder/ponder.config.ts | 2 + services/txnBot/service.js | 184 +++++++++++++++++++++++++++++-- web-app/src/config.ts | 18 +-- 4 files changed, 195 insertions(+), 21 deletions(-) diff --git a/scripts/local_env.sh b/scripts/local_env.sh index bb957df..46f4f26 100755 --- a/scripts/local_env.sh +++ b/scripts/local_env.sh @@ -90,7 +90,7 @@ log() { wait_for_rpc() { local url="$1" for _ in {1..60}; do - if curl -s -o /dev/null -X POST "$url" -H "Content-Type: application/json" \ + if curl --max-time 1 --connect-timeout 1 -s -o /dev/null -X POST "$url" -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'; then return 0 fi @@ -103,7 +103,7 @@ wait_for_rpc() { wait_for_http() { local url="$1" for _ in {1..60}; do - if curl -sSf "$url" >/dev/null 2>&1; then + if curl --max-time 1 --connect-timeout 1 -sSf "$url" >/dev/null 2>&1; then return 0 fi sleep 1 @@ -186,7 +186,8 @@ start_anvil() { log "Starting Anvil (forking $FORK_URL)" local anvil_args=("--fork-url" "$FORK_URL" "--chain-id" 31337 "--block-time" 1 \ - "--host" 127.0.0.1 "--port" 8545) + "--host" 127.0.0.1 "--port" 8545 "--threads" 4 "--timeout" 2000 "--retries" 2 \ + "--fork-retry-backoff" 100) if [[ -f "$MNEMONIC_FILE" ]]; then local mnemonic @@ -305,7 +306,7 @@ prepare_application_state() { prime_chain_for_indexing() { log "Pre-mining blocks for indexer stability" - for _ in {1..1200}; do + for _ in {1..2000}; do "$CAST" rpc --rpc-url "$ANVIL_RPC" evm_mine > /dev/null 2>&1 || true done } @@ -393,9 +394,12 @@ start_frontend() { "VITE_LOCAL_RPC_PROXY_TARGET=$ANVIL_RPC" "VITE_KRAIKEN_ADDRESS=$KRAIKEN" "VITE_STAKE_ADDRESS=$STAKE" + "VITE_LIQUIDITY_MANAGER=$LIQUIDITY_MANAGER" "VITE_SWAP_ROUTER=$SWAP_ROUTER" "VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK=$GRAPHQL_ENDPOINT" "VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK=$LOCAL_TXNBOT_URL" + "VITE_ENABLE_EVENT_STREAM=false" + "VITE_POSITIONS_POLL_MS=0" ) log "Starting frontend (Vite dev server)" diff --git a/services/ponder/ponder.config.ts b/services/ponder/ponder.config.ts index e73f22f..42385af 100644 --- a/services/ponder/ponder.config.ts +++ b/services/ponder/ponder.config.ts @@ -8,6 +8,7 @@ const networks = { BASE_SEPOLIA_LOCAL_FORK: { chainId: 31337, rpc: process.env.PONDER_RPC_URL_BASE_SEPOLIA_LOCAL_FORK || "http://127.0.0.1:8545", + disableCache: true, contracts: { kraiken: process.env.KRAIKEN_ADDRESS || "0x56186c1E64cA8043dEF78d06AfF222212eA5df71", stake: process.env.STAKE_ADDRESS || "0x056E4a859558A3975761ABd7385506BC4D8A8E60", @@ -56,6 +57,7 @@ export default createConfig({ [NETWORK]: { id: selectedNetwork.chainId, rpc: selectedNetwork.rpc, + disableCache: selectedNetwork.disableCache, }, }, contracts: { diff --git a/services/txnBot/service.js b/services/txnBot/service.js index 5325c69..ec66a14 100644 --- a/services/txnBot/service.js +++ b/services/txnBot/service.js @@ -57,11 +57,25 @@ const provider = new ethers.JsonRpcProvider(PROVIDER_URL); const wallet = new ethers.Wallet(PRIVATE_KEY, provider); const liquidityManager = new ethers.Contract(LM_CONTRACT_ADDRESS, LM_ABI, wallet); const stakeContract = new ethers.Contract(STAKE_CONTRACT_ADDRESS, STAKE_ABI, wallet); +const walletAddress = ethers.getAddress(wallet.address); +const ZERO_ADDRESS = ethers.ZeroAddress; +const DEFAULT_RECENTER_ACCESS_SLOT = 6n; +const STORAGE_SLOT_SEARCH_LIMIT = 64n; + +let recenterAccessSlot = null; +let slotDetectionAttempted = false; let startTime = new Date(); let lastRecenterTime = null; let lastLiquidationTime = null; let lastRecenterTx = null; +let lastRecenterAccessStatus = null; +let lastRecenterEligibility = { + checkedAtMs: null, + canRecenter: null, + reason: null, + error: null, +}; async function fetchActivePositions() { const response = await fetch(GRAPHQL_ENDPOINT, { @@ -88,21 +102,156 @@ async function checkFunds() { return ethers.formatEther(balance); } -async function canCallFunction() { +function extractRevertReason(error) { + const candidates = [ + error?.shortMessage, + error?.reason, + error?.info?.error?.message, + error?.error?.message, + error?.message, + ]; + + for (const candidate of candidates) { + if (typeof candidate !== 'string' || candidate.length === 0) { + continue; + } + const match = candidate.match(/execution reverted(?: due to)?(?::|: )?(.*)/i); + if (match && match[1]) { + return match[1].trim(); + } + if (!candidate.toLowerCase().startsWith('execution reverted')) { + return candidate.trim(); + } + } + return null; +} + +function parseStorageAddress(value) { + if (typeof value !== 'string' || value.length <= 2) { + return ZERO_ADDRESS; + } try { - // this will throw if the function is not callable - await liquidityManager.recenter.estimateGas(); - return true; + return ethers.getAddress(ethers.dataSlice(value, 12, 32)); } catch (error) { - return false; + return ZERO_ADDRESS; } } +async function detectRecenterAccessSlot() { + if (recenterAccessSlot !== null) { + return recenterAccessSlot; + } + if (slotDetectionAttempted) { + return DEFAULT_RECENTER_ACCESS_SLOT; + } + slotDetectionAttempted = true; + + try { + const feeDestinationAddress = await liquidityManager.feeDestination(); + if (feeDestinationAddress !== ZERO_ADDRESS) { + for (let slot = 0n; slot < STORAGE_SLOT_SEARCH_LIMIT; slot++) { + const raw = await provider.getStorage(LM_CONTRACT_ADDRESS, slot); + if (parseStorageAddress(raw) === feeDestinationAddress) { + recenterAccessSlot = slot > 0n ? slot - 1n : DEFAULT_RECENTER_ACCESS_SLOT; + break; + } + } + } + } catch (error) { + console.error('Failed to detect recenter access slot:', error); + } + + if (recenterAccessSlot === null) { + recenterAccessSlot = DEFAULT_RECENTER_ACCESS_SLOT; + } + + return recenterAccessSlot; +} + +async function getRecenterAccessStatus(forceRefresh = false) { + const now = Date.now(); + if (!forceRefresh && lastRecenterAccessStatus && lastRecenterAccessStatus.checkedAtMs && (now - lastRecenterAccessStatus.checkedAtMs) < 30000) { + return lastRecenterAccessStatus; + } + + let recenterAddress = null; + let hasAccess = null; + let slotHex = null; + let errorMessage = null; + + try { + const slot = await detectRecenterAccessSlot(); + slotHex = ethers.toBeHex(slot); + const raw = await provider.getStorage(LM_CONTRACT_ADDRESS, slot); + recenterAddress = parseStorageAddress(raw); + hasAccess = recenterAddress === ZERO_ADDRESS || recenterAddress === walletAddress; + } catch (error) { + errorMessage = error?.shortMessage || error?.message || 'unknown error'; + recenterAddress = null; + } + + lastRecenterAccessStatus = { + hasAccess, + recenterAccessAddress: recenterAddress, + slot: slotHex, + checkedAtMs: now, + error: errorMessage, + }; + + return lastRecenterAccessStatus; +} + +async function evaluateRecenterOpportunity() { + const now = Date.now(); + const accessStatus = await getRecenterAccessStatus(true); + + if (accessStatus.error && accessStatus.hasAccess === null) { + lastRecenterEligibility = { + checkedAtMs: now, + canRecenter: false, + reason: 'Failed to determine recenter access.', + error: accessStatus.error, + }; + return lastRecenterEligibility; + } + + if (accessStatus.hasAccess === false) { + lastRecenterEligibility = { + checkedAtMs: now, + canRecenter: false, + reason: 'txnBot is not the authorized recenter caller.', + error: null, + }; + return lastRecenterEligibility; + } + + try { + await liquidityManager.recenter.estimateGas(); + lastRecenterEligibility = { + checkedAtMs: now, + canRecenter: true, + reason: null, + error: null, + }; + } catch (error) { + lastRecenterEligibility = { + checkedAtMs: now, + canRecenter: false, + reason: extractRevertReason(error) || 'recenter not currently executable.', + error: error?.shortMessage || error?.message || null, + }; + } + + return lastRecenterEligibility; +} + async function attemptRecenter() { - if (!(await canCallFunction())) { + const eligibility = await evaluateRecenterOpportunity(); + if (!eligibility.canRecenter) { return { executed: false, - message: 'Liquidity manager denied recenter (likely already centered).' + message: eligibility.reason || 'Liquidity manager denied recenter.', + eligibility, }; } @@ -113,7 +262,8 @@ async function attemptRecenter() { return { executed: true, txHash: tx.hash, - message: 'recenter transaction submitted' + message: 'recenter transaction submitted', + eligibility: lastRecenterEligibility, }; } @@ -151,7 +301,8 @@ async function liquidityLoop() { if (result.executed) { console.log(`recenter called successfully. tx=${result.txHash}`); } else { - console.log(`No liquidity can be moved at the moment. - ${(new Date()).toISOString()}`); + const reason = result.message ? `Reason: ${result.message}` : 'Reason: unavailable'; + console.log(`Recenter skipped. ${reason} - ${(new Date()).toISOString()}`); } } catch (error) { console.error('Error in liquidity loop:', error); @@ -205,12 +356,27 @@ app.get('/status', async (req, res) => { try { const balance = await checkFunds(); const uptime = formatDuration(new Date() - startTime); + const recenterAccessStatus = await getRecenterAccessStatus(); + const recenterEligibility = lastRecenterEligibility; const status = { balance: `${balance} ETH`, uptime: uptime, lastRecenterTime: lastRecenterTime ? lastRecenterTime.toISOString() : 'Never', lastLiquidationTime: lastLiquidationTime ? lastLiquidationTime.toISOString() : 'Never', lastRecenterTx, + recenterAccess: { + hasAccess: recenterAccessStatus?.hasAccess ?? null, + grantedTo: recenterAccessStatus?.recenterAccessAddress ?? null, + slot: recenterAccessStatus?.slot ?? null, + checkedAt: recenterAccessStatus?.checkedAtMs ? new Date(recenterAccessStatus.checkedAtMs).toISOString() : null, + error: recenterAccessStatus?.error ?? null, + }, + recenterEligibility: { + canRecenter: recenterEligibility?.canRecenter ?? null, + reason: recenterEligibility?.reason ?? null, + checkedAt: recenterEligibility?.checkedAtMs ? new Date(recenterEligibility.checkedAtMs).toISOString() : null, + error: recenterEligibility?.error ?? null, + }, }; if (parseFloat(balance) < 0.1) { diff --git a/web-app/src/config.ts b/web-app/src/config.ts index e710453..85a3d39 100644 --- a/web-app/src/config.ts +++ b/web-app/src/config.ts @@ -1,16 +1,18 @@ import deploymentsLocal from "../../onchain/deployments-local.json"; -const LOCAL_PONDER_URL = import.meta.env.VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK ?? "http://127.0.0.1:42069/graphql"; -const LOCAL_TXNBOT_URL = import.meta.env.VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK ?? "http://127.0.0.1:43069"; -const LOCAL_RPC_URL = import.meta.env.VITE_LOCAL_RPC_URL ?? "/rpc/anvil"; +const env = import.meta.env; + +const LOCAL_PONDER_URL = env.VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK ?? "http://127.0.0.1:42069/graphql"; +const LOCAL_TXNBOT_URL = env.VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK ?? "http://127.0.0.1:43069"; +const LOCAL_RPC_URL = env.VITE_LOCAL_RPC_URL ?? "/rpc/anvil"; const localContracts = (deploymentsLocal as any)?.contracts ?? {}; const localInfra = (deploymentsLocal as any)?.infrastructure ?? {}; -const LOCAL_KRAIKEN = (localContracts.Kraiken ?? "").trim(); -const LOCAL_STAKE = (localContracts.Stake ?? "").trim(); -const LOCAL_LM = (localContracts.LiquidityManager ?? "").trim(); -const LOCAL_WETH = (localInfra.weth ?? "0x4200000000000000000000000000000000000006").trim(); -const LOCAL_ROUTER = "0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4"; +const LOCAL_KRAIKEN = (env.VITE_KRAIKEN_ADDRESS ?? localContracts.Kraiken ?? "").trim(); +const LOCAL_STAKE = (env.VITE_STAKE_ADDRESS ?? localContracts.Stake ?? "").trim(); +const LOCAL_LM = (env.VITE_LIQUIDITY_MANAGER ?? localContracts.LiquidityManager ?? "").trim(); +const LOCAL_WETH = (env.VITE_LOCAL_WETH ?? localInfra.weth ?? "0x4200000000000000000000000000000000000006").trim(); +const LOCAL_ROUTER = (env.VITE_SWAP_ROUTER ?? "0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4").trim(); function detectDefaultChainId(): number { const envValue = import.meta.env.VITE_DEFAULT_CHAIN_ID;