diff --git a/CLAUDE.md b/CLAUDE.md index b0830e37..e57e1b2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,6 +51,14 @@ Key files: - `packages/blockchains/{evm,solana,starknet,aztec}/src/client.ts` — chain-specific HTLC implementations - `apps/app/lib/wallets/utils/atomicTypes.ts` — chain-specific wallet/atomic interfaces +### RPC Node Resolution & Consensus +- `apps/app/lib/rpc/` — dynamic RPC resolution: `nodeResolver.ts` (entry point), `evmNodes.ts` (chainlist-rpcs), `nonEvmNodes.ts` (static registry) +- `resolveNodes(caip2Id)` returns all available RPCs (existing nodes first, then dynamic/static). Called server-side in `getSettings.ts` +- `rpcConfigStore` manages user custom RPC overrides; `getEffectiveRpcUrls(network)` returns custom URLs or `network.nodes` +- **Consensus verification**: `getSolverLockDetailsWithConsensus()` in SDK queries nodes in batches of `batchSize` (default 3), retries with next batch if quorum (`minQuorum`, default 2) not met +- `ConsensusOptions`: `{ minQuorum?: number, batchSize?: number }` — configurable per-call or via subclass defaults +- Consensus runs once on first solver lock detection (tracked by `consensusVerified` ref in `useSolverLockPolling`), then falls back to single-node polling + ### Secret & Nonce - Secret derived from: `deriveInitialKey()` + `deriveSecretFromTimelock(key, nonce)` - Nonce = `Date.now()` timestamp, stored in URL query params (for page refresh recovery) and on-chain via `userData` bytes field diff --git a/apps/app/context/atomicContext.tsx b/apps/app/context/atomicContext.tsx index b2dd3d9f..8b1c8abb 100644 --- a/apps/app/context/atomicContext.tsx +++ b/apps/app/context/atomicContext.tsx @@ -62,6 +62,8 @@ type CommitStatesDict = Record; export function AtomicProvider({ children }) { const router = useRouter() const { networks } = useSettingsState() + const rpcConfigs = useRpcConfigStore(s => s.rpcConfigs) + const getEffectiveRpcUrls = useRpcConfigStore(s => s.getEffectiveRpcUrls) const activeHashlock = useSwapStore(s => s.activeHashlock) const updateSwap = useSwapStore(s => s.updateSwap) @@ -84,7 +86,6 @@ export function AtomicProvider({ children }) { useShallow(s => activeHashlock ? s.swaps[activeHashlock] ?? null : null) ) const currentSwap = tempSwap ?? committedSwap - const { getEffectiveRpcUrls } = useRpcConfigStore(); const address = currentSwap?.address const amount = currentSwap?.requestedAmount @@ -168,7 +169,7 @@ 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) @@ -248,6 +249,13 @@ export function AtomicProvider({ children }) { onSuccess: handleUserLockSuccess, }) + // Subscribe reactively to rpcConfigs so RPC URL changes trigger a rerender + const destNodeUrls = useMemo( + () => destination_network ? getEffectiveRpcUrls(destination_network) : [], + // eslint-disable-next-line react-hooks/exhaustive-deps + [destination_network, rpcConfigs] + ) + useSolverLockPolling({ network: destination_network, hashlock, @@ -257,7 +265,7 @@ export function AtomicProvider({ children }) { client: destinationClient, solverAddress: destinationSolverAddress, onSuccess: handleSolverLockSuccess, - nodeUrls: destination_network ? getEffectiveRpcUrls(destination_network) : [], + nodeUrls: destNodeUrls, }) // useEffect(() => { @@ -340,7 +348,7 @@ export function AtomicProvider({ children }) { const timer = setTimeout(() => { updateHTLCState(hashlock, { manualClaimRequired: true }); - }, 3 * 60 * 1000); // 2 minutes + }, 2 * 60 * 1000); // 2 minutes return () => clearTimeout(timer); }, [sourceDetails?.status, sourceDetails?.secret, solverLockDetails?.sender, solverLockDetails?.status, hashlock, manualClaimRequired]) diff --git a/apps/app/helpers/getSettings.ts b/apps/app/helpers/getSettings.ts index f9e27442..2b062da5 100644 --- a/apps/app/helpers/getSettings.ts +++ b/apps/app/helpers/getSettings.ts @@ -2,6 +2,7 @@ import { NetworkContract } from "@/Models/Network"; // import TrainApiClient from "../lib/trainApiClient"; import { getThemeData } from "./settingsHelper"; import KnownInternalNames from "@/lib/knownIds"; +import { resolveNodes } from "@/lib/rpc/nodeResolver"; // const apiClient = new TrainApiClient() @@ -27,20 +28,22 @@ export async function getServerSideProps(context) { "solana:devnet:11111111111111111111111111111111": 150, } - // Use mock data while backend ngrok is off - const resolvedNetworks = MOCK_API_NETWORKS.map(network => { + //const resolvedNetworks = (await Promise.all(networks.map(async network => { + const resolvedNetworks = (await Promise.all(MOCK_API_NETWORKS.map(async network => { const _network = mockData.data.find(n => n.caip2Id === network.caip2Id) + const seedNodes = _network?.nodes ?? [] + const resolvedNodes = await resolveNodes(network.caip2Id, seedNodes) return { ...network, - nodes: _network?.nodes ?? [], + nodes: resolvedNodes.map(n => ({ providerName: n.providerName, url: n.url })), contracts: (_network?.contracts as NetworkContract[]) ?? [], tokens: network.tokens.map(token => ({ ...token, priceInUsd: prices[`${network.caip2Id}:${token.contractAddress}`] || 0, })), } - }).filter(n => n.nodes.length > 0 && n.contracts.length > 0) + }))).filter(n => n?.nodes?.length > 0 && n?.contracts?.length > 0) const settings = { networks: resolvedNetworks, diff --git a/apps/app/hooks/htlc/useSolverLockPolling.tsx b/apps/app/hooks/htlc/useSolverLockPolling.tsx index b8fcb5e0..88214618 100644 --- a/apps/app/hooks/htlc/useSolverLockPolling.tsx +++ b/apps/app/hooks/htlc/useSolverLockPolling.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef } from "react" import useSWR from "swr" import { Network, Token } from "@/Models/Network" import { LockDetails } from "@/Models/phtlc/PHTLC" @@ -28,6 +29,11 @@ const useSolverLockPolling = ({ onSuccess, }: UseSolverLockPollingParams) => { const type: 'erc20' | 'native' = destinationAsset?.contractAddress && destinationAsset.contractAddress !== '0x0000000000000000000000000000000000000000' ? 'erc20' : 'native' + const consensusVerified = useRef(false) + + useEffect(() => { + consensusVerified.current = false + }, [hashlock, nodeUrls]) const shouldPoll = !!(network && hashlock && contractAddress && enabled) @@ -49,8 +55,25 @@ const useSolverLockPolling = ({ solverAddress, } + const primaryUrl = nodeUrls[0] + if (!primaryUrl) return null + try { - return await client.getSolverLockDetails(params, nodeUrls) + // Regular polling: single node only + const result = await client.getSolverLockDetails(params, primaryUrl) + + if (!result) return null + + // First detection: verify with multi-node consensus + if (!consensusVerified.current && nodeUrls.length > 1) { + const verified = await client.getSolverLockDetailsWithConsensus(params, nodeUrls) + if (verified) { + consensusVerified.current = true + } + return verified + } + + return result } catch (err) { console.error('Error fetching solver lock details:', err) throw err diff --git a/apps/app/lib/balances/providers/aztecBalanceProvider.ts b/apps/app/lib/balances/providers/aztecBalanceProvider.ts index 7411346b..a4b34e53 100644 --- a/apps/app/lib/balances/providers/aztecBalanceProvider.ts +++ b/apps/app/lib/balances/providers/aztecBalanceProvider.ts @@ -6,6 +6,7 @@ import { Fr } from "@aztec/aztec.js/fields"; import { createAztecNodeClient } from "@aztec/aztec.js/node"; import { deriveStorageSlotInMap } from "@aztec/stdlib/hash"; import { formatUnits } from "viem"; +import { getNetworkRpcUrl } from "@/lib/rpc/resolveNetworkRpcUrl"; // Storage slot for public_balances map in the Token contract (slot 9 for standard Aztec token) const TOKEN_PUBLIC_BALANCES_SLOT = new Fr(9n) @@ -18,7 +19,7 @@ export class AztecBalanceProvider extends BalanceProvider { fetchBalance: BalanceProvider['fetchBalance'] = async (address, network) => { if (!address || !network?.tokens) return [] - const nodeUrl = network.nodes?.[0]?.url + const nodeUrl = getNetworkRpcUrl(network) if (!nodeUrl) return [] const client = createAztecNodeClient(nodeUrl) diff --git a/apps/app/lib/balances/providers/evmBalanceProvider.ts b/apps/app/lib/balances/providers/evmBalanceProvider.ts index 18b44b5e..50ce0af6 100644 --- a/apps/app/lib/balances/providers/evmBalanceProvider.ts +++ b/apps/app/lib/balances/providers/evmBalanceProvider.ts @@ -1,5 +1,5 @@ -import { Chain, formatUnits, PublicClient, http } from "viem" +import { Chain, formatUnits, PublicClient, http, fallback } from "viem" import { TokenBalance } from "@/Models/Balance" import { Network, Token, getNativeToken } from "@/Models/Network" import { createConfig } from '@wagmi/core' @@ -10,6 +10,7 @@ import resolveChain from "@/lib/resolveChain" import BalanceGetterAbi from "@/lib/abis/BALANCEGETTERABI.json" import KnownInternalNames from "@/lib/knownIds" import { BalanceProvider } from "@/Models/BalanceProvider" +import { getNetworkRpcUrls, buildNetworkTransport } from "@/lib/rpc/resolveNetworkRpcUrl" export class EVMBalanceProvider extends BalanceProvider { supportsNetwork: BalanceProvider['supportsNetwork'] = (network) => { @@ -38,10 +39,10 @@ export class EVMBalanceProvider extends BalanceProvider { const { createPublicClient } = await import("viem") const publicClient: PublicClient = createPublicClient({ chain, - transport: http(network.nodes?.[0]?.url, { + transport: buildNetworkTransport(network, { timeout: options?.timeoutMs, retryCount: options?.retryCount, - }) + }), }) const erc20Promise = getErc20Balances({ @@ -84,10 +85,10 @@ export class EVMBalanceProvider extends BalanceProvider { const { createPublicClient } = await import("viem") const publicClient = createPublicClient({ chain, - transport: http(network.nodes?.[0]?.url, { + transport: buildNetworkTransport(network, { timeout: options?.timeoutMs, retryCount: options?.retryCount, - }) + }), }) const contract = balanceGetterContracts.find(c => c.networks.includes(network.caip2Id)) @@ -230,7 +231,7 @@ export const getErc20Balances = async ({ const config = createConfig({ chains: [chain], transports: { - [chain.id]: http(network.nodes?.[0]?.url, { timeout: timeoutMs, retryCount }) + [chain.id]: buildNetworkTransport(network, { timeout: timeoutMs, retryCount }) } }) @@ -325,7 +326,7 @@ export const getTokenBalance = async (address: `0x${string}`, network: Network, const config = createConfig({ chains: [chain], transports: { - [chain.id]: http(network.nodes?.[0]?.url, { timeout: timeoutMs, retryCount }) + [chain.id]: buildNetworkTransport(network, { timeout: timeoutMs, retryCount }) } }) diff --git a/apps/app/lib/balances/providers/solanaBalanceProvider.ts b/apps/app/lib/balances/providers/solanaBalanceProvider.ts index aa54b230..fb065611 100644 --- a/apps/app/lib/balances/providers/solanaBalanceProvider.ts +++ b/apps/app/lib/balances/providers/solanaBalanceProvider.ts @@ -2,6 +2,7 @@ import { BalanceProvider } from "@/Models/BalanceProvider"; import { TokenBalance } from "@/Models/Balance"; import { formatUnits } from "viem"; import KnownInternalNames from "@/lib/knownIds"; +import { getNetworkRpcUrl } from "@/lib/rpc/resolveNetworkRpcUrl"; export class SolanaBalanceProvider extends BalanceProvider { supportsNetwork: BalanceProvider['supportsNetwork'] = (network) => { @@ -22,7 +23,7 @@ export class SolanaBalanceProvider extends BalanceProvider { if (!network?.tokens || !walletPublicKey) return const connection = new SolanaConnection( - `${network.nodes?.[0]?.url}`, + getNetworkRpcUrl(network), "confirmed" ); diff --git a/apps/app/lib/balances/providers/starknetBalanceProvider.ts b/apps/app/lib/balances/providers/starknetBalanceProvider.ts index ace1abd7..261c7a6e 100644 --- a/apps/app/lib/balances/providers/starknetBalanceProvider.ts +++ b/apps/app/lib/balances/providers/starknetBalanceProvider.ts @@ -3,6 +3,7 @@ import { formatUnits } from "viem"; import Erc20Abi from '@/lib/abis/ERC20.json' import KnownInternalNames from "@/lib/knownIds"; import { BalanceProvider } from "@/Models/BalanceProvider"; +import { getNetworkRpcUrl } from "@/lib/rpc/resolveNetworkRpcUrl"; export class StarknetBalanceProvider extends BalanceProvider { supportsNetwork: BalanceProvider['supportsNetwork'] = (network) => { @@ -22,7 +23,7 @@ export class StarknetBalanceProvider extends BalanceProvider { if (!network?.tokens) return const provider = new RpcProvider({ - nodeUrl: network.nodes?.[0]?.url, + nodeUrl: getNetworkRpcUrl(network), }); diff --git a/apps/app/lib/gases/providers/evmGasProvider.ts b/apps/app/lib/gases/providers/evmGasProvider.ts index cf9d9159..f89bfb1e 100644 --- a/apps/app/lib/gases/providers/evmGasProvider.ts +++ b/apps/app/lib/gases/providers/evmGasProvider.ts @@ -8,6 +8,7 @@ import { gasPriceOracleABI, gasPriceOracleAddress } from '@eth-optimism/contracts-ts' +import { buildNetworkTransport } from "../../rpc/resolveNetworkRpcUrl" const ERC20_TRANSFER_FROM_GAS_BUFFER = 65_000n @@ -27,13 +28,13 @@ export class EVMGasProvider implements GasProvider { if (!atomicContract) return try { - const { createPublicClient, http } = await import("viem") + const { createPublicClient } = await import("viem") const chain = resolveChain(network) if (!chain) return const publicClient = createPublicClient({ chain, - transport: http(network.nodes?.[0]?.url), + transport: buildNetworkTransport(network), }) const nativeToken = getNativeToken(network) diff --git a/apps/app/lib/gases/providers/solanaGasProvider.ts b/apps/app/lib/gases/providers/solanaGasProvider.ts index 66b56730..0ed869f5 100644 --- a/apps/app/lib/gases/providers/solanaGasProvider.ts +++ b/apps/app/lib/gases/providers/solanaGasProvider.ts @@ -1,6 +1,7 @@ import { GasProps } from "../../../Models/Balance"; import { Network, getNativeToken, NetworkContractType } from "../../../Models/Network"; import { formatUnits } from "viem"; +import { getNetworkRpcUrl } from "../../rpc/resolveNetworkRpcUrl"; export class SolanaGasProvider { supportsNetwork(network: Network): boolean { @@ -18,7 +19,7 @@ export class SolanaGasProvider { try { const lamports = await estimateSolanaGas({ - rpcUrl: network.nodes?.[0]?.url ?? '', + rpcUrl: getNetworkRpcUrl(network), contractAddress: atomicContract, address, tokenSymbol: token.symbol, diff --git a/apps/app/lib/gases/providers/starknetGasProvider.ts b/apps/app/lib/gases/providers/starknetGasProvider.ts index 31c81a4f..4a304f01 100644 --- a/apps/app/lib/gases/providers/starknetGasProvider.ts +++ b/apps/app/lib/gases/providers/starknetGasProvider.ts @@ -1,6 +1,7 @@ import { GasProps } from "../../../Models/Balance" import { Network, getNativeToken, NetworkContractType } from "../../../Models/Network" import { GasProvider } from "./types" +import { getNetworkRpcUrl } from "../../rpc/resolveNetworkRpcUrl" export class StarknetGasProvider implements GasProvider { supportsNetwork(network: Network): boolean { @@ -12,7 +13,7 @@ export class StarknetGasProvider implements GasProvider { if (!account || !network) return - const rpcUrl = network.nodes?.[0]?.url + const rpcUrl = getNetworkRpcUrl(network) const contractAddress = network.contracts?.find(c => c.type === NetworkContractType.Train)?.address const nativeToken = getNativeToken(network) diff --git a/apps/app/lib/resolveChain.ts b/apps/app/lib/resolveChain.ts index c2710c84..4ce46a12 100644 --- a/apps/app/lib/resolveChain.ts +++ b/apps/app/lib/resolveChain.ts @@ -3,6 +3,7 @@ import { Network, getNativeToken } from "../Models/Network"; import NetworkSettings from "./NetworkSettings"; import { SendErrorMessage } from "./telegram"; import { chainConfig } from 'viem/op-stack' +import { getNetworkRpcUrl } from "./rpc/resolveNetworkRpcUrl"; export default function resolveChain(network: Network, customRpcUrl?: string) { @@ -18,8 +19,8 @@ export default function resolveChain(network: Network, customRpcUrl?: string) { const opStackChainConfig = Number(network.chainId) == 10 ? chainConfig : {} - // Use custom RPC URL if provided, otherwise use the network's default RPC - const rpcUrl = customRpcUrl || network.nodes?.[0]?.url; + // Use custom RPC URL if provided, otherwise use the network's effective RPC + const rpcUrl = customRpcUrl || getNetworkRpcUrl(network); const res = defineChain({ id: Number(network.chainId), diff --git a/apps/app/lib/rpc/evmNodes.ts b/apps/app/lib/rpc/evmNodes.ts new file mode 100644 index 00000000..cbac63e5 --- /dev/null +++ b/apps/app/lib/rpc/evmNodes.ts @@ -0,0 +1,45 @@ +import type { ResolvedNode } from './types' + +function extractProviderName(url: string): string { + try { + const parts = new URL(url).hostname.split('.') + return parts.length >= 2 ? parts[parts.length - 2] : parts[0] + } catch { + return 'unknown' + } +} + +/** + * Resolve EVM RPC nodes for a given chainId from chainlist-rpcs. + * Returns up to 3 HTTPS endpoints with no tracking or limited tracking. + */ +export async function resolveEvmNodes(chainId: string): Promise { + const { get_rpcs_for_chain } = await import('chainlist-rpcs') + const rpcs = get_rpcs_for_chain({ + chain_id: Number(chainId), + allowed_tracking: ['none', 'limited'], + }) + + if (!Array.isArray(rpcs)) return [] + + const seen = new Set() + const results: ResolvedNode[] = [] + + for (const entry of rpcs) { + const url = typeof entry === 'string' ? entry : entry.url + if ( + typeof url === 'string' && + url.startsWith('https://') && + !url.includes('${') + ) { + const normalized = url.replace(/\/+$/, '') + const key = normalized.toLowerCase() + if (!seen.has(key)) { + seen.add(key) + results.push({ url: normalized, providerName: extractProviderName(url) }) + } + } + } + + return results +} diff --git a/apps/app/lib/rpc/nodeResolver.ts b/apps/app/lib/rpc/nodeResolver.ts new file mode 100644 index 00000000..23b1441c --- /dev/null +++ b/apps/app/lib/rpc/nodeResolver.ts @@ -0,0 +1,48 @@ +import type { ResolvedNode, ChainNamespace } from './types' +import { NON_EVM_NODES } from './nonEvmNodes' +import { resolveEvmNodes } from './evmNodes' + +/** + * Resolves RPC node URLs for a given CAIP-2 network ID. + * Existing nodes (from API/mock) get priority, then public RPCs are appended. + */ +export async function resolveNodes( + caip2Id: string, + existingNodes?: Array<{ url: string; providerName: string }>, +): Promise { + const [namespace, chainId] = caip2Id.split(':') as [ChainNamespace, string] + const seen = new Set() + const results: ResolvedNode[] = [] + + const add = (node: ResolvedNode) => { + const key = node.url.replace(/\/+$/, '').toLowerCase() + if (!seen.has(key)) { + seen.add(key) + results.push(node) + } + } + + // 1. Existing nodes first (known to work with this app) + if (existingNodes?.length) { + for (const n of existingNodes) { + add({ url: n.url, providerName: n.providerName }) + } + } + + // 2. Dynamic resolution by chain type + if (namespace === 'eip155') { + try { + for (const n of await resolveEvmNodes(chainId)) { + add(n) + } + } catch (e) { + console.warn(`[resolveNodes] Failed to resolve dynamic EVM nodes for ${caip2Id}:`, e) + } + } else { + for (const n of NON_EVM_NODES[caip2Id] ?? []) { + add(n) + } + } + + return results +} diff --git a/apps/app/lib/rpc/nonEvmNodes.ts b/apps/app/lib/rpc/nonEvmNodes.ts new file mode 100644 index 00000000..37beec87 --- /dev/null +++ b/apps/app/lib/rpc/nonEvmNodes.ts @@ -0,0 +1,44 @@ +import type { ResolvedNode } from './types' + +/** + * Curated public RPC endpoints for non-EVM chains. + * Keyed by CAIP-2 ID. + */ +export const NON_EVM_NODES: Record = { + // ── Solana ── + 'solana:mainnet': [ + { url: 'https://api.mainnet-beta.solana.com', providerName: 'solana-official' }, + { url: 'https://solana-rpc.publicnode.com', providerName: 'publicnode' }, + { url: 'https://rpc.ankr.com/solana', providerName: 'ankr' }, + ], + 'solana:devnet': [ + { url: 'https://api.devnet.solana.com', providerName: 'solana-official' }, + ], + 'solana:testnet': [ + { url: 'https://api.testnet.solana.com', providerName: 'solana-official' }, + ], + + // ── Starknet ── + 'starknet:SN_MAIN': [ + { url: 'https://starknet-mainnet-rpc.publicnode.com', providerName: 'publicnode' }, + { url: 'https://free-rpc.nethermind.io/mainnet-juno/', providerName: 'nethermind' }, + { url: 'https://rpc.starknet.lava.build', providerName: 'lava' }, + ], + 'starknet:SN_SEPOLIA': [ + { url: 'https://starknet-sepolia-rpc.publicnode.com', providerName: 'publicnode' }, + { url: 'https://free-rpc.nethermind.io/sepolia-juno/', providerName: 'nethermind' }, + ], + + // ── Aztec ── + 'aztec:aztec-devnet': [ + { url: 'https://v4-devnet-2.aztec-labs.com', providerName: 'aztec-labs' }, + ], + + // ── TON ── + 'ton:mainnet': [ + { url: 'https://toncenter.com/api/v2/jsonRPC', providerName: 'toncenter' }, + ], + 'ton:testnet': [ + { url: 'https://testnet.toncenter.com/api/v2/jsonRPC', providerName: 'toncenter-testnet' }, + ], +} diff --git a/apps/app/lib/rpc/resolveNetworkRpcUrl.ts b/apps/app/lib/rpc/resolveNetworkRpcUrl.ts new file mode 100644 index 00000000..66ba8eb4 --- /dev/null +++ b/apps/app/lib/rpc/resolveNetworkRpcUrl.ts @@ -0,0 +1,37 @@ +import { useRpcConfigStore } from '@/stores/rpcConfigStore' +import { Network } from '@/Models/Network' +import { http, fallback, type Transport } from 'viem' + +/** + * Get the effective RPC URLs for a network, respecting user overrides. + * Works outside React (balance providers, gas providers) via Zustand getState(). + */ +export function getNetworkRpcUrls(network: Network): string[] { + const store = useRpcConfigStore.getState() + return store.getEffectiveRpcUrls(network) +} + +/** + * Get the primary effective RPC URL (first in priority order). + */ +export function getNetworkRpcUrl(network: Network): string { + return getNetworkRpcUrls(network)[0] ?? '' +} + +/** + * Build a viem Transport with fallback across all effective RPC URLs. + * Throws if no RPC URLs are available for the network. + */ +export function buildNetworkTransport( + network: Network, + options?: { timeout?: number; retryCount?: number }, +): Transport { + const urls = getNetworkRpcUrls(network) + if (urls.length === 0) { + throw new Error(`No RPC URLs available for network ${network.caip2Id}`) + } + if (urls.length > 1) { + return fallback(urls.map(u => http(u, options))) + } + return http(urls[0], options) +} diff --git a/apps/app/lib/rpc/types.ts b/apps/app/lib/rpc/types.ts new file mode 100644 index 00000000..432d5ec2 --- /dev/null +++ b/apps/app/lib/rpc/types.ts @@ -0,0 +1,9 @@ +import type { NetworkNode } from '@train-protocol/sdk' + +/** + * A resolved RPC node. Identical to NetworkNode so the result can be + * assigned directly to Network.nodes without mapping. + */ +export type ResolvedNode = NetworkNode + +export type ChainNamespace = 'eip155' | 'solana' | 'starknet' | 'aztec' | 'ton' diff --git a/apps/app/lib/wallets/ton/client.ts b/apps/app/lib/wallets/ton/client.ts deleted file mode 100644 index 11323aa7..00000000 --- a/apps/app/lib/wallets/ton/client.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TonClient } from "@ton/ton"; - -const tonClient = new TonClient({ - endpoint: process.env.NEXT_PUBLIC_API_VERSION == 'sandbox' ? 'https://testnet.toncenter.com/api/v2/jsonRPC' : 'https://toncenter.com/api/v2/jsonRPC', - apiKey: process.env.NEXT_PUBLIC_TON_API_KEY, -}); - -export default tonClient; \ No newline at end of file diff --git a/apps/app/next.config.ts b/apps/app/next.config.ts index f97b8cf4..f2e8a4ee 100644 --- a/apps/app/next.config.ts +++ b/apps/app/next.config.ts @@ -2,7 +2,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { reactStrictMode: true, - transpilePackages: ["@aztec/wallet-sdk", "@train-protocol/sdk", "@train-protocol/aztec"], + transpilePackages: ["@aztec/wallet-sdk", "@train-protocol/sdk", "@train-protocol/aztec", "chainlist-rpcs"], productionBrowserSourceMaps: true, images: { remotePatterns: [ diff --git a/apps/app/package.json b/apps/app/package.json index 244dfbea..5799f9cf 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -26,6 +26,7 @@ "@defi-wonderland/aztec-standards": "4.0.0-devnet.2-patch.1", "@badrap/bar-of-progress": "^0.2.2", "@cartridge/controller": "^0.9.2", + "chainlist-rpcs": "^0.5.0", "@coral-xyz/anchor": "catalog:", "@eth-optimism/contracts-ts": "^0.15.0", "@imtbl/sdk": "1.45.10", diff --git a/apps/app/stores/rpcConfigStore.ts b/apps/app/stores/rpcConfigStore.ts index f31825c1..b0ab627b 100644 --- a/apps/app/stores/rpcConfigStore.ts +++ b/apps/app/stores/rpcConfigStore.ts @@ -2,6 +2,15 @@ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' import { Network } from '../Models/Network' +function isValidRpcUrl(url: string): boolean { + try { + const parsed = new URL(url) + return parsed.protocol === 'https:' || parsed.protocol === 'wss:' + } catch { + return false + } +} + export interface RpcConfig { customRpcUrls: string[] // Changed to array to support multiple URLs useCustomRpc: boolean @@ -40,6 +49,8 @@ export const useRpcConfigStore = create()( customRpcUrls = [config.customRpcUrl] } + customRpcUrls = customRpcUrls.filter(isValidRpcUrl) + return { rpcConfigs: { ...state.rpcConfigs, @@ -110,6 +121,7 @@ export const useRpcConfigStore = create()( }, addRpcUrl: (networkId: string, url: string) => { + if (!isValidRpcUrl(url)) return set((state) => { const config = state.rpcConfigs[networkId] || { customRpcUrls: [], useCustomRpc: true } return { @@ -149,6 +161,7 @@ export const useRpcConfigStore = create()( }, updateRpcUrl: (networkId: string, index: number, url: string) => { + if (!isValidRpcUrl(url)) return set((state) => { const config = state.rpcConfigs[networkId] if (!config || !config.customRpcUrls) return state diff --git a/packages/blockchains/IntegrationRules.md b/packages/blockchains/IntegrationRules.md index faab0d40..edbab97f 100644 --- a/packages/blockchains/IntegrationRules.md +++ b/packages/blockchains/IntegrationRules.md @@ -131,6 +131,8 @@ export class {Chain}HTLCClient extends HTLCClient { super(config.apiClient) // Always pass apiClient to base this.rpc = ... this.signer = config.signer + // Override default consensus options if needed (e.g., Aztec: minQuorum 1) + // this.consensusOptions = { minQuorum: 1 } } // ── Write Operations ─────────────────────────────────────────────── @@ -141,7 +143,7 @@ export class {Chain}HTLCClient extends HTLCClient { // ── Read Operations ──────────────────────────────────────────────── - async _getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { ... } + async getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { ... } async getUserLockDetails(params: LockParams): Promise { ... } async recoverSwap(txHash: string): Promise { ... } @@ -155,7 +157,7 @@ export class {Chain}HTLCClient extends HTLCClient { ### Ordering rules 1. **Write operations first** — `userLock` → `refund` → `redeemSolver` -2. **Read operations second** — `getUserLockDetails` → `_getSolverLockDetails` → `recoverSwap` +2. **Read operations second** — `getUserLockDetails` → `getSolverLockDetails` → `recoverSwap` 3. **Private helpers last** — `requireSigner()` first, then chain-specific utilities 4. **Use section comments** — `// ── Write Operations ───...` separator style between groups @@ -163,8 +165,26 @@ export class {Chain}HTLCClient extends HTLCClient { The base `HTLCClient` class provides these methods — subclasses should **not** override them: -- `getSolverLockDetails(params, nodeUrls)` — queries multiple nodes via `_getSolverLockDetails`, validates results match across nodes - `revealSecret(solverId, hashlock, secret)` — delegates to `apiClient.revealSecret()` +- `getSolverLockDetailsWithConsensus(params, nodeUrls, options?)` — queries multiple nodes via `getSolverLockDetails`, validates results match across nodes (see below) + +### Cross-node consensus + +The base class provides `getSolverLockDetailsWithConsensus()` which fans out `getSolverLockDetails()` to multiple RPC nodes and validates that all successful responses agree on critical fields (`amount`, `sender`, `recipient`, `token`, `timelock`). + +**Consensus options:** +- The base class sets `protected consensusOptions: ConsensusOptions = { minQuorum: 2 }` by default +- Subclasses can override this in their constructor (e.g., Aztec sets `minQuorum: 1` since it typically has fewer public nodes) +- Per-call `options` passed to `getSolverLockDetailsWithConsensus()` take priority over the instance default + +**How it works:** +1. Queries all `nodeUrls` in parallel via `Promise.allSettled` +2. Filters for non-null results +3. Requires at least `minQuorum` agreeing results (capped to `nodeUrls.length`) +4. Compares critical fields across all valid results — throws if they disagree +5. Returns the first valid result if consensus passes + +Chain implementations only need to implement the single-node abstract method `getSolverLockDetails(params, nodeUrl)`. --- @@ -215,12 +235,12 @@ try { 3. If `txId` is provided, fetch transaction logs to extract `userData` (nonce) 4. Return `LockDetails` object with all fields mapped -### _getSolverLockDetails — Count-Then-Loop Pattern +### getSolverLockDetails — Count-Then-Loop Pattern -**This is a critical shared pattern.** The base class calls `_getSolverLockDetails` for each node URL and verifies results match. Your subclass implements the single-node version. The contract stores multiple solver locks per hashlock. Always: +**This is a critical shared pattern.** The base class calls `getSolverLockDetails` for each node URL and verifies results match via `getSolverLockDetailsWithConsensus()`. Your subclass implements the single-node version. The contract stores multiple solver locks per hashlock. Always: ```ts -async _getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { +async getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { // 1. Get the count of solver locks for this hashlock const count = /* call getSolverLockCount(hashlock) */ @@ -364,6 +384,7 @@ import { AtomicResult, RecoveredSwapData, BaseHTLCClientConfig, + ConsensusOptions, } from '@train-protocol/sdk' // Key derivation @@ -407,6 +428,7 @@ describe('register{Chain}Sdk', () => { }) expect(typeof client.getUserLockDetails).toBe('function') expect(typeof client.getSolverLockDetails).toBe('function') + expect(typeof client.getSolverLockDetailsWithConsensus).toBe('function') expect(typeof client.userLock).toBe('function') expect(typeof client.refund).toBe('function') expect(typeof client.redeemSolver).toBe('function') @@ -436,7 +458,8 @@ const ZERO_ADDRESS = '0x000...' // Chain's empty/zero address representation - [ ] Add `declare module '@train-protocol/sdk'` augmentation for `HTLCClientConfigMap` and `WalletSignConfigMap` - [ ] Implement `{Chain}HTLCClient extends HTLCClient` in `client.ts` - [ ] Follow function ordering: writes → reads → private helpers -- [ ] Implement count-then-loop pattern in `_getSolverLockDetails` (1-indexed, single-node version) +- [ ] Implement count-then-loop pattern in `getSolverLockDetails` (1-indexed, single-node version) +- [ ] Set `this.consensusOptions` in constructor if chain needs non-default quorum (default: `minQuorum: 2`) - [ ] Validate `txHash` format at the top of `recoverSwap` before any RPC calls - [ ] Define `{Chain}WalletLike` minimal interface in `login/wallet-sign.ts` - [ ] Implement key derivation in `login/wallet-sign.ts` using `deriveKeyMaterial` + `IDENTITY_SALT` diff --git a/packages/blockchains/aztec/src/client.ts b/packages/blockchains/aztec/src/client.ts index 019411f5..b764af46 100644 --- a/packages/blockchains/aztec/src/client.ts +++ b/packages/blockchains/aztec/src/client.ts @@ -36,6 +36,7 @@ export class AztecHTLCClient extends HTLCClient { super(config.apiClient) this.rpcUrl = config.rpcUrl this.signer = config.signer + this.consensusOptions = { minQuorum: 1, batchSize: 1 } } async userLock(params: UserLockParams): Promise { @@ -245,7 +246,7 @@ export class AztecHTLCClient extends HTLCClient { } } - async _getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { + async getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { const signer = this.requireSigner() const { id, contractAddress } = params const { contract, userAztecAddress } = await this.getContractInstance(contractAddress, signer, nodeUrl) diff --git a/packages/blockchains/evm/src/client.ts b/packages/blockchains/evm/src/client.ts index ea103a6d..71188f7d 100644 --- a/packages/blockchains/evm/src/client.ts +++ b/packages/blockchains/evm/src/client.ts @@ -181,7 +181,7 @@ export class EvmHTLCClient extends HTLCClient { } } - async _getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { + async getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { const { id, contractAddress } = params const rpc = new JsonRpcClient(nodeUrl) diff --git a/packages/blockchains/solana/src/client.ts b/packages/blockchains/solana/src/client.ts index 93b73549..33a047fc 100644 --- a/packages/blockchains/solana/src/client.ts +++ b/packages/blockchains/solana/src/client.ts @@ -220,7 +220,7 @@ export class SolanaHTLCClient extends HTLCClient { } } - async _getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { + async getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { const { contractAddress, id } = params if (!contractAddress) throw new Error('No contract address') diff --git a/packages/blockchains/starknet/src/client.ts b/packages/blockchains/starknet/src/client.ts index 24fea092..9525eb9b 100644 --- a/packages/blockchains/starknet/src/client.ts +++ b/packages/blockchains/starknet/src/client.ts @@ -144,7 +144,7 @@ export class StarknetHTLCClient extends HTLCClient { } } - async _getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { + async getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { const { id, contractAddress } = params const provider = new RpcProvider({ nodeUrl }) const contract = this.createContract(contractAddress, provider) diff --git a/packages/blockchains/ton/src/client.ts b/packages/blockchains/ton/src/client.ts index 1b8571ef..350e5883 100644 --- a/packages/blockchains/ton/src/client.ts +++ b/packages/blockchains/ton/src/client.ts @@ -183,7 +183,7 @@ export class TonHTLCClient extends HTLCClient { } } - async _getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { + async getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { const { id, contractAddress } = params const rpc = TonRpcClient.fromUrl(nodeUrl, this.apiKey) diff --git a/packages/sdk/src/types/htlc-client.ts b/packages/sdk/src/types/htlc-client.ts index f1112243..fed501fc 100644 --- a/packages/sdk/src/types/htlc-client.ts +++ b/packages/sdk/src/types/htlc-client.ts @@ -9,7 +9,8 @@ export type BaseHTLCClientConfig = { export interface IHTLCClient { getUserLockDetails(params: LockParams): Promise - getSolverLockDetails(params: LockParams, nodeUrls: string[]): Promise + getSolverLockDetails(params: LockParams, nodeUrl: string): Promise + getSolverLockDetailsWithConsensus(params: LockParams, nodeUrls: string[], options?: ConsensusOptions): Promise recoverSwap(txHash: string): Promise userLock(params: UserLockParams): Promise @@ -20,6 +21,7 @@ export interface IHTLCClient { export abstract class HTLCClient implements IHTLCClient { protected apiClient: TrainApiClient + protected consensusOptions: ConsensusOptions = { minQuorum: 2, batchSize: 3 } constructor(apiClient: TrainApiClient) { this.apiClient = apiClient @@ -29,26 +31,81 @@ export abstract class HTLCClient implements IHTLCClient { return this.apiClient.revealSecret(solverId, hashlock, secret) } - async getSolverLockDetails(params: LockParams, nodeUrls: string[]): Promise { - const results = await Promise.all( - nodeUrls.map(url => this._getSolverLockDetails(params, url)) - ) + async getSolverLockDetailsWithConsensus( + params: LockParams, + nodeUrls: string[], + options?: ConsensusOptions + ): Promise { + const { minQuorum, batchSize } = { ...this.consensusOptions, ...options } as Required + + if (!nodeUrls.length) return null + + const effectiveQuorum = Math.min(minQuorum, nodeUrls.length) + + // Partition nodeUrls into batches + const batches: string[][] = [] + for (let i = 0; i < nodeUrls.length; i += batchSize) { + batches.push(nodeUrls.slice(i, i + batchSize)) + } + + let totalValid = 0 + let totalQueried = 0 + let lastError: unknown = null + + for (const batch of batches) { + const results = await Promise.allSettled( + batch.map(url => this.getSolverLockDetails(params, url)) + ) - const validResults = results.filter((r): r is LockDetails => r !== null) - if (!validResults.length) return null + totalQueried += batch.length - const [first, ...rest] = validResults - if (!rest.every(r => r.amount === first.amount && r.sender === first.sender && r.recipient === first.recipient && r.token === first.token && r.timelock === first.timelock)) { - throw new Error('Lock details do not match across the provided nodes') + const fulfilled = results.filter( + (r): r is PromiseFulfilledResult => r.status === 'fulfilled' + ) + const validResults = fulfilled.map(r => r.value).filter((r): r is LockDetails => r !== null) + + totalValid += validResults.length + + const batchError = results.find( + (r): r is PromiseRejectedResult => r.status === 'rejected' + ) + if (batchError) lastError = batchError.reason + + if (validResults.length >= effectiveQuorum) { + const [first, ...rest] = validResults + if (rest.length > 0 && !rest.every(r => + String(r.amount) === String(first.amount) && + r.sender === first.sender && + r.recipient === first.recipient && + r.token === first.token && + r.timelock === first.timelock + )) { + throw new Error('Lock details do not match across the provided nodes') + } + return first + } } - return first + // All batches exhausted + if (totalValid === 0) { + if (lastError) throw lastError + return null + } + + throw new Error( + `Insufficient node agreement: ${totalValid} of ${totalQueried} nodes returned results, need at least ${effectiveQuorum}` + ) } abstract getUserLockDetails(params: LockParams): Promise - abstract _getSolverLockDetails(params: LockParams, nodeUrl: string): Promise + abstract getSolverLockDetails(params: LockParams, nodeUrl: string): Promise abstract recoverSwap(txHash: string): Promise abstract userLock(params: UserLockParams): Promise abstract refund(params: RefundParams): Promise abstract redeemSolver(params: RedeemSolverParams): Promise } + +export interface ConsensusOptions { + minQuorum?: number + batchSize?: number +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77598242..bb794158 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -233,6 +233,9 @@ importers: bn.js: specifier: ^5.2.0 version: 5.2.3 + chainlist-rpcs: + specifier: ^0.5.0 + version: 0.5.270 clsx: specifier: ^1.2.1 version: 1.2.1 @@ -319,7 +322,7 @@ importers: version: 9.0.1 viem: specifier: 'catalog:' - version: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4) wagmi: specifier: 2.14.11 version: 2.14.11(@react-native-async-storage/async-storage@1.24.0)(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@18.3.1))(@types/react@18.3.28)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) @@ -6620,6 +6623,9 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chainlist-rpcs@0.5.270: + resolution: {integrity: sha512-7rgQr7TVMFNlOQBSvy7Z92Urawvvha7jFwGsx4Qhgv7MyXDz1riTYbKRCQ6exjd1MlhIWu1wocxIIth6QnK15Q==} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -11542,7 +11548,7 @@ snapshots: '@aztec/noir-types': 4.0.0-devnet.2-patch.2 '@aztec/simulator': 4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@aztec/stdlib': 4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@aztec/telemetry-client': 4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@aztec/telemetry-client': 4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@aztec/world-state': 4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) commander: 12.1.0 pako: 2.1.0 @@ -12100,7 +12106,7 @@ snapshots: lodash.omit: 4.5.0 sha3: 2.1.4 tslib: 2.8.1 - viem: '@aztec/viem@2.38.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)' + viem: '@aztec/viem@2.38.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)' transitivePeerDependencies: - '@types/pg' - aws-crt @@ -12124,7 +12130,7 @@ snapshots: '@aztec/noir-types': 4.0.0-devnet.2-patch.2 '@aztec/protocol-contracts': 4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@aztec/stdlib': 4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@aztec/telemetry-client': 4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@aztec/telemetry-client': 4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@aztec/world-state': 4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lodash.clonedeep: 4.5.0 lodash.merge: 4.6.2 @@ -12243,6 +12249,38 @@ snapshots: - typescript - utf-8-validate + '@aztec/telemetry-client@4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@aztec/foundation': 4.0.0-devnet.2-patch.2 + '@aztec/stdlib': 4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.55.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/host-metrics': 0.36.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-gcp': 0.32.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + prom-client: 15.1.3 + viem: '@aztec/viem@2.38.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)' + transitivePeerDependencies: + - '@types/pg' + - aws-crt + - bufferutil + - debug + - encoding + - pg-native + - supports-color + - typescript + - utf-8-validate + - zod + '@aztec/telemetry-client@4.0.0-devnet.2-patch.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@aztec/foundation': 4.0.0-devnet.2-patch.2 @@ -12326,6 +12364,23 @@ snapshots: - typescript - utf-8-validate + '@aztec/viem@2.38.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.3)(zod@3.25.76) + isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.9.3) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + '@aztec/viem@2.38.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/curves': 1.9.1 @@ -15544,7 +15599,7 @@ snapshots: dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - typescript @@ -15566,7 +15621,7 @@ snapshots: dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - typescript @@ -15590,7 +15645,7 @@ snapshots: '@reown/appkit-wallet': 1.7.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.21.8(@react-native-async-storage/async-storage@1.24.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) valtio: 1.13.2(@types/react@18.3.28)(react@18.3.1) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -15625,7 +15680,7 @@ snapshots: '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.21.8(@react-native-async-storage/async-storage@1.24.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) valtio: 1.13.2(@types/react@18.3.28)(react@18.3.1) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -15849,7 +15904,7 @@ snapshots: '@walletconnect/logger': 2.1.2 '@walletconnect/universal-provider': 2.21.8(@react-native-async-storage/async-storage@1.24.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) valtio: 1.13.2(@types/react@18.3.28)(react@18.3.1) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -15887,7 +15942,7 @@ snapshots: '@walletconnect/logger': 2.1.2 '@walletconnect/universal-provider': 2.21.8(@react-native-async-storage/async-storage@1.24.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) valtio: 1.13.2(@types/react@18.3.28)(react@18.3.1) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -15951,7 +16006,7 @@ snapshots: '@walletconnect/universal-provider': 2.21.8(@react-native-async-storage/async-storage@1.24.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) bs58: 6.0.0 valtio: 1.13.2(@types/react@18.3.28)(react@18.3.1) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -15994,7 +16049,7 @@ snapshots: '@walletconnect/universal-provider': 2.21.8(@react-native-async-storage/async-storage@1.24.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) bs58: 6.0.0 valtio: 1.13.2(@types/react@18.3.28)(react@18.3.1) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -16129,7 +16184,7 @@ snapshots: '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.23.1 - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - typescript @@ -18822,7 +18877,7 @@ snapshots: '@turnkey/crypto': 2.3.1 '@turnkey/encoding': 0.4.0 optionalDependencies: - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - typescript @@ -19273,7 +19328,7 @@ snapshots: '@wagmi/core': 2.16.4(@tanstack/query-core@5.90.20)(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(viem@2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) '@walletconnect/ethereum-provider': 2.21.8(@react-native-async-storage/async-storage@1.24.0)(@types/react@18.3.28)(bufferutil@4.1.0)(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -19308,7 +19363,7 @@ snapshots: dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) zustand: 5.0.0(@types/react@18.3.28)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) optionalDependencies: '@tanstack/query-core': 5.90.20 @@ -19323,7 +19378,7 @@ snapshots: dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) zustand: 5.0.0(@types/react@18.3.28)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) optionalDependencies: '@tanstack/query-core': 5.90.20 @@ -20661,6 +20716,8 @@ snapshots: chai@6.2.2: {} + chainlist-rpcs@0.5.270: {} + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -23499,7 +23556,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -23514,7 +23571,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -23529,7 +23586,22 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - zod + + ox@0.9.6(typescript@5.9.3): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -25284,6 +25356,23 @@ snapshots: - utf-8-validate - zod + viem@2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) + isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + ox: 0.11.3(typescript@5.9.3)(zod@3.22.4) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + viem@2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4): dependencies: '@noble/curves': 1.9.1 @@ -25379,7 +25468,7 @@ snapshots: '@wagmi/core': 2.16.4(@tanstack/query-core@5.90.20)(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) react: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1) - viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: