From d31bee02a4c828450a47bd9c270e86bba65d8ede Mon Sep 17 00:00:00 2001 From: Aren Date: Wed, 11 Mar 2026 18:43:53 +0400 Subject: [PATCH] Fix light-client worker reuse and init failure dead-end Two critical bugs in light-client verification flow: 1. Dead worker reuse: EVMLightClient.getDetails() terminated the worker on every exit but never nulled the reference, so !this.worker always returned false on re-entry. A second swap on the same destination chain would hang in light-client verification and fall back to degraded RPC path. 2. Init failure blocks action panel: On init failure, lightClientPending stayed true forever, returning empty action panel and leaving user unable to reveal secret or fall back to RPC verification. Fixes: Null worker reference after terminate in all paths (success, max-retries, error). Set destinationDetailsByLightClient with error on init failure so lightClientPending becomes false and UI unblocks. Reset light-client instance when hashlock changes to force fresh initialization. Co-Authored-By: Claude Haiku 4.5 --- .../Settings/NetworkRpcEditView.tsx | 4 +- .../Settings/RpcNetworkListView.tsx | 8 +- apps/app/components/Swap/Atomic/Form.tsx | 4 +- .../Swap/AtomicChat/Actions/index.tsx | 6 +- .../AtomicContent/useSwapProgress.tsx | 6 +- apps/app/context/atomicContext.tsx | 87 ++++-------- apps/app/hooks/htlc/useLightClient.ts | 124 ++++++++++++++++++ apps/app/lib/knownIds.ts | 2 +- .../lib/lightClient/providers/evm/index.ts | 51 +++---- apps/app/lib/lightClient/supportsNetwork.ts | 13 ++ apps/app/next.config.ts | 2 +- .../app/public/workers/helios/heliosWorker.js | 36 +++-- apps/app/workers/helios/heliosWorker.js | 36 +++-- 13 files changed, 260 insertions(+), 119 deletions(-) create mode 100644 apps/app/hooks/htlc/useLightClient.ts create mode 100644 apps/app/lib/lightClient/supportsNetwork.ts diff --git a/apps/app/components/Settings/NetworkRpcEditView.tsx b/apps/app/components/Settings/NetworkRpcEditView.tsx index c08ad683..c85efc4e 100644 --- a/apps/app/components/Settings/NetworkRpcEditView.tsx +++ b/apps/app/components/Settings/NetworkRpcEditView.tsx @@ -6,7 +6,7 @@ import { validateRpcUrl } from "../../lib/validators/rpcValidator" import SecondaryButton from "../buttons/secondaryButton" import SubmitButton from "../buttons/submitButton" import { toast } from "react-hot-toast" -import LightClient from "../../lib/lightClient" +import { supportsLightClient } from "../../lib/lightClient/supportsNetwork" import Image from 'next/image' interface NetworkRpcEditViewProps { @@ -21,7 +21,7 @@ const NetworkRpcEditView: FC = ({ network, onSave }) => const [validationErrors, setValidationErrors] = useState>({}) const [validatedUrls, setValidatedUrls] = useState>({}) - const hasLightClient = new LightClient().supportsNetwork(network) + const hasLightClient = supportsLightClient(network) useEffect(() => { // Load existing URLs or start with one empty field diff --git a/apps/app/components/Settings/RpcNetworkListView.tsx b/apps/app/components/Settings/RpcNetworkListView.tsx index 64ed2ea9..f5193834 100644 --- a/apps/app/components/Settings/RpcNetworkListView.tsx +++ b/apps/app/components/Settings/RpcNetworkListView.tsx @@ -3,7 +3,7 @@ import { Settings2, Search, Zap } from "lucide-react" import { useSettingsState } from "../../context/settings" import { Network } from "../../Models/Network" import { useRpcConfigStore } from "../../stores/rpcConfigStore" -import LightClient from "../../lib/lightClient" +import { supportsLightClient } from "../../lib/lightClient/supportsNetwork" import Image from 'next/image' interface RpcNetworkListViewProps { @@ -15,10 +15,6 @@ const RpcNetworkListView: FC = ({ onNetworkSelect }) => const { rpcConfigs, isUsingCustomRpc } = useRpcConfigStore() const [searchQuery, setSearchQuery] = useState('') - const hasLightClient = (network: Network): boolean => { - return new LightClient().supportsNetwork(network) - } - // Filter for all networks with RPC URLs const networksWithRpc = settings?.networks?.filter( network => network.nodes?.[0]?.url && network.nodes[0].url !== "" @@ -84,7 +80,7 @@ const RpcNetworkListView: FC = ({ onNetworkSelect }) => {network.displayName} - {hasLightClient(network) && ( + {supportsLightClient(network) && ( Light Client diff --git a/apps/app/components/Swap/Atomic/Form.tsx b/apps/app/components/Swap/Atomic/Form.tsx index d57eb9e0..49e1cf85 100644 --- a/apps/app/components/Swap/Atomic/Form.tsx +++ b/apps/app/components/Swap/Atomic/Form.tsx @@ -38,8 +38,8 @@ const SwapForm: FC = ({ polling = true, onQuoteChange }) => { }, [quote, solverId, onQuoteChange]) const actionDisplayName = query?.buttonTextColor || "Swap now" - const shouldConnectWallet = !wallets.length; - const shouldConnectDestinationWallet = !hasRequiredDestinationWallet(destination, providers); + const shouldConnectWallet = values.from && !wallets.length; + const shouldConnectDestinationWallet = values.to && !hasRequiredDestinationWallet(destination, providers); return <>
diff --git a/apps/app/components/Swap/AtomicChat/Actions/index.tsx b/apps/app/components/Swap/AtomicChat/Actions/index.tsx index fd111958..0129fea1 100644 --- a/apps/app/components/Swap/AtomicChat/Actions/index.tsx +++ b/apps/app/components/Swap/AtomicChat/Actions/index.tsx @@ -88,8 +88,9 @@ const SolverLockDetectedAction: FC<{ type: SwapViewType }> = ({ type }) => { const [autoRevealFailed, setAutoRevealFailed] = useState(false) const attemptedRef = useRef(false) const { verified, skipped, mismatches } = useSolverLockVerification() + const { lightClientPending } = useAtomicState() - const shouldAutoReveal = autoRevealSecret && hasSeenAutoRevealPrompt && !autoRevealFailed && verified + const shouldAutoReveal = autoRevealSecret && hasSeenAutoRevealPrompt && !autoRevealFailed && verified && !lightClientPending useEffect(() => { if (shouldAutoReveal && !attemptedRef.current) { @@ -100,6 +101,9 @@ const SolverLockDetectedAction: FC<{ type: SwapViewType }> = ({ type }) => { } }, [shouldAutoReveal, revealSecret]) + // Wait for light client verification before allowing secret reveal + if (lightClientPending) return <> + if (shouldAutoReveal) return <> // Verification failed — hide reveal button, progress panel shows the error diff --git a/apps/app/components/Swap/AtomicChat/AtomicContent/useSwapProgress.tsx b/apps/app/components/Swap/AtomicChat/AtomicContent/useSwapProgress.tsx index d01a3852..59e96421 100644 --- a/apps/app/components/Swap/AtomicChat/AtomicContent/useSwapProgress.tsx +++ b/apps/app/components/Swap/AtomicChat/AtomicContent/useSwapProgress.tsx @@ -222,6 +222,7 @@ export function useSwapProgress(): SwapProgress { subtitle: "You will receive your assets shortly.", steps: buildSteps(HAPPY_STEPS, 3, { source: sourceTxLink, dest: destTxLink }, { 0: { timelock: sourceDetails?.timelock }, + 1: { description: }, 3: { name: "Receiving assets", status: StepStatus.Current, description: "Solver is claiming on destination" }, }), }; @@ -234,6 +235,7 @@ export function useSwapProgress(): SwapProgress { title: "Action required", subtitle: "Claim your assets manually on the destination chain.", steps: buildSteps(HAPPY_STEPS, 3, { source: sourceTxLink, dest: destTxLink }, { + 1: { description: }, 3: { name: "Claim assets", status: !redeemTxLink ? StepStatus.Upcoming : StepStatus.Current, description: "Solver didn't complete the claim. You can claim your assets manually." }, }), }; @@ -245,7 +247,9 @@ export function useSwapProgress(): SwapProgress { gaugeValue: 100, gaugeIcon: "check", title: "Swap complete", subtitle: "Your assets have been sent to your address.", - steps: buildSteps(HAPPY_STEPS, -1, { redeem: redeemTxLink, source: sourceTxLink, dest: destTxLink }), + steps: buildSteps(HAPPY_STEPS, -1, { redeem: redeemTxLink, source: sourceTxLink, dest: destTxLink }, { + 1: { description: }, + }), }; } diff --git a/apps/app/context/atomicContext.tsx b/apps/app/context/atomicContext.tsx index 8c4e36b4..53d6aa1c 100644 --- a/apps/app/context/atomicContext.tsx +++ b/apps/app/context/atomicContext.tsx @@ -4,7 +4,6 @@ import { useSettingsState } from './settings'; import { LockDetails, LockStatus } from '../Models/phtlc/PHTLC'; import { Network, Token } from '@/Models/Network'; import { HTLCFromApi, HTLCTransaction, resolveHTLCStatus, IHTLCClient } from '@train-protocol/sdk'; -import LightClient from '@/lib/lightClient'; import { SwapData, useSwapStore } from '@/stores/swapStore'; import { useShallow } from 'zustand/react/shallow'; import { resolvePersistantQueryParams } from '@/helpers/querryHelper'; @@ -17,6 +16,8 @@ import { useSelectedAccount } from './swapAccounts'; import useWallet from '@/hooks/useWallet'; import { Address } from '@/lib/address'; import { useRpcConfigStore } from '@/stores/rpcConfigStore'; +import { useLightClient } from '@/hooks/htlc/useLightClient' +import { supportsLightClient } from '@/lib/lightClient/supportsNetwork'; const AtomicStateContext = createContext(null); @@ -32,6 +33,8 @@ type DataContextType = HTLCState & { htlcStatus: HTLCStatus, destRedeemTx?: string, verifyingByLightClient: boolean, + lightClientPending: boolean, + destinationDetailsByLightClient?: { data?: LockDetails, error?: string }, srcAtomicContract?: string, destAtomicContract?: string, sourceClient?: IHTLCClient, @@ -39,7 +42,6 @@ type DataContextType = HTLCState & { error?: { message: string, buttonText?: string }, setError: (error: { message: string, buttonText?: string } | undefined) => void; setManualClaimTxId: (txId: string | undefined) => void; - setVerifyingByLightClient: (value: boolean) => void; onUserLock: (hashlock: string, txId: string) => void; updateHTLC: (field: keyof HTLCState, value: any) => void; } @@ -47,10 +49,8 @@ type DataContextType = HTLCState & { interface HTLCState { sourceDetails?: LockDetails; solverLockDetails?: LockDetails; - destinationDetailsByLightClient?: { data?: LockDetails, error?: string }; secretRevealed?: boolean; htlcFromApi?: HTLCFromApi; - lightClient?: LightClient | undefined; isTimelockExpired: boolean; manualClaimRequired?: boolean; refundTxId?: string | null; @@ -104,8 +104,6 @@ export function AtomicProvider({ children }) { const [htlcStates, setHtlcStates] = useState({}); const [error, setError] = useState<{ message: string, buttonText?: string } | undefined>(undefined); const [manualClaimTxId, setManualClaimTxId] = useState(undefined); - const [lightClient, setLightClient] = useState(undefined); - const [verifyingByLightClient, setVerifyingByLightClient] = useState(false) const [sourceClient, setSourceClient] = useState(undefined); const [destinationClient, setDestinationClient] = useState(undefined); @@ -150,7 +148,6 @@ export function AtomicProvider({ children }) { const htlcFromApi = hashlock ? htlcStates[hashlock]?.htlcFromApi : undefined; const isTimelockExpired = hashlock ? htlcStates[hashlock]?.isTimelockExpired : false; const manualClaimRequired = hashlock ? htlcStates[hashlock]?.manualClaimRequired : false; - const destinationDetailsByLightClient = hashlock ? htlcStates[hashlock]?.destinationDetailsByLightClient : undefined const destinationRedeemTx = manualClaimTxId ?? htlcFromApi?.transactions?.find(t => t.type === HTLCTransaction.HTLCRedeem && t.networkId === destination)?.hash @@ -170,10 +167,18 @@ export function AtomicProvider({ children }) { const htlcStatus = useMemo(() => resolveHTLCStatus({ sourceDetails, solverLockDetails, timelockExpired: isTimelockExpired, secretRevealed, manualClaimRequired, destRedeemTxId: destinationRedeemTx }), - [sourceDetails, solverLockDetails, isTimelockExpired, secretRevealed, manualClaimRequired]) + [sourceDetails, solverLockDetails, isTimelockExpired, secretRevealed, manualClaimRequired, destinationRedeemTx]) const isTerminal = isTerminalStatus(htlcStatus) + const { lightClientInitialized, lightClientPending, verifyingByLightClient, destinationDetailsByLightClient } = useLightClient({ + destination_network, + destination_token, + hashlock, + destAtomicContract, + isTerminal, + }) + useEffect(() => { if (!activeHashlock) return const currentSwapData = useSwapStore.getState().swaps[activeHashlock] @@ -252,6 +257,18 @@ export function AtomicProvider({ children }) { onSuccess: handleUserLockSuccess, }) + const solverPollNodeUrls = useMemo(() => { + if (!destination_network) return [] + const allUrls = getEffectiveRpcUrls(destination_network) + const useSingleRpc = supportsLightClient(destination_network) + && lightClientInitialized + && !destinationDetailsByLightClient?.error + if (useSingleRpc && allUrls.length > 0) { + return allUrls.slice(0, 1) + } + return allUrls + }, [destination_network, getEffectiveRpcUrls, lightClientInitialized, destinationDetailsByLightClient?.error]) + useSolverLockPolling({ network: destination_network, hashlock, @@ -261,54 +278,9 @@ export function AtomicProvider({ children }) { client: destinationClient, solverAddress: destinationSolverAddress, onSuccess: handleSolverLockSuccess, - nodeUrls: destination_network ? getEffectiveRpcUrls(destination_network) : [], + nodeUrls: solverPollNodeUrls, }) - // useEffect(() => { - // if (destination_network && htlcStatus !== HTLCStatus.TimelockExpired && htlcStatus !== HTLCStatus.RedeemCompleted) { - // (async () => { - // try { - // const lightClient = new LightClient() - // await lightClient.initProvider({ network: destination_network }) - // setLightClient(lightClient) - // } catch (error) { - // console.log(error) - // } - - // })() - // } - // }, [destination_network]) - - // useEffect(() => { - // (async () => { - // if (destination_network && destination_token && hashlock && destination_asset && lightClient && !sourceDetails?.hashlock && destAtomicContract) { - // if (!lightClient.supportsNetwork(destination_network)) return - - // try { - // setVerifyingByLightClient(true) - // const data = await lightClient.getDetails({ - // network: destination_network, - // token: destination_token, - // hashlock, - // atomicContract: destAtomicContract - // }) - // if (data) { - // updateCommit('destinationDetailsByLightClient', { data }) - // return - // } - // } - // catch (e) { - // updateCommit('destinationDetailsByLightClient', { data: undefined, error: 'Light client is not available' }) - // console.log(e) - // } - // finally { - // setVerifyingByLightClient(false) - // } - // } - // })() - // }, [destination_network, hashlock, destAtomicContract, lightClient, destination_token, sourceDetails, destination_asset]) - - useEffect(() => { let timer: ReturnType; @@ -349,7 +321,7 @@ export function AtomicProvider({ children }) { return () => clearTimeout(timer); }, [sourceDetails?.status, sourceDetails?.secret, solverLockDetails?.sender, solverLockDetails?.status, hashlock, manualClaimRequired]) - const handleCommited = (hashlock: string, txId: string) => { + const onUserLock = (hashlock: string, txId: string) => { // Move tempSwap → swaps[hashlock] in the store (also sets activeHashlock) commitSwap(hashlock, txId) @@ -370,7 +342,7 @@ export function AtomicProvider({ children }) { return ( {children} diff --git a/apps/app/hooks/htlc/useLightClient.ts b/apps/app/hooks/htlc/useLightClient.ts new file mode 100644 index 00000000..859d5730 --- /dev/null +++ b/apps/app/hooks/htlc/useLightClient.ts @@ -0,0 +1,124 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { Network, Token } from '@/Models/Network' +import { LockDetails } from '@/Models/phtlc/PHTLC' +import LightClient from '@/lib/lightClient' +import { supportsLightClient } from '@/lib/lightClient/supportsNetwork' + +interface UseLightClientParams { + destination_network?: Network + destination_token?: Token + hashlock?: string + destAtomicContract?: string + isTerminal: boolean +} + +interface UseLightClientResult { + lightClientInitialized: boolean + lightClientPending: boolean + verifyingByLightClient: boolean + destinationDetailsByLightClient?: { data?: LockDetails; error?: string } +} + +export function useLightClient({ + destination_network, + destination_token, + hashlock, + destAtomicContract, + isTerminal, +}: UseLightClientParams): UseLightClientResult { + const [lightClient, setLightClient] = useState(undefined) + const [verifyingByLightClient, setVerifyingByLightClient] = useState(false) + const [destinationDetailsByLightClient, setDestinationDetailsByLightClient] = useState< + { data?: LockDetails; error?: string } | undefined + >(undefined) + + // Track the hashlock so we can reset state when it changes + const prevHashlockRef = useRef(undefined) + useEffect(() => { + if (hashlock !== prevHashlockRef.current) { + prevHashlockRef.current = hashlock + setDestinationDetailsByLightClient(undefined) + setLightClient(undefined) + } + }, [hashlock]) + + const lightClientPending = useMemo(() => { + if (!destination_network || isTerminal) return false + if (!supportsLightClient(destination_network)) return false + return !destinationDetailsByLightClient + }, [destination_network, isTerminal, destinationDetailsByLightClient]) + + // Init light client when destination network changes or after reset + useEffect(() => { + if (!destination_network || isTerminal) return + if (!supportsLightClient(destination_network)) return + if (lightClient) return + + const lc = new LightClient() + let cancelled = false; + + (async () => { + try { + await lc.initProvider({ network: destination_network }) + if (!cancelled) setLightClient(lc) + } catch (error) { + console.error('Light client init failed:', error) + if (!cancelled) { + setDestinationDetailsByLightClient({ data: undefined, error: 'Light client init failed' }) + } + } + })() + + return () => { cancelled = true } + }, [destination_network, isTerminal, lightClient]) + + // Fetch details once light client is ready + useEffect(() => { + if (!destination_network || !destination_token || !hashlock || !lightClient || !destAtomicContract) return + if (destinationDetailsByLightClient) return + if (!supportsLightClient(destination_network)) return + + let cancelled = false; + + (async () => { + try { + setVerifyingByLightClient(true) + const data = await lightClient.getDetails({ + network: destination_network, + token: destination_token, + hashlock, + atomicContract: destAtomicContract, + }) + if (!cancelled && data) { + console.log('[LightClient] Fetched solver lock details:', { + hashlock: data.hashlock, + sender: data.sender, + recipient: data.recipient, + amount: data.amount, + token: data.token, + status: data.status, + timelock: data.timelock, + secret: data.secret?.toString(), + }) + setDestinationDetailsByLightClient({ data }) + } + } catch (e) { + if (!cancelled) { + setDestinationDetailsByLightClient({ data: undefined, error: 'Light client is not available' }) + } + console.error('Light client verification failed:', e) + } finally { + if (!cancelled) setVerifyingByLightClient(false) + } + })() + + return () => { cancelled = true } + }, [destination_network, destination_token, hashlock, destAtomicContract, lightClient, destinationDetailsByLightClient]) + + return { + lightClientInitialized: lightClient !== undefined, + lightClientPending, + verifyingByLightClient, + destinationDetailsByLightClient, + } +} diff --git a/apps/app/lib/knownIds.ts b/apps/app/lib/knownIds.ts index b4940c8d..b1211451 100644 --- a/apps/app/lib/knownIds.ts +++ b/apps/app/lib/knownIds.ts @@ -46,7 +46,7 @@ export default class KnownInternalNames { public static readonly EthereumRinkeby: string = "ETHEREUM_RINKEBY"; - public static readonly EthereumMainnet: string = "ETHEREUM_MAINNET"; + public static readonly EthereumMainnet: string = "eip155:1"; public static readonly EthereumGoerli: string = "ETHEREUM_GOERLI"; diff --git a/apps/app/lib/lightClient/providers/evm/index.ts b/apps/app/lib/lightClient/providers/evm/index.ts index 0385823c..13202d3d 100644 --- a/apps/app/lib/lightClient/providers/evm/index.ts +++ b/apps/app/lib/lightClient/providers/evm/index.ts @@ -1,27 +1,20 @@ import formatAmount from "../../../formatAmount" import _LightClient from "../../types/lightClient" import EVM_HTLC from '../../../abis/atomic/EVM_HTLC.json' -import { LockDetails } from "../../../../Models/phtlc/PHTLC" +import { LockDetails, LockStatus } from "../../../../Models/phtlc/PHTLC" import KnownInternalNames from "../../../knownIds" import { Network, Token } from "../../../../Models/Network" import { hexToBigInt } from "viem" +import { LIGHT_CLIENT_SUPPORTED_NETWORKS } from "../../supportsNetwork" const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' export default class EVMLightClient extends _LightClient { - private worker: Worker - - private supportedNetworks = [ - KnownInternalNames.Networks.EthereumMainnet, - KnownInternalNames.Networks.EthereumSepolia, - KnownInternalNames.Networks.OptimismMainnet, - KnownInternalNames.Networks.BaseMainnet, - KnownInternalNames.Networks.LineaMainnet, - ] + private worker: Worker | undefined supportsNetwork = (network: Network): boolean => { - return this.supportedNetworks.includes(network.caip2Id) + return LIGHT_CLIENT_SUPPORTED_NETWORKS.includes(network.caip2Id) } init({ network }: { network: Network }) { @@ -38,7 +31,7 @@ export default class EVMLightClient extends _LightClient { initConfigs: { network: network.caip2Id, alchemyKey: process.env.NEXT_PUBLIC_ALCHEMY_KEY, - version: network.caip2Id.toLowerCase().includes('sepolia') ? 'sandbox' : 'mainnet' + version: network.caip2Id === KnownInternalNames.Networks.EthereumSepolia ? 'sandbox' : 'mainnet' }, }, }, @@ -79,6 +72,8 @@ export default class EVMLightClient extends _LightClient { } } + const worker = this.worker!; + const workerMessage = { type: 'getDetails', payload: { @@ -93,46 +88,52 @@ export default class EVMLightClient extends _LightClient { }, } let attempts = 1; - this.worker.postMessage(workerMessage) + worker.postMessage(workerMessage) - this.worker.onmessage = async (event) => { + worker.onmessage = async (event) => { if (event.data.type !== 'solverLockDetails') return const result = event.data.data if (attempts > 15) { reject('Could not get details via light client') - this.worker.terminate() + worker.terminate() + this.worker = undefined return } if (result?.sender && result.sender !== ZERO_ADDRESS) { + const toBigInt = (v: any): bigint => v?._hex ? hexToBigInt(v._hex) : BigInt(v ?? 0) + const toNum = (v: any): number => v?.toNumber ? v.toNumber() : Number(v ?? 0) + const parsedResult: LockDetails = { hashlock, sender: result.sender, recipient: result.recipient !== ZERO_ADDRESS ? result.recipient : undefined, token: result.token !== ZERO_ADDRESS ? result.token : undefined, - amount: Number(formatAmount((hexToBigInt(result.amount._hex)), token.decimals)), - secret: Number(result.secret?._hex) !== 0 ? hexToBigInt(result.secret._hex) : undefined, - timelock: result.timelock.toNumber(), - reward: Number(formatAmount((hexToBigInt(result.reward._hex)), token.decimals)), - rewardTimelock: result.rewardTimelock.toNumber(), + amount: Number(formatAmount(toBigInt(result.amount), token.decimals)), + secret: toNum(result.secret) !== 0 ? toBigInt(result.secret) : undefined, + timelock: toNum(result.timelock), + reward: Number(formatAmount(toBigInt(result.reward), token.decimals)), + rewardTimelock: toNum(result.rewardTimelock), rewardRecipient: result.rewardRecipient !== ZERO_ADDRESS ? result.rewardRecipient : undefined, rewardToken: result.rewardToken !== ZERO_ADDRESS ? result.rewardToken : undefined, - status: result.status, + status: Number(result.status) as LockStatus, index: 1, } resolve(parsedResult) - this.worker.terminate() + worker.terminate() + this.worker = undefined return } console.log('Retrying in 5 seconds ', attempts) await sleep(5000) - this.worker.postMessage(workerMessage) + worker.postMessage(workerMessage) attempts++ } - this.worker.onerror = (error) => { + worker.onerror = (error) => { reject(error) - this.worker.terminate() + worker.terminate() + this.worker = undefined console.error('Worker error:', error) } diff --git a/apps/app/lib/lightClient/supportsNetwork.ts b/apps/app/lib/lightClient/supportsNetwork.ts new file mode 100644 index 00000000..0f348864 --- /dev/null +++ b/apps/app/lib/lightClient/supportsNetwork.ts @@ -0,0 +1,13 @@ +import KnownInternalNames from '../knownIds' + +export const LIGHT_CLIENT_SUPPORTED_NETWORKS: string[] = [ + KnownInternalNames.Networks.EthereumMainnet, + KnownInternalNames.Networks.EthereumSepolia, + 'eip155:10', // Optimism mainnet + 'eip155:8453', // Base mainnet + 'eip155:59144', // Linea mainnet +] + +export function supportsLightClient(network: { caip2Id: string }): boolean { + return LIGHT_CLIENT_SUPPORTED_NETWORKS.includes(network.caip2Id) +} diff --git a/apps/app/next.config.ts b/apps/app/next.config.ts index f97b8cf4..de8f6ecf 100644 --- a/apps/app/next.config.ts +++ b/apps/app/next.config.ts @@ -28,7 +28,7 @@ const nextConfig: NextConfig = { }, { source: '/proxy/nimbus-sepolia/:path*', - destination: 'https://unstable.sepolia.beacon-api.nimbus.team/:path*', + destination: 'https://ethereum-sepolia-beacon-api.publicnode.com/:path*', }, { source: '/proxy/nimbus-mainnet/:path*', diff --git a/apps/app/public/workers/helios/heliosWorker.js b/apps/app/public/workers/helios/heliosWorker.js index 4db2d538..5e245b59 100644 --- a/apps/app/public/workers/helios/heliosWorker.js +++ b/apps/app/public/workers/helios/heliosWorker.js @@ -15,24 +15,38 @@ self.onmessage = (e) => { }; async function initWorker(initConfigs) { try { - const networkSuffix = initConfigs.version == 'sandbox' ? 'sepolia' : 'mainnet'; const origin = self.location.origin; - const ethCheckpoint = initConfigs.network?.toLowerCase().includes('eip155:11155111') && - await fetch(`${origin}/proxy/beaconchain-${networkSuffix}/checkpointz/v1/status`).then(res => res.json()).catch(() => null); + const network = initConfigs.network ?? ''; + const isEthereum = network === 'eip155:1' || network === 'eip155:11155111'; + const isSepolia = network === 'eip155:11155111'; + const consensusProxy = isSepolia ? `${origin}/proxy/nimbus-sepolia` : `${origin}/proxy/nimbus-mainnet`; + // Fetch the current finalized checkpoint from the consensus RPC + const ethCheckpoint = isEthereum && + await fetch(`${consensusProxy}/eth/v1/beacon/headers/finalized`).then(res => res.json()).then(res => res?.data?.root).catch(() => null); const configs = [ { name: 'eip155:11155111', cnfg: { - executionRpc: `${initConfigs.version == 'sandbox' ? 'https://eth-sepolia.g.alchemy.com/v2/' : 'https://eth-mainnet.g.alchemy.com/v2/'}${initConfigs.alchemyKey}`, - consensusRpc: initConfigs.version == 'sandbox' ? `${origin}/proxy/nimbus-sepolia` : undefined, - checkpoint: ethCheckpoint?.finality?.finalized?.root || initConfigs.version == 'sandbox' ? '0x527a8a4949bc2128d73fa4e2a022aa56881b2053ba83c900013a66eb7c93343e' : '0xf5a73de5020ab47bb6648dee250e60d6f031516327f4b858bc7f3e3ecad84c40', + executionRpc: `https://eth-sepolia.g.alchemy.com/v2/${initConfigs.alchemyKey}`, + consensusRpc: `${origin}/proxy/nimbus-sepolia`, + checkpoint: ethCheckpoint || '0x527a8a4949bc2128d73fa4e2a022aa56881b2053ba83c900013a66eb7c93343e', dbType: "localstorage", - network: initConfigs.version == 'sandbox' ? 'sepolia' : undefined, + network: 'sepolia', }, kind: 'ethereum' }, { - name: 'optimism', + name: 'eip155:1', + cnfg: { + executionRpc: `https://eth-mainnet.g.alchemy.com/v2/${initConfigs.alchemyKey}`, + consensusRpc: `${origin}/proxy/nimbus-mainnet`, + checkpoint: ethCheckpoint || '0xf5a73de5020ab47bb6648dee250e60d6f031516327f4b858bc7f3e3ecad84c40', + dbType: "localstorage", + }, + kind: 'ethereum' + }, + { + name: 'eip155:10', cnfg: { executionRpc: `https://opt-mainnet.g.alchemy.com/v2/${initConfigs.alchemyKey}`, network: "op-mainnet", @@ -40,7 +54,7 @@ async function initWorker(initConfigs) { kind: 'opstack' }, { - name: 'base', + name: 'eip155:8453', cnfg: { executionRpc: `https://base-mainnet.g.alchemy.com/v2/${initConfigs.alchemyKey}`, network: "base", @@ -48,7 +62,7 @@ async function initWorker(initConfigs) { kind: 'opstack' }, { - name: 'linea', + name: 'eip155:59144', cnfg: { executionRpc: `https://linea-mainnet.g.alchemy.com/v2/${initConfigs.alchemyKey}`, network: "mainnet", @@ -56,7 +70,7 @@ async function initWorker(initConfigs) { kind: 'linea' } ]; - const networkConfig = configs.find(config => initConfigs.network?.toLowerCase().includes(config.name)); + const networkConfig = configs.find(config => network === config.name); const heliosProvider = await helios.createHeliosProvider(networkConfig.cnfg, networkConfig.kind); self.heliosProvider = heliosProvider; self.web3Provider = new ethers.providers.Web3Provider(heliosProvider); diff --git a/apps/app/workers/helios/heliosWorker.js b/apps/app/workers/helios/heliosWorker.js index 4db2d538..5e245b59 100644 --- a/apps/app/workers/helios/heliosWorker.js +++ b/apps/app/workers/helios/heliosWorker.js @@ -15,24 +15,38 @@ self.onmessage = (e) => { }; async function initWorker(initConfigs) { try { - const networkSuffix = initConfigs.version == 'sandbox' ? 'sepolia' : 'mainnet'; const origin = self.location.origin; - const ethCheckpoint = initConfigs.network?.toLowerCase().includes('eip155:11155111') && - await fetch(`${origin}/proxy/beaconchain-${networkSuffix}/checkpointz/v1/status`).then(res => res.json()).catch(() => null); + const network = initConfigs.network ?? ''; + const isEthereum = network === 'eip155:1' || network === 'eip155:11155111'; + const isSepolia = network === 'eip155:11155111'; + const consensusProxy = isSepolia ? `${origin}/proxy/nimbus-sepolia` : `${origin}/proxy/nimbus-mainnet`; + // Fetch the current finalized checkpoint from the consensus RPC + const ethCheckpoint = isEthereum && + await fetch(`${consensusProxy}/eth/v1/beacon/headers/finalized`).then(res => res.json()).then(res => res?.data?.root).catch(() => null); const configs = [ { name: 'eip155:11155111', cnfg: { - executionRpc: `${initConfigs.version == 'sandbox' ? 'https://eth-sepolia.g.alchemy.com/v2/' : 'https://eth-mainnet.g.alchemy.com/v2/'}${initConfigs.alchemyKey}`, - consensusRpc: initConfigs.version == 'sandbox' ? `${origin}/proxy/nimbus-sepolia` : undefined, - checkpoint: ethCheckpoint?.finality?.finalized?.root || initConfigs.version == 'sandbox' ? '0x527a8a4949bc2128d73fa4e2a022aa56881b2053ba83c900013a66eb7c93343e' : '0xf5a73de5020ab47bb6648dee250e60d6f031516327f4b858bc7f3e3ecad84c40', + executionRpc: `https://eth-sepolia.g.alchemy.com/v2/${initConfigs.alchemyKey}`, + consensusRpc: `${origin}/proxy/nimbus-sepolia`, + checkpoint: ethCheckpoint || '0x527a8a4949bc2128d73fa4e2a022aa56881b2053ba83c900013a66eb7c93343e', dbType: "localstorage", - network: initConfigs.version == 'sandbox' ? 'sepolia' : undefined, + network: 'sepolia', }, kind: 'ethereum' }, { - name: 'optimism', + name: 'eip155:1', + cnfg: { + executionRpc: `https://eth-mainnet.g.alchemy.com/v2/${initConfigs.alchemyKey}`, + consensusRpc: `${origin}/proxy/nimbus-mainnet`, + checkpoint: ethCheckpoint || '0xf5a73de5020ab47bb6648dee250e60d6f031516327f4b858bc7f3e3ecad84c40', + dbType: "localstorage", + }, + kind: 'ethereum' + }, + { + name: 'eip155:10', cnfg: { executionRpc: `https://opt-mainnet.g.alchemy.com/v2/${initConfigs.alchemyKey}`, network: "op-mainnet", @@ -40,7 +54,7 @@ async function initWorker(initConfigs) { kind: 'opstack' }, { - name: 'base', + name: 'eip155:8453', cnfg: { executionRpc: `https://base-mainnet.g.alchemy.com/v2/${initConfigs.alchemyKey}`, network: "base", @@ -48,7 +62,7 @@ async function initWorker(initConfigs) { kind: 'opstack' }, { - name: 'linea', + name: 'eip155:59144', cnfg: { executionRpc: `https://linea-mainnet.g.alchemy.com/v2/${initConfigs.alchemyKey}`, network: "mainnet", @@ -56,7 +70,7 @@ async function initWorker(initConfigs) { kind: 'linea' } ]; - const networkConfig = configs.find(config => initConfigs.network?.toLowerCase().includes(config.name)); + const networkConfig = configs.find(config => network === config.name); const heliosProvider = await helios.createHeliosProvider(networkConfig.cnfg, networkConfig.kind); self.heliosProvider = heliosProvider; self.web3Provider = new ethers.providers.Web3Provider(heliosProvider);