From ab63faafb78285a599cb823a91a4621bd2c3e6dc Mon Sep 17 00:00:00 2001 From: fr1j0 Date: Fri, 8 May 2026 13:10:42 -0400 Subject: [PATCH] Use websocket for morning soil refresh --- src/state/protocol/field/field.updater.ts | 81 ++++++++++++++++++----- src/utils/wagmi/chains.ts | 21 ++++++ 2 files changed, 86 insertions(+), 16 deletions(-) diff --git a/src/state/protocol/field/field.updater.ts b/src/state/protocol/field/field.updater.ts index 5a5897f43..6a5f6ef81 100644 --- a/src/state/protocol/field/field.updater.ts +++ b/src/state/protocol/field/field.updater.ts @@ -6,13 +6,16 @@ import { FieldIssuedSoilDocument } from "@/generated/gql/pintostalk/graphql"; import { useProtocolAddress } from "@/hooks/pinto/useProtocolAddress"; import useUpdateQueryKeys from "@/state/query/useUpdateQueryKeys"; import { useInvalidateField } from "@/state/useFieldData"; +import { usePriceQuery } from "@/state/usePriceData"; import { useSeason } from "@/state/useSunData"; import { exists } from "@/utils/utils"; +import { getChainWebSocketRpcUrl, getChainWithChainId } from "@/utils/wagmi/chains"; import { useQuery } from "@tanstack/react-query"; import request from "graphql-request"; import { useAtomValue, useSetAtom } from "jotai"; -import { useCallback, useEffect } from "react"; -import { useChainId, useReadContract, useReadContracts, useWatchContractEvent } from "wagmi"; +import { useEffect, useState } from "react"; +import { createPublicClient, webSocket } from "viem"; +import { useChainId, useReadContract, useReadContracts } from "wagmi"; import { morningAtom } from "../sun/sun.atoms"; import { fieldInitialSoilAtom, @@ -25,6 +28,8 @@ import { } from "./field.atoms"; const INTERVAL = 1000 * 60 * 20; // 20 minutes, in milliseconds +const MORNING_SOIL_REFRESH_INTERVAL = 1000 * 10; // 10 seconds, in milliseconds +const VALUE_TARGET = 1; const settings = { query: { @@ -248,30 +253,74 @@ export const useUpdateField = () => { // ---------------------------------------- Non Top level updater hooks ---------------------------------------- /** - * Update the soil every 10 seconds + * Keep morning soil fresh while below peg. */ export const useUpdateMorningSoilOnInterval = () => { const diamond = useProtocolAddress(); - + const chainId = useChainId(); const morning = useAtomValue(morningAtom); const soil = useAtomValue(fieldTotalSoilAtom).totalSoil; const invalidateField = useInvalidateField(); const devMode = useAtomValue(morningFieldDevModeAtom); + const priceQuery = usePriceQuery(); + const [usePollingFallback, setUsePollingFallback] = useState(false); const isMorning = morning.isMorning; - const noSoil = soil.lte(0); + const hasSoil = soil.gt(0); + const isBelowPeg = + exists(priceQuery.data) && TV.fromBlockchain(priceQuery.data.price, SOIL_DECIMALS).lt(VALUE_TARGET); + const shouldRefreshMorningSoil = isMorning && isBelowPeg && hasSoil && !devMode.freeze; + const webSocketRpcUrl = getChainWebSocketRpcUrl(chainId); - const handleInvalidateSoil = useCallback(() => { - invalidateField("soil"); - }, [invalidateField]); + useEffect(() => { + setUsePollingFallback(false); - // Watch for Sow Events & invalidate the soil query when they occur - useWatchContractEvent({ - address: diamond, - abi: diamondABI, - eventName: "Sow", - onLogs: handleInvalidateSoil, - enabled: isMorning && !noSoil && !devMode.freeze, - }); + if (!shouldRefreshMorningSoil || !webSocketRpcUrl) return; + + const chain = getChainWithChainId(chainId); + if (!chain) return; + + const client = createPublicClient({ + chain, + transport: webSocket(webSocketRpcUrl), + }); + + const unwatch = client.watchContractEvent({ + address: diamond, + abi: diamondABI, + eventName: "Sow", + onLogs: () => invalidateField("soil"), + onError: (error) => { + console.warn("[protocol/field/useUpdateMorningSoilOnInterval]: Sow event websocket failed", error); + setUsePollingFallback(true); + }, + }); + + return () => unwatch(); + }, [chainId, diamond, invalidateField, shouldRefreshMorningSoil, webSocketRpcUrl]); + + useEffect(() => { + if (!shouldRefreshMorningSoil || (webSocketRpcUrl && !usePollingFallback)) return; + + const refreshSoil = () => { + if (typeof document !== "undefined" && document.visibilityState === "hidden") return; + invalidateField("soil"); + }; + + const intervalId = window.setInterval(refreshSoil, MORNING_SOIL_REFRESH_INTERVAL); + + const handleVisibilityChange = () => { + if (document.visibilityState === "visible") { + refreshSoil(); + } + }; + + document.addEventListener("visibilitychange", handleVisibilityChange); + + return () => { + window.clearInterval(intervalId); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, [invalidateField, shouldRefreshMorningSoil, usePollingFallback, webSocketRpcUrl]); }; diff --git a/src/utils/wagmi/chains.ts b/src/utils/wagmi/chains.ts index 2cd0475b4..d8c379039 100644 --- a/src/utils/wagmi/chains.ts +++ b/src/utils/wagmi/chains.ts @@ -5,16 +5,22 @@ import { arbitrum as viem__arbitrum } from "viem/chains"; const API_KEY = import.meta.env.VITE_ALCHEMY_API_KEY; const ARB_ALCHEMY_RPC_URL = `https://arb-mainnet.g.alchemy.com/v2/${API_KEY}`; +const ARB_ALCHEMY_WS_URL = `wss://arb-mainnet.g.alchemy.com/v2/${API_KEY}`; const BASE_ALCHEMY_RPC_URL = `https://base-mainnet.g.alchemy.com/v2/${API_KEY}`; +const BASE_ALCHEMY_WS_URL = `wss://base-mainnet.g.alchemy.com/v2/${API_KEY}`; export const LOCAL_RPC_URL = "http://127.0.0.1:8545"; +export const LOCAL_WS_RPC_URL = "ws://127.0.0.1:8545"; export const BASE_RPC_URL = API_KEY ? BASE_ALCHEMY_RPC_URL : viem__base.rpcUrls.default.http[0]; +export const BASE_WS_RPC_URL = API_KEY ? BASE_ALCHEMY_WS_URL : undefined; export const ARB_RPC_URL = API_KEY ? ARB_ALCHEMY_RPC_URL : viem__arbitrum.rpcUrls.default.http[0]; +export const ARB_WS_RPC_URL = API_KEY ? ARB_ALCHEMY_WS_URL : undefined; export const TENDERLY_RPC_URL = import.meta.env.VITE_TENDERLY_RPC_URL ?? BASE_RPC_URL; +const TENDERLY_WS_RPC_URL = TENDERLY_RPC_URL ? TENDERLY_RPC_URL.replace(/^http/, "ws") : BASE_WS_RPC_URL; const localhostNetwork = defineChain({ ...viem__base, @@ -80,6 +86,21 @@ export const getChainWithChainId = (chainId: number) => { return CHAIN_ID_MAP[cid]; }; +export const getChainWebSocketRpcUrl = (chainId: number) => { + switch (chainId) { + case baseNetwork.id: + return BASE_WS_RPC_URL; + case arbitrumNetwork.id: + return ARB_WS_RPC_URL; + case localhostNetwork.id: + return LOCAL_WS_RPC_URL; + case tenderlyTestnetNetwork.id: + return TENDERLY_WS_RPC_URL; + default: + return undefined; + } +}; + export const getEnvEnabledChains = (): Chain[] => { const chainIds: number[] = enabledChainIds.map(Number);