diff --git a/web/assets/js/react/components/Modal/SubmitProof/SubmitStep.tsx b/web/assets/js/react/components/Modal/SubmitProof/SubmitStep.tsx index e50e847c2..bb299186c 100644 --- a/web/assets/js/react/components/Modal/SubmitProof/SubmitStep.tsx +++ b/web/assets/js/react/components/Modal/SubmitProof/SubmitStep.tsx @@ -12,6 +12,7 @@ import { import { useAligned, useBatcherNonce, + useBatcherMaxFee, useBatcherPaymentService, useEthPrice, useBeastLeaderboardContract, @@ -111,6 +112,10 @@ export const SubmitProofStep = ({ batcher_url, user_address ); + const { maxFee: latestMaxFee, isLoading: previousMaxFeeLoading } = useBatcherMaxFee( + batcher_url, + user_address + ); const [invalidGameConfig, setInvalidGameConfig] = useState(false); const [levelAlreadyReached, setLevelAlreadyReached] = useState(false); const [gameIdx, setGameIdx] = useState(initialGameIdx); @@ -152,12 +157,17 @@ export const SubmitProofStep = ({ useEffect(() => { const fn = async () => { - const maxFee = await estimateMaxFeeForBatchOfProofs(16); - if (!maxFee) return; + const estimatedMaxFee = await estimateMaxFeeForBatchOfProofs(16); + if (!estimatedMaxFee) return; + + const maxFee = latestMaxFee && latestMaxFee < estimatedMaxFee + ? latestMaxFee + : estimatedMaxFee; + setMaxFee(maxFee); }; fn(); - }, [estimateMaxFeeForBatchOfProofs]); + }, [estimateMaxFeeForBatchOfProofs, latestMaxFee, maxFee]); const handleCombinedProofFile = async ( e: React.ChangeEvent @@ -244,12 +254,17 @@ export const SubmitProofStep = ({ return; } - const maxFee = await estimateMaxFeeForBatchOfProofs(16); - if (!maxFee) { + // This value should be capped by the previous proof max fee + const estimatedMaxFee = await estimateMaxFeeForBatchOfProofs(16); + if (!estimatedMaxFee) { alert("Could not estimate max fee"); return; } + const maxFee = latestMaxFee && latestMaxFee < estimatedMaxFee + ? latestMaxFee + : estimatedMaxFee; + if (nonce == null) { alert("Nonce is still loading or failed"); return; @@ -302,15 +317,20 @@ export const SubmitProofStep = ({ payment_service_addr, chainId, nonce, + latestMaxFee, + maxFee, ]); const handleSend = useCallback( async (proofToSubmitData: VerificationData) => { - const maxFee = await estimateMaxFeeForBatchOfProofs(16); - if (!maxFee) { + const estimatedMaxFee = await estimateMaxFeeForBatchOfProofs(16); + if (!estimatedMaxFee) { alert("Could not estimate max fee"); return; } + const maxFee = latestMaxFee && latestMaxFee < estimatedMaxFee + ? latestMaxFee + : estimatedMaxFee; if (nonce == null) { alert("Nonce is still loading or failed"); @@ -352,6 +372,8 @@ export const SubmitProofStep = ({ payment_service_addr, chainId, nonce, + latestMaxFee, + maxFee, ] ); @@ -646,6 +668,7 @@ export const SubmitProofStep = ({ !publicInputs || (balance.data || 0) < maxFee || nonceLoading || + previousMaxFeeLoading || nonce == null || levelAlreadyReached } @@ -660,6 +683,7 @@ export const SubmitProofStep = ({ disabled={ (balance.data || 0) < maxFee || nonceLoading || + previousMaxFeeLoading || nonce == null || levelAlreadyReached } diff --git a/web/assets/js/react/hooks/index.ts b/web/assets/js/react/hooks/index.ts index 4b69e1bc2..e2e7d387b 100644 --- a/web/assets/js/react/hooks/index.ts +++ b/web/assets/js/react/hooks/index.ts @@ -4,5 +4,6 @@ export * from "./useIsMounted"; export * from "./useModal"; export * from "./useOnClickOutside"; export * from "./useBatcherNonce"; +export * from "./useBatcherMaxFee"; export * from "./useBeastLeaderboardContract"; export * from "./useEthPrice"; diff --git a/web/assets/js/react/hooks/useBatcherMaxFee.ts b/web/assets/js/react/hooks/useBatcherMaxFee.ts new file mode 100644 index 000000000..a54424af6 --- /dev/null +++ b/web/assets/js/react/hooks/useBatcherMaxFee.ts @@ -0,0 +1,72 @@ +import { useEffect, useState } from "react"; +import { encode as cborEncode, decode as cborDecode } from 'cbor2'; +import { hexToBigInt } from "viem"; + +type GetLastMaxFeeFromBatcherResponse = { + ProtocolVersion: string, + LastMaxFee: `0x${string}`; + EthRpcError: string; + InvalidRequest: string; +} + +export function useBatcherMaxFee(batcher_url: string, address?: string) { + const [maxFee, setMaxFee] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + let ws: WebSocket | null = null; + + const fetchMaxFee = () => { + if (!address) { + return + } + + ws = new WebSocket(`${batcher_url}`); + ws.binaryType = 'arraybuffer'; + + ws.onopen = () => { + console.log("WebSocket connection established"); + }; + + ws.onmessage = (event) => { + try { + const cbor_data = event.data; + const data = cborDecode(new Uint8Array(cbor_data)); + + if (data?.ProtocolVersion) { + const message = { GetLastMaxFee: address }; + const encoded = cborEncode(message).buffer; + ws?.send(encoded); + } else if (data?.LastMaxFee) { + ws?.close(); + setMaxFee(hexToBigInt(data.LastMaxFee)); + setIsLoading(false); + } else if (data?.EthRpcError || data?.InvalidRequest) { + ws?.close(); + setError(new Error(JSON.stringify(data))); + setIsLoading(false); + } + } catch (e) { + ws?.close(); + setError(e as Error); + setIsLoading(false); + } + }; + + ws.onerror = () => { + ws?.close(); + setError(new Error("WebSocket connection error")); + setIsLoading(false); + }; + }; + + fetchMaxFee(); + + return () => { + ws?.close(); + }; + }, [batcher_url, address]); + + return { maxFee, isLoading, error }; +}